Skip to content

Commit 33bd90e

Browse files
joevilchesfacebook-github-bot
authored andcommitted
Improve accessibilityOrder algo on iOS (facebook#50265)
Summary: Pull Request resolved: facebook#50265 This is a fun one! "Improvements" consist of * Performance is better now. Previously we did a tree walk for each ID in the array, now its just one :) * Properly handles coopting (more on that below) **Performance** The previous implementation naively walked the tree until it found the right nativeId for each nativeId in the prop. This new algo just does a single tree walk and collects the views that have the right nativeIds as we are doing that walk. **Coopting** Our iOS code implements a form of accessibility coopting, where an element can "speak for" a descendant. This happens when some parent element does not have an accessibility label but a descendant does. We look at the subtree and grab every node that has a label and lift it up to the aforementioned element without a label. This enables some nice a11y features like wrapping `Text` in a `View` and letting the `View` just read all the `Text` inside (imagine a button with a label, you would only want to focus the button and just read the text instead of the text itself). This feature is nice but it becomes buggy when we introduce `accessibilityOrder`. Previously, there was no way to access nested elements on iOS, the platform prohibits this. However, you can get around this by using `accessibilityElements`, which our `accessibilityOrder` prop maps to. So you could define the order as `['parent', 'child']` and access both elements just fine. However, if that `parent` is a `View` that coopts `Text`, we have some issues. The `View` will read the `Text` but then when the user swipes we focus the `Text` and read it again! To get around this we check up the superview chain in RCTParagraphViewComponentView looking for Views that might coopt us and a cooresponding accessibilityElements with said candidates. If there is such a View we do not announce ourselves. Performance is iffy here, we need to iterate up to root for all text focusing, but this should be fairly fast for all intents and purposes and I have not noticed any lag when changing focus ordering. Changelog: [Internal] Reviewed By: jorge-cab Differential Revision: D71562476 fbshipit-source-id: 31fd935df0764459403464bd645aae2e664c69cb
1 parent 701859b commit 33bd90e

File tree

5 files changed

+89
-71
lines changed

5 files changed

+89
-71
lines changed

packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ - (NSString *)accessibilityLabel
160160
return self.attributedText.string;
161161
}
162162

163+
- (NSString *)accessibilityLabelForCoopting
164+
{
165+
return self.accessibilityLabel;
166+
}
167+
163168
- (BOOL)isAccessibilityElement
164169
{
165170
// All accessibility functionality of the component is implemented in `accessibilityElements` method below.
@@ -196,7 +201,45 @@ - (NSArray *)accessibilityElements
196201
}
197202
}
198203

199-
return _accessibilityProvider.accessibilityElements;
204+
NSArray<UIAccessibilityElement *> *elements = _accessibilityProvider.accessibilityElements;
205+
if ([elements count] > 0) {
206+
elements[0].isAccessibilityElement = ![self isAccessibilityCoopted];
207+
}
208+
return elements;
209+
}
210+
211+
- (BOOL)isAccessibilityCoopted
212+
{
213+
UIView *ancestor = self.superview;
214+
NSMutableSet<UIView *> *cooptingCandidates = [NSMutableSet new];
215+
while (ancestor) {
216+
if ([ancestor isKindOfClass:[RCTViewComponentView class]]) {
217+
NSArray *elements = ancestor.accessibilityElements;
218+
if ([elements count] > 0 && [cooptingCandidates count] > 0) {
219+
for (UIView *element in elements) {
220+
if ([cooptingCandidates containsObject:element]) {
221+
return YES;
222+
}
223+
}
224+
}
225+
226+
if ([((RCTViewComponentView *)ancestor) accessibilityLabelForCoopting]) {
227+
// We found a label above us. That would be coopted before we would be
228+
return NO;
229+
} else if (ancestor.isAccessibilityElement) {
230+
// We found an accessible view without a label for coopting before anything
231+
// else, if it is in some accessibilityElements somewhere then it will coopt
232+
[cooptingCandidates addObject:ancestor];
233+
}
234+
} else if (![ancestor isKindOfClass:[RCTViewComponentView class]] && ancestor.accessibilityLabel) {
235+
// Same as above, for UIView case. Cannot call this on RCTViewComponentView
236+
// as it is recursive and quite expensive.
237+
return NO;
238+
}
239+
ancestor = ancestor.superview;
240+
}
241+
242+
return NO;
200243
}
201244

