Allow accessibilityOrder to reference itself (#51004)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/51004

It would be very convenient if `accessibilityOrder` could reference itself. Meaning the View with the `accessibilityOrder` prop can include its own `nativeID` in the array. This makes sense API wise - we allow for referencing parents and their descendants, so long as they are treated as an element and not a container. This is pretty nice since you no longer have to wrap everything in a View who's sole purpose is `accessibilityOrder`.

Under the hood things get a bit garbled, however, since iOS only lets you have UIViews that are either accessibility elements or accessibility containers - and we need to support both at the same time for this to work. To do this, we make use of the `UIAccessibilityElement` class and just forward all of the logic to the View with the `accessibilityOrder` prop. This View will also not be an accessibility element from the point of view of iOS.

Changelog: [Internal]

Reviewed By: jorge-cab

Differential Revision: D73792934

fbshipit-source-id: b0810277c8e410319639b863b59e4e60782bffca
This commit is contained in:
Joe Vilches
2025-04-30 15:33:35 -07:00
committed by Facebook GitHub Bot
parent 62742e21c9
commit 4d8eeb34f9
5 changed files with 172 additions and 23 deletions
@@ -9,6 +9,7 @@
#import "RCTParagraphComponentAccessibilityProvider.h"
#import <MobileCoreServices/UTCoreTypes.h>
#import <React/RCTViewAccessibilityElement.h>
#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
#import <react/renderer/components/text/ParagraphProps.h>
#import <react/renderer/components/text/ParagraphState.h>
@@ -212,23 +213,26 @@ using namespace facebook::react;
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
} else if ([((RCTViewComponentView *)ancestor) wantsToCooptLabel]) {
// We found an view that is looking to coopt a label below it
[cooptingCandidates addObject:ancestor];
}
NSArray *elements = ancestor.accessibilityElements;
if ([elements count] > 0 && [cooptingCandidates count] > 0) {
for (NSObject *element in elements) {
if ([element isKindOfClass:[UIView class]] && [cooptingCandidates containsObject:((UIView *)element)]) {
return YES;
} else if (
[element isKindOfClass:[RCTViewAccessibilityElement class]] &&
[cooptingCandidates containsObject:((RCTViewAccessibilityElement *)element).view]) {
return YES;
}
}
}
} 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.
@@ -0,0 +1,28 @@
/*
* 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 "RCTViewComponentView.h"
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/*
* A UIAcccessibilityElement representing a RCTViewComponentView from an
* accessibility standpoint. This enables RCTViewComponentView's to reference
* themselves in `accessibilityElements` without actually being an accessibility
* element. If it were, then iOS would not call into `accessibilityElements`.
*/
@interface RCTViewAccessibilityElement : UIAccessibilityElement
@property (readonly) RCTViewComponentView *view;
- (instancetype)initWithView:(RCTViewComponentView *)view;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,83 @@
/*
* 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 "RCTViewAccessibilityElement.h"
@implementation RCTViewAccessibilityElement
- (instancetype)initWithView:(RCTViewComponentView *)view
{
if (self = [super initWithAccessibilityContainer:view]) {
_view = view;
}
return self;
}
- (CGRect)accessibilityFrame
{
return UIAccessibilityConvertFrameToScreenCoordinates(_view.bounds, _view);
}
#pragma mark - Forwarding to _view
- (NSString *)accessibilityLabel
{
return _view.accessibilityLabel;
}
- (NSString *)accessibilityValue
{
return _view.accessibilityValue;
}
- (UIAccessibilityTraits)accessibilityTraits
{
return _view.accessibilityTraits;
}
- (NSString *)accessibilityHint
{
return _view.accessibilityHint;
}
- (BOOL)accessibilityIgnoresInvertColors
{
return _view.accessibilityIgnoresInvertColors;
}
- (BOOL)shouldGroupAccessibilityChildren
{
return _view.shouldGroupAccessibilityChildren;
}
- (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
{
return _view.accessibilityCustomActions;
}
- (NSString *)accessibilityLanguage
{
return _view.accessibilityLanguage;
}
- (BOOL)accessibilityViewIsModal
{
return _view.accessibilityViewIsModal;
}
- (BOOL)accessibilityElementsHidden
{
return _view.accessibilityElementsHidden;
}
- (BOOL)accessibilityRespondsToUserInteraction
{
return _view.accessibilityRespondsToUserInteraction;
}
@end
@@ -81,6 +81,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSString *)accessibilityLabelForCoopting;
/*
* This View has no label and will look to coopt something below it
*/
- (BOOL)wantsToCooptLabel;
/*
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
*/
@@ -6,6 +6,7 @@
*/
#import "RCTViewComponentView.h"
#import "RCTViewAccessibilityElement.h"
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
@@ -49,7 +50,8 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
UIView *_containerView;
BOOL _useCustomContainerView;
NSMutableArray<NSString *> *_accessibleElementsNativeIds;
NSMutableSet<NSString *> *_accessibilityOrderNativeIDs;
RCTViewAccessibilityElement *_axElementDescribingSelf;
}
#ifdef RCT_DYNAMIC_FRAMEWORKS
@@ -391,11 +393,15 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
}
}
// `accessibilityOrder`
if (oldViewProps.accessibilityOrder != newViewProps.accessibilityOrder &&
ReactNativeFeatureFlags::enableAccessibilityOrder()) {
_accessibleElementsNativeIds = [NSMutableArray new];
// Creating a set since a lot of logic requires lookups in here. However,
// we still need to preserve the orginal order. So just read from props
// if need to access that
_accessibilityOrderNativeIDs = [NSMutableSet new];
for (const std::string &childId : newViewProps.accessibilityOrder) {
[_accessibleElementsNativeIds addObject:RCTNSStringFromString(childId)];
[_accessibilityOrderNativeIDs addObject:RCTNSStringFromString(childId)];
}
}
@@ -1141,20 +1147,31 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
- (NSArray<NSObject *> *)accessibilityElements
{
if ([_accessibleElementsNativeIds count] <= 0) {
if ([_accessibilityOrderNativeIDs 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];
[RCTViewComponentView collectAccessibilityElements:self
intoDictionary:nativeIdToView
nativeIds:_accessibilityOrderNativeIDs];
NSMutableArray<UIView *> *elements = [NSMutableArray new];
for (NSString *childId : _accessibleElementsNativeIds) {
UIView *viewWithMatchingNativeId = [nativeIdToView objectForKey:childId];
if (viewWithMatchingNativeId) {
[elements addObject:viewWithMatchingNativeId];
NSMutableArray<NSObject *> *elements = [NSMutableArray new];
for (auto childId : _props->accessibilityOrder) {
NSString *nsStringChildId = RCTNSStringFromString(childId);
// Special case to allow for self-referencing with accessibilityOrder
if (nsStringChildId == self.nativeId) {
if (!_axElementDescribingSelf) {
_axElementDescribingSelf = [[RCTViewAccessibilityElement alloc] initWithView:self];
}
_axElementDescribingSelf.isAccessibilityElement = [super isAccessibilityElement];
[elements addObject:_axElementDescribingSelf];
} else {
UIView *viewWithMatchingNativeId = [nativeIdToView objectForKey:nsStringChildId];
if (viewWithMatchingNativeId) {
[elements addObject:viewWithMatchingNativeId];
}
}
}
@@ -1211,12 +1228,24 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
return super.accessibilityLabel;
}
- (BOOL)wantsToCooptLabel
{
return !super.accessibilityLabel && super.isAccessibilityElement;
}
- (BOOL)isAccessibilityElement
{
if (self.contentView != nil) {
return self.contentView.isAccessibilityElement;
}
// If we reference ourselves in accessibilityOrder then we will make a
// UIAccessibilityElement object to represent ourselves since returning YES
// here would mean iOS would not call into accessibilityElements
if ([_accessibilityOrderNativeIDs containsObject:self.nativeId]) {
return NO;
}
return [super isAccessibilityElement];
}