Allow links that encorporate entire <Text> to be keyboard accessible

Summary:
I was helping a team with some keyboard link accessibility and they had this strange bug where a link was not keyboard accessible despite having `accessible={true}`. Something that fixed it for them was adding an `accessibilityLabel`, which makes no sense.

Turns out there is a bug when the link encompasses the entirety of the `Text` component. We have a special case for handling links of this manner, and right now we are not setting the `frame` property which is responsible for outlining the element when it takes focus.

This property is not one defined by UIKit, rather it is defined in RCTAccessibilityElement. We use it to derive the accessibilityFrame which has to be in screen coordinates. If you do not set the frame property then it is supposed to use the bounds of the container. This does not work properly, seemingly bc the name "frame" gets mangled internally in UIKit leading to UB. I changed the name the "something" and it works.

To remedy this lets just change the name

Changelog: [iOS] [Fixed]

Reviewed By: javache

Differential Revision: D72338838

fbshipit-source-id: da91d0c28baeb6765d4d604f3e47c91952167f9d
This commit is contained in:
Joe Vilches
2025-04-07 14:24:15 -07:00
committed by Facebook GitHub Bot
parent b0f2083d9d
commit 83fae860df
3 changed files with 8 additions and 8 deletions
@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
*
* Default value: `CGRectZero`.
*/
@property (nonatomic, assign) CGRect frame;
@property (nonatomic, assign) CGRect frameInContainerSpace;
@end
NS_ASSUME_NONNULL_END
@@ -12,10 +12,10 @@
- (CGRect)accessibilityFrame
{
UIView *container = (UIView *)self.accessibilityContainer;
if (CGRectEqualToRect(_frame, CGRectZero)) {
if (CGRectEqualToRect(_frameInContainerSpace, CGRectZero)) {
return UIAccessibilityConvertFrameToScreenCoordinates(container.bounds, container);
} else {
return UIAccessibilityConvertFrameToScreenCoordinates(_frame, container);
return UIAccessibilityConvertFrameToScreenCoordinates(_frameInContainerSpace, container);
}
}
@@ -62,20 +62,18 @@ using namespace facebook::react;
accessibilityLabel = RCTNSStringFromString(_attributedString.getString());
}
// add first element has the text for the whole textview in order to read out the whole text
RCTAccessibilityElement *firstElement =
[[RCTAccessibilityElement alloc] initWithAccessibilityContainer:_view.superview];
RCTAccessibilityElement *firstElement = [[RCTAccessibilityElement alloc] initWithAccessibilityContainer:_view];
firstElement.isAccessibilityElement = YES;
firstElement.accessibilityTraits = _view.accessibilityTraits;
firstElement.accessibilityLabel = accessibilityLabel;
firstElement.accessibilityLanguage = _view.accessibilityLanguage;
firstElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(_view.bounds, _view);
[firstElement setAccessibilityActivationPoint:CGPointMake(
firstElement.accessibilityFrame.origin.x + 1.0,
firstElement.accessibilityFrame.origin.y + 1.0)];
[elements addObject:firstElement];
// add additional elements for those parts of text with embedded link so VoiceOver could specially recognize links
[_layoutManager getRectWithAttributedString:_attributedString
paragraphAttributes:_paragraphAttributes
enumerateAttribute:RCTTextAttributesAccessibilityRoleAttributeName
@@ -84,6 +82,7 @@ using namespace facebook::react;
if ((![value isEqualToString:@"button"] && ![value isEqualToString:@"link"])) {
return;
}
if ([fragmentText isEqualToString:firstElement.accessibilityLabel]) {
if ([value isEqualToString:@"link"]) {
firstElement.accessibilityTraits |= UIAccessibilityTraitLink;
@@ -93,6 +92,7 @@ using namespace facebook::react;
// The fragment is the entire paragraph. This is handled as `firstElement`.
return;
}
if ([value isEqualToString:@"button"] &&
([fragmentText isEqualToString:@"See Less"] ||
[fragmentText isEqualToString:@"See More"])) {
@@ -110,7 +110,7 @@ using namespace facebook::react;
numberOfButtons++;
}
element.accessibilityLabel = fragmentText;
element.frame = fragmentRect;
element.frameInContainerSpace = fragmentRect;
[elements addObject:element];
}];