diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index 0350d8adcdd317..79a025dcec5825 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -45,6 +45,7 @@ type Props = $ReadOnly<{| accessibilityLabel?: ?Stringish, accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'), accessibilityRole?: ?AccessibilityRole, + accessibilitySplitFocus?: ?boolean, accessibilityState?: ?AccessibilityState, accessibilityValue?: ?AccessibilityValue, accessibilityViewIsModal?: ?boolean, diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 3a89c55d142d1b..977117436a9ad5 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -158,6 +158,7 @@ class TouchableBounce extends React.Component { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + accessibilitySplitFocus={this.props.accessibilitySplitFocus} nativeID={this.props.nativeID} testID={this.props.testID} hitSlop={this.props.hitSlop} diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 350ea535e11fc3..4e0948e91a2eae 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -305,6 +305,7 @@ class TouchableHighlight extends React.Component { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + accessibilitySplitFocus={this.props.accessibilitySplitFocus} style={StyleSheet.compose( this.props.style, this.state.extraStyles?.underlay, diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js index 5dc03df8770fd8..428f2253900cfe 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -281,6 +281,7 @@ class TouchableNativeFeedback extends React.Component { accessibilityLiveRegion: this.props.accessibilityLiveRegion, accessibilityViewIsModal: this.props.accessibilityViewIsModal, accessibilityElementsHidden: this.props.accessibilityElementsHidden, + accessibilitySplitFocus: this.props.accessibilitySplitFocus, hasTVPreferredFocus: this.props.hasTVPreferredFocus, hitSlop: this.props.hitSlop, focusable: diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index a621a7192d41a8..b9a8832f194ea3 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -234,6 +234,7 @@ class TouchableOpacity extends React.Component { accessibilityLiveRegion={this.props.accessibilityLiveRegion} accessibilityViewIsModal={this.props.accessibilityViewIsModal} accessibilityElementsHidden={this.props.accessibilityElementsHidden} + accessibilitySplitFocus={this.props.accessibilitySplitFocus} style={[this.props.style, {opacity: this.state.anim}]} nativeID={this.props.nativeID} testID={this.props.testID} diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 0cb20d0dbad078..beb9da6aa1d747 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -41,6 +41,7 @@ type Props = $ReadOnly<{| accessibilityLabel?: ?Stringish, accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'), accessibilityRole?: ?AccessibilityRole, + accessibilitySplitFocus?: ?boolean, accessibilityState?: ?AccessibilityState, accessibilityValue?: ?AccessibilityValue, accessibilityViewIsModal?: ?boolean, @@ -80,6 +81,7 @@ const PASSTHROUGH_PROPS = [ 'accessibilityLabel', 'accessibilityLiveRegion', 'accessibilityRole', + 'accessibilitySplitFocus', 'accessibilityState', 'accessibilityValue', 'accessibilityViewIsModal', diff --git a/Libraries/Components/View/ReactNativeViewViewConfig.js b/Libraries/Components/View/ReactNativeViewViewConfig.js index 4d6f0dd0a6ce10..a66a0c99a3bd23 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfig.js +++ b/Libraries/Components/View/ReactNativeViewViewConfig.js @@ -122,6 +122,7 @@ const ReactNativeViewConfig = { accessibilityLabel: true, accessibilityLiveRegion: true, accessibilityRole: true, + accessibilitySplitFocus: true, accessibilityStates: true, // TODO: Can be removed after next release accessibilityState: true, accessibilityValue: true, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 497d94cff18b5a..6385b321db8d54 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -366,6 +366,15 @@ type IOSViewProps = $ReadOnly<{| */ accessibilityElementsHidden?: ?boolean, + /** + * A value indicating whether the focus of a group of nested accessibility elements + * can be captured separately from their parent element. + * + * @platform ios + * + */ + accessibilitySplitFocus?: ?boolean, + /** * Whether this `View` should be rendered as a bitmap before compositing. * diff --git a/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js index c008361311a7a2..251459ff33d784 100644 --- a/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js +++ b/Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js @@ -153,6 +153,15 @@ module.exports = { */ accessibilityElementsHidden: PropTypes.bool, + /** + * A value indicating whether the focus of a group of nested accessibility elements + * can be captured separately from their parent element. + * + * @platform ios + * + */ + accessibilitySplitFocus: PropTypes.bool, + /** * When `accessible` is true, the system will try to invoke this function * when the user performs an accessibility custom action. diff --git a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js index 1ca8e44f54f679..654b51d46eb64a 100644 --- a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js +++ b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js @@ -11,7 +11,7 @@ 'use strict'; const React = require('react'); -const {Text, View, Alert} = require('react-native'); +const {Text, View, Alert, TouchableWithoutFeedback} = require('react-native'); const RNTesterBlock = require('../../components/RNTesterBlock'); @@ -55,6 +55,27 @@ class AccessibilityIOSExample extends React.Component { This view's children are hidden from the accessibility tree + + Outer Element + + + First Inner Element + + + + + Second Inner Element + + + ); } diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 7b86d088ae66e9..7fc05e58c8ad2a 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -20,6 +20,11 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @interface RCTView : UIView +/** + * Accessibility properties + */ +@property (nonatomic, assign) BOOL shouldAbandonAccessibilityFocus; + /** * Accessibility event handlers */ diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 91e28ab95ec664..d826b956064fcb 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -208,11 +208,28 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + BOOL result = NO; if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { - return [super pointInside:point withEvent:event]; + result = [super pointInside:point withEvent:event]; + } else { + CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); + result = CGRectContainsPoint(hitFrame, point); } - CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); - return CGRectContainsPoint(hitFrame, point); + + if (result && self.accessibilitySplitFocus) { + NSArray *sortedSubviews = [self reactZIndexSortedSubviews]; + UIView *hitSubview = nil; + for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) { + CGPoint convertedPoint = [subview convertPoint:point fromView:self]; + hitSubview = [subview hitTest:convertedPoint withEvent:event]; + if (hitSubview != nil) { + break; + } + } + _shouldAbandonAccessibilityFocus = hitSubview != nil && [hitSubview isAccessibilityElement]; + } + + return result; } #pragma mark - Accessibility @@ -383,7 +400,11 @@ - (UIView *)reactAccessibilityElement - (BOOL)isAccessibilityElement { if (self.reactAccessibilityElement == self) { - return [super isAccessibilityElement]; + if ([super isAccessibilityElement]) { + return self.accessibilitySplitFocus ? !_shouldAbandonAccessibilityFocus : YES; + } else { + return NO; + } } return NO; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 8b948eb5eb9ab8..b66d17df3d7b3f 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -142,6 +142,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(onMagicTap, reactAccessibilityElement.onMagicTap, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(onAccessibilityEscape, reactAccessibilityElement.onAccessibilityEscape, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString) +RCT_EXPORT_VIEW_PROPERTY(accessibilitySplitFocus, BOOL) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 67c665b2bc712b..98c30b9df1729d 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -120,6 +120,7 @@ @property (nonatomic, copy) NSDictionary *accessibilityState; @property (nonatomic, copy) NSArray *accessibilityActions; @property (nonatomic, copy) NSDictionary *accessibilityValueInternal; +@property (nonatomic, assign) BOOL accessibilitySplitFocus; /** * Used in debugging to get a description of the view hierarchy rooted at diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 60b30d9a111b1f..c45426d731a660 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -354,6 +354,16 @@ - (void)setAccessibilityValueInternal:(NSDictionary *)accessibil self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (BOOL)accessibilitySplitFocus +{ + return [objc_getAssociatedObject(self, _cmd) boolValue]; +} + +- (void)setAccessibilitySplitFocus:(BOOL)accessibilitySplitFocus +{ + objc_setAssociatedObject(self, @selector(accessibilitySplitFocus), @(accessibilitySplitFocus), OBJC_ASSOCIATION_ASSIGN); +} + #pragma mark - Debug - (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level {