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 e0040aecd67..e934ce626da 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -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 *elements = _accessibilityProvider.accessibilityElements; + if ([elements count] > 0) { + elements[0].isAccessibilityElement = ![self isAccessibilityCoopted]; + } + return elements; +} + +- (BOOL)isAccessibilityCoopted +{ + UIView *ancestor = self.superview; + 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 + [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 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 b9b2b995d27..ac13fca3221 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h @@ -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. */ 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 2679c8695f6..0eb3b14933b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -18,7 +18,6 @@ #import #import #import -#import #import #import #import @@ -604,23 +603,6 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f; _reactSubviews = [NSMutableArray new]; } -- (NSArray *)accessibilityElements -{ - if ([_accessibleElementsNativeIds count] <= 0) { - return super.accessibilityElements; - } - - NSMutableArray *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 *_Nullable)props { _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props; @@ -1136,6 +1118,41 @@ static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle) return self; } +- (NSArray *)accessibilityElements +{ + if ([_accessibleElementsNativeIds count] <= 0) { + return super.accessibilityElements; + } + + NSMutableDictionary *nativeIdToView = [NSMutableDictionary new]; + NSSet *nativeIdSet = [[NSSet alloc] initWithArray:_accessibleElementsNativeIds]; + + [RCTViewComponentView collectAccessibilityElements:self intoDictionary:nativeIdToView nativeIds:nativeIdSet]; + + NSMutableArray *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 *)dict + nativeIds:(NSSet *)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) { diff --git a/packages/react-native/React/Fabric/Utils/RCTViewFinder.h b/packages/react-native/React/Fabric/Utils/RCTViewFinder.h deleted file mode 100644 index cbeac5b9fbe..00000000000 --- a/packages/react-native/React/Fabric/Utils/RCTViewFinder.h +++ /dev/null @@ -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 - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTViewFinder : NSObject - -+ (UIView *)findView:(UIView *)root withNativeId:(NSString *)nativeId; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Utils/RCTViewFinder.mm b/packages/react-native/React/Fabric/Utils/RCTViewFinder.mm deleted file mode 100644 index e6406846ce7..00000000000 --- a/packages/react-native/React/Fabric/Utils/RCTViewFinder.mm +++ /dev/null @@ -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 - -@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