Skip to content

Fix multi-line link accessibility paths#561

Merged
meherkasam-square merged 1 commit intosquare:mainfrom
meherkasam-square:meher/text-links-2
Jun 24, 2025
Merged

Fix multi-line link accessibility paths#561
meherkasam-square merged 1 commit intosquare:mainfrom
meherkasam-square:meher/text-links-2

Conversation

@meherkasam-square
Copy link
Contributor

@meherkasam-square meherkasam-square commented Jun 17, 2025

This change is a fast-follow to this PR. The previous PR improved how multi-line links were exposed to VoiceOver and Full Keyboard Access. However, it was flawed in that it would only expose the first fragment. This PR fixes that by encompassing the entire link inside a single bezier path.

Demo

(For the "Before" view, refer to the description in this PR.)

VoiceOver

VoiceOver.MP4

Full keyboard access

Full.Keyboard.Access.mov

@soroushsq
Copy link
Contributor

Can you base this PR off of meherkasam-square:meher/text-links? That way we can the diff more cleanly

@meherkasam-square meherkasam-square marked this pull request as ready for review June 19, 2025 07:25
@meherkasam-square meherkasam-square requested a review from a team as a code owner June 19, 2025 07:25
@meherkasam-square meherkasam-square force-pushed the meher/text-links-2 branch 2 times, most recently from 3c08978 to 0f68690 Compare June 23, 2025 18:43
Copy link
Contributor

@soroushsq soroushsq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ok to me, would love for @RoyalPineapple to take a look as well

Comment on lines +669 to +672
hasher.combine(firstRect.origin.x)
hasher.combine(firstRect.origin.y)
hasher.combine(firstRect.size.width)
hasher.combine(firstRect.size.height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So annoying that CGRect isn't Hashable, what's that about

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seriously! I was originally thinking of making an extension but I realised that I would be pushing that extension to every downstream dependency and potentially cause build errors.

Comment on lines 700 to 719
let path: UIBezierPath? = {
// In cases where a link overflows from one line to the next,
// we will get back more than one rect. If we had used the boundingRect
// function instead, it will only return us a bounding rect that can
// fully encompass all the sub-rects - this is undesired behavior as
// it will suppress focusable items within the label that have an
// overlap with the larger bounding box.
guard rects.count > 1 else { return nil }

let cgPath = CGMutablePath()
let cornerRadius: CGFloat = 4.0

for rect in rects {
cgPath.addRoundedRect(in: rect, cornerWidth: cornerRadius, cornerHeight: cornerRadius)
}
return .init(cgPath: cgPath)
}()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This to me looks it could be expressed well with a reduce

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Updated.

return .init(cgPath: cgPath)
}()

return .init(firstRect: rects[0], path: path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any chance rect could be empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to the rects array? I am struggling to think of a situation where it would be empty. The only one that comes to mind is a malformed markdown string that has an empty string for a linked portion. Line 698 would address this possible edge case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, rects, sorry

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might feel cleaner if this unchecked rects[0] were instead baked into the above guard statement. I like to do something like this.

guard let first = rects.first else { return .init(firstRect: .zero) }

so that we can later say

return .init(firstRect: first, path: path)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes that's good

Copy link
Collaborator

@RoyalPineapple RoyalPineapple left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all makes sense, one minor stylistic suggestion.
I cant wait unitl we can expose these in snapshots tests.

@meherkasam-square meherkasam-square merged commit 2334742 into square:main Jun 24, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants