From 6d3d4d95e4aa4eb1519429ae5e219ff8847b1a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AF=BB=E6=AC=A2?= <5747483+BangtanBoys@user.noreply.gitee.com> Date: Mon, 15 Jun 2020 17:53:07 +0800 Subject: [PATCH 1/5] feat: add support for splitting focus on nested views of iOS * feat: add support for splitting focus on nested views of iOS --- Libraries/Components/Pressable/Pressable.js | 1 + .../Components/Touchable/TouchableBounce.js | 1 + .../Touchable/TouchableHighlight.js | 1 + .../Touchable/TouchableNativeFeedback.js | 1 + .../Components/Touchable/TouchableOpacity.js | 1 + .../Touchable/TouchableWithoutFeedback.js | 2 ++ .../View/ReactNativeViewViewConfig.js | 1 + Libraries/Components/View/ViewPropTypes.js | 9 +++++++ .../DeprecatedViewPropTypes.js | 9 +++++++ .../Accessibility/AccessibilityIOSExample.js | 17 +++++++++++- React/Views/RCTView.h | 5 ++++ React/Views/RCTView.m | 27 ++++++++++++++++--- React/Views/RCTViewManager.m | 1 + React/Views/UIView+React.h | 1 + React/Views/UIView+React.m | 10 +++++++ 15 files changed, 83 insertions(+), 4 deletions(-) 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..fc0725d50ed8a3 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,21 @@ 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..f4712821e1bcad 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]; } CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); - return CGRectContainsPoint(hitFrame, point); + result = 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; + } + + 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..b6b07fa42850d8 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 { From 3e6bf3ba9a3ba4d8445640c942d849350fd8598e Mon Sep 17 00:00:00 2001 From: AKB48 Date: Mon, 15 Jun 2020 21:41:12 +0800 Subject: [PATCH 2/5] fix: code style --- .../js/examples/Accessibility/AccessibilityIOSExample.js | 7 ++++--- React/Views/RCTView.m | 8 ++++---- React/Views/UIView+React.m | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js index fc0725d50ed8a3..d7fff31d5ef269 100644 --- a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js +++ b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js @@ -55,9 +55,10 @@ class AccessibilityIOSExample extends React.Component { This view's children are hidden from the accessibility tree - + Outer Element diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index f4712821e1bcad..2336fc096b7fcb 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -211,9 +211,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event BOOL result = NO; if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { 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); - result = CGRectContainsPoint(hitFrame, point); if (result && self.accessibilitySplitFocus) { NSArray *sortedSubviews = [self reactZIndexSortedSubviews]; @@ -224,8 +225,7 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event if (hitSubview != nil) { break; } - } - + } _shouldAbandonAccessibilityFocus = hitSubview != nil; } diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index b6b07fa42850d8..c45426d731a660 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -354,7 +354,7 @@ - (void)setAccessibilityValueInternal:(NSDictionary *)accessibil self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -- (BOOL) accessibilitySplitFocus +- (BOOL)accessibilitySplitFocus { return [objc_getAssociatedObject(self, _cmd) boolValue]; } From 44e99c0ef7042969df71f2a32d4a9a3df8eae35c Mon Sep 17 00:00:00 2001 From: AKB48 Date: Mon, 15 Jun 2020 22:19:51 +0800 Subject: [PATCH 3/5] fix: code style --- .../js/examples/Accessibility/AccessibilityIOSExample.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js index d7fff31d5ef269..49c3d1abe5378e 100644 --- a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js +++ b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js @@ -61,12 +61,17 @@ class AccessibilityIOSExample extends React.Component { accessibilitySplitFocus={true}> Outer Element - + First Inner Element - + Second Inner Element From fa32a85ac1addcb9d6a7f425464da1f27f8b8a70 Mon Sep 17 00:00:00 2001 From: AKB48 Date: Mon, 15 Jun 2020 22:38:03 +0800 Subject: [PATCH 4/5] fix: code style --- RNTester/js/examples/Accessibility/AccessibilityIOSExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js index 49c3d1abe5378e..654b51d46eb64a 100644 --- a/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js +++ b/RNTester/js/examples/Accessibility/AccessibilityIOSExample.js @@ -70,7 +70,7 @@ class AccessibilityIOSExample extends React.Component { style={{ paddingVertical: 10, marginTop: 10, - backgroundColor: 'red' + backgroundColor: 'red', }}> Second Inner Element From 4960daf3bb6d988a58df0c6c3ea2fba73841e16d Mon Sep 17 00:00:00 2001 From: AKB48 Date: Tue, 16 Jun 2020 21:34:23 +0800 Subject: [PATCH 5/5] fix: only accessible subview could capture focus from its parent view --- React/Views/RCTView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 2336fc096b7fcb..d826b956064fcb 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -226,7 +226,7 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event break; } } - _shouldAbandonAccessibilityFocus = hitSubview != nil; + _shouldAbandonAccessibilityFocus = hitSubview != nil && [hitSubview isAccessibilityElement]; } return result;