mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Improve accessibilityOrder algo on iOS (#50265)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
701859b397
commit
33bd90e07c
+44
-1
@@ -160,6 +160,11 @@ using namespace facebook::react;
|
||||
return self.attributedText.string;
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityLabelForCoopting
|
||||
{
|
||||
return self.accessibilityLabel;
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityElement
|
||||
{
|
||||
// All accessibility functionality of the component is implemented in `accessibilityElements` method below.
|
||||
@@ -196,7 +201,45 @@ using namespace facebook::react;
|
||||
}
|
||||
}
|
||||
|
||||
return _accessibilityProvider.accessibilityElements;
|
||||
NSArray<UIAccessibilityElement *> *elements = _accessibilityProvider.accessibilityElements;
|
||||
if ([elements count] > 0) {
|
||||
elements[0].isAccessibilityElement = ![self isAccessibilityCoopted];
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityCoopted
|
||||
{
|
||||
UIView *ancestor = self.superview;
|
||||
NSMutableSet<UIView *> *cooptingCandidates = [NSMutableSet new];
|
||||
while (ancestor) {
|
||||
if ([ancestor isKindOfClass:[RCTViewComponentView class]]) {
|
||||
NSArray *elements = ancestor.accessibilityElements;
|
||||
if ([elements count] > 0 && [cooptingCandidates count] > 0) {
|
||||
for (UIView *element in elements) {
|
||||
if ([cooptingCandidates containsObject:element]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ([((RCTViewComponentView *)ancestor) accessibilityLabelForCoopting]) {
|
||||
// We found a label above us. That would be coopted before we would be
|
||||
return NO;
|
||||
} else if (ancestor.isAccessibilityElement) {
|
||||
// We found an accessible view without a label for coopting before anything
|
||||
// else, if it is in some accessibilityElements somewhere then it will coopt
|
||||
[cooptingCandidates addObject:ancestor];
|
||||
}
|
||||
} else if (![ancestor isKindOfClass:[RCTViewComponentView class]] && ancestor.accessibilityLabel) {
|
||||
// Same as above, for UIView case. Cannot call this on RCTViewComponentView
|
||||
// as it is recursive and quite expensive.
|
||||
return NO;
|
||||
}
|
||||
ancestor = ancestor.superview;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (UIAccessibilityTraits)accessibilityTraits
|
||||
|
||||
@@ -76,6 +76,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)prepareForRecycle NS_REQUIRES_SUPER;
|
||||
- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event;
|
||||
|
||||
/*
|
||||
* This is the label that would be coopted by another element
|
||||
*/
|
||||
- (NSString *)accessibilityLabelForCoopting;
|
||||
|
||||
/*
|
||||
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
|
||||
*/
|
||||
|
||||
+40
-18
@@ -18,7 +18,6 @@
|
||||
#import <React/RCTConversions.h>
|
||||
#import <React/RCTLinearGradient.h>
|
||||
#import <React/RCTLocalizedString.h>
|
||||
#import <React/RCTViewFinder.h>
|
||||
#import <react/featureflags/ReactNativeFeatureFlags.h>
|
||||
#import <react/renderer/components/view/ViewComponentDescriptor.h>
|
||||
#import <react/renderer/components/view/ViewEventEmitter.h>
|
||||
@@ -604,23 +603,6 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
||||
_reactSubviews = [NSMutableArray new];
|
||||
}
|
||||
|
||||
- (NSArray<NSObject *> *)accessibilityElements
|
||||
{
|
||||
if ([_accessibleElementsNativeIds count] <= 0) {
|
||||
return super.accessibilityElements;
|
||||
}
|
||||
|
||||
NSMutableArray<UIView *> *elements = [NSMutableArray new];
|
||||
for (NSString *childId : _accessibleElementsNativeIds) {
|
||||
UIView *viewWithMatchingNativeId = [RCTViewFinder findView:self withNativeId:childId];
|
||||
if (viewWithMatchingNativeId) {
|
||||
[elements addObject:viewWithMatchingNativeId];
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
|
||||
{
|
||||
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props;
|
||||
@@ -1136,6 +1118,41 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSObject *> *)accessibilityElements
|
||||
{
|
||||
if ([_accessibleElementsNativeIds count] <= 0) {
|
||||
return super.accessibilityElements;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSString *, UIView *> *nativeIdToView = [NSMutableDictionary new];
|
||||
NSSet<NSString *> *nativeIdSet = [[NSSet alloc] initWithArray:_accessibleElementsNativeIds];
|
||||
|
||||
[RCTViewComponentView collectAccessibilityElements:self intoDictionary:nativeIdToView nativeIds:nativeIdSet];
|
||||
|
||||
NSMutableArray<UIView *> *elements = [NSMutableArray new];
|
||||
for (NSString *childId : _accessibleElementsNativeIds) {
|
||||
UIView *viewWithMatchingNativeId = [nativeIdToView objectForKey:childId];
|
||||
if (viewWithMatchingNativeId) {
|
||||
[elements addObject:viewWithMatchingNativeId];
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
+ (void)collectAccessibilityElements:(UIView *)view
|
||||
intoDictionary:(NSMutableDictionary<NSString *, UIView *> *)dict
|
||||
nativeIds:(NSSet<NSString *> *)nativeIds
|
||||
{
|
||||
for (UIView *subview in view.subviews) {
|
||||
if ([subview isKindOfClass:[RCTViewComponentView class]] &&
|
||||
[nativeIds containsObject:((RCTViewComponentView *)subview).nativeId]) {
|
||||
[dict setObject:subview forKey:((RCTViewComponentView *)subview).nativeId];
|
||||
}
|
||||
[RCTViewComponentView collectAccessibilityElements:subview intoDictionary:dict nativeIds:nativeIds];
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
||||
{
|
||||
// Result string is initialized lazily to prevent useless but costly allocations.
|
||||
@@ -1168,6 +1185,11 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
||||
return RCTRecursiveAccessibilityLabel(self.currentContainerView);
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityLabelForCoopting
|
||||
{
|
||||
return super.accessibilityLabel;
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityElement
|
||||
{
|
||||
if (self.contentView != nil) {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RCTViewFinder : NSObject
|
||||
|
||||
+ (UIView *)findView:(UIView *)root withNativeId:(NSString *)nativeId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import "RCTViewFinder.h"
|
||||
#include <React/RCTViewComponentView.h>
|
||||
|
||||
@implementation RCTViewFinder
|
||||
|
||||
+ (UIView *)findView:(UIView *)root withNativeId:(NSString *)nativeId
|
||||
{
|
||||
if (!nativeId) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([root isKindOfClass:[RCTViewComponentView class]] &&
|
||||
[nativeId isEqualToString:((RCTViewComponentView *)root).nativeId]) {
|
||||
return root;
|
||||
}
|
||||
|
||||
for (UIView *subview in root.subviews) {
|
||||
UIView *result = [RCTViewFinder findView:subview withNativeId:nativeId];
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user