diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index d76a755d99e..ca1b63f8b1f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -9,6 +9,7 @@ #import "RCTParagraphComponentAccessibilityProvider.h" #import +#import #import #import #import @@ -212,23 +213,26 @@ using namespace facebook::react; NSMutableSet *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. diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.h new file mode 100644 index 00000000000..cb7566e1b32 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.h @@ -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 + +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 diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.mm new file mode 100644 index 00000000000..5af6a99c932 --- /dev/null +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewAccessibilityElement.mm @@ -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 *)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 diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h index ac13fca3221..a3c58cb16f5 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -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. */ diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5000b921701..d8c416133c4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -6,6 +6,7 @@ */ #import "RCTViewComponentView.h" +#import "RCTViewAccessibilityElement.h" #import #import @@ -49,7 +50,8 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f; NSSet *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN; UIView *_containerView; BOOL _useCustomContainerView; - NSMutableArray *_accessibleElementsNativeIds; + NSMutableSet *_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 *)accessibilityElements { - if ([_accessibleElementsNativeIds count] <= 0) { + if ([_accessibilityOrderNativeIDs count] <= 0) { return super.accessibilityElements; } NSMutableDictionary *nativeIdToView = [NSMutableDictionary new]; - NSSet *nativeIdSet = [[NSSet alloc] initWithArray:_accessibleElementsNativeIds]; - [RCTViewComponentView collectAccessibilityElements:self intoDictionary:nativeIdToView nativeIds:nativeIdSet]; + [RCTViewComponentView collectAccessibilityElements:self + intoDictionary:nativeIdToView + nativeIds:_accessibilityOrderNativeIDs]; - NSMutableArray *elements = [NSMutableArray new]; - for (NSString *childId : _accessibleElementsNativeIds) { - UIView *viewWithMatchingNativeId = [nativeIdToView objectForKey:childId]; - if (viewWithMatchingNativeId) { - [elements addObject:viewWithMatchingNativeId]; + NSMutableArray *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]; }