202245
- (UIAccessibilityTraits)accessibilityTraits

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ NS_ASSUME_NONNULL_BEGIN
7676
- (void)prepareForRecycle NS_REQUIRES_SUPER;
7777
- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event;
7878

79+
/*
80+
* This is the label that would be coopted by another element
81+
*/
82+
- (NSString *)accessibilityLabelForCoopting;
83+
7984
/*
8085
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
8186
*/

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
#import <React/RCTConversions.h>
1919
#import <React/RCTLinearGradient.h>
2020
#import <React/RCTLocalizedString.h>
21-
#import <React/RCTViewFinder.h>
2221
#import <react/featureflags/ReactNativeFeatureFlags.h>
2322
#import <react/renderer/components/view/ViewComponentDescriptor.h>
2423
#import <react/renderer/components/view/ViewEventEmitter.h>
@@ -604,23 +603,6 @@ - (void)prepareForRecycle
604603
_reactSubviews = [NSMutableArray new];
605604
}
606605

607-
- (NSArray<NSObject *> *)accessibilityElements
608-
{
609-
if ([_accessibleElementsNativeIds count] <= 0) {
610-
return super.accessibilityElements;
611-
}
612-
613-
NSMutableArray<UIView *> *elements = [NSMutableArray new];
614-
for (NSString *childId : _accessibleElementsNativeIds) {
615-
UIView *viewWithMatchingNativeId = [RCTViewFinder findView:self withNativeId:childId];
616-
if (viewWithMatchingNativeId) {
617-
[elements addObject:viewWithMatchingNativeId];
618-
}
619-
}
620-
621-
return elements;
622-
}
623-
624606
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
625607
{
626608
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props;
@@ -1136,6 +1118,41 @@ - (NSObject *)accessibilityElement
11361118
return self;
11371119
}
11381120

1121+
- (NSArray<NSObject *> *)accessibilityElements
1122+
{
1123+
if ([_accessibleElementsNativeIds count] <= 0) {
1124+
return super.accessibilityElements;
1125+
}
1126+
1127+
NSMutableDictionary<NSString *, UIView *> *nativeIdToView = [NSMutableDictionary new];
1128+
NSSet<NSString *> *nativeIdSet = [[NSSet alloc] initWithArray:_accessibleElementsNativeIds];
1129+
1130+
[RCTViewComponentView collectAccessibilityElements:self intoDictionary:nativeIdToView nativeIds:nativeIdSet];
1131+
1132+
NSMutableArray<UIView *> *elements = [NSMutableArray new];
1133+
for (NSString *childId : _accessibleElementsNativeIds) {
1134+
UIView *viewWithMatchingNativeId = [nativeIdToView objectForKey:childId];
1135+
if (viewWithMatchingNativeId) {
1136+
[elements addObject:viewWithMatchingNativeId];
1137+
}
1138+
}
1139+
1140+
return elements;
1141+
}
1142+
1143+
+ (void)collectAccessibilityElements:(UIView *)view
1144+
intoDictionary:(NSMutableDictionary<NSString *, UIView *> *)dict
1145+
nativeIds:(NSSet<NSString *> *)nativeIds
1146+
{
1147+
for (UIView *subview in view.subviews) {
1148+
if ([subview isKindOfClass:[RCTViewComponentView class]] &&
1149+
[nativeIds containsObject:((RCTViewComponentView *)subview).nativeId]) {
1150+
[dict setObject:subview forKey:((RCTViewComponentView *)subview).nativeId];
1151+
}
1152+
[RCTViewComponentView collectAccessibilityElements:subview intoDictionary:dict nativeIds:nativeIds];
1153+
}
1154+
}
1155+
11391156
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
11401157
{
11411158
// Result string is initialized lazily to prevent useless but costly allocations.
@@ -1168,6 +1185,11 @@ - (NSString *)accessibilityLabel
11681185
return RCTRecursiveAccessibilityLabel(self.currentContainerView);
11691186
}
11701187

1188+
- (NSString *)accessibilityLabelForCoopting
1189+
{
1190+
return super.accessibilityLabel;
1191+
}
1192+
11711193
- (BOOL)isAccessibilityElement
11721194
{
11731195
if (self.contentView != nil) {

packages/react-native/React/Fabric/Utils/RCTViewFinder.h

Lines changed: 0 additions & 18 deletions
This file was deleted.

packages/react-native/React/Fabric/Utils/RCTViewFinder.mm

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)