From 83fae860df8d1ac2d89b3cddf8c595b2cc88a74f Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Mon, 7 Apr 2025 14:24:15 -0700 Subject: [PATCH] Allow links that encorporate entire 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 --- .../ComponentViews/Text/RCTAccessibilityElement.h | 2 +- .../ComponentViews/Text/RCTAccessibilityElement.mm | 4 ++-- .../Text/RCTParagraphComponentAccessibilityProvider.mm | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h index 7d06fb61ba8..2b7c14fa25a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.h @@ -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 diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm index 7ce44a3f174..60dfc922f9e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTAccessibilityElement.mm @@ -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); } } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm index 31f7ed37837..c2cc3884264 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentAccessibilityProvider.mm @@ -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]; }];