From 98d84e571df64b00e1ed3484923a1d8169dcfda1 Mon Sep 17 00:00:00 2001 From: ankit-tailor Date: Thu, 8 Sep 2022 03:38:39 -0700 Subject: [PATCH] Feat/accessibility state alias (#34524) Summary: This adds aliasing for accessibility state, it's used as requested on https://github.com/facebook/react-native/issues/34424. - [aria-disabled](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled) to equivalent [accessibilityState.disabled](https://reactnative.dev/docs/accessibility#accessibilitystate) - [aria-busy](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-busy) to equivalent [accessibilityState.busy](https://reactnative.dev/docs/accessibility#accessibilitystate) - [aria-checked](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-checked) to equivalent [accessibilityState.checked](https://reactnative.dev/docs/accessibility#accessibilitystate) - [aria-expanded](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded) to equivalent [accessibilityState.expanded](https://reactnative.dev/docs/accessibility#accessibilitystate) - [aria-selected](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected) to equivalent [accessibilityState.selected](https://reactnative.dev/docs/accessibility#accessibilitystate) ## Changelog [General] [Added] - Add aria-disabled, aria-busy, aria-checked, aria-expanded and aria-selected prop to core components Pull Request resolved: https://github.com/facebook/react-native/pull/34524 Test Plan: ```js Blue background ``` Reviewed By: cipolleschi Differential Revision: D39137790 Pulled By: jacdebug fbshipit-source-id: 27b5c56e91731ba36bb4754d9862286a7a8191bc --- Libraries/Components/Button.js | 37 ++- Libraries/Components/Pressable/Pressable.js | 35 ++- .../__snapshots__/Pressable-test.js.snap | 48 ++++ Libraries/Components/TextInput/TextInput.js | 10 + .../__snapshots__/TextInput-test.js.snap | 18 ++ .../Components/Touchable/TouchableBounce.js | 14 +- .../Touchable/TouchableNativeFeedback.js | 24 +- .../Components/Touchable/TouchableOpacity.js | 25 +- .../Touchable/TouchableWithoutFeedback.js | 37 ++- .../TouchableNativeFeedback-test.js.snap | 215 ++++++++++-------- .../TouchableOpacity-test.js.snap | 17 ++ .../TouchableWithoutFeedback-test.js.snap | 28 +++ Libraries/Components/View/View.js | 21 +- Libraries/Components/View/ViewPropTypes.js | 11 +- .../__snapshots__/Button-test.js.snap | 51 +++++ Libraries/Image/Image.android.js | 7 + Libraries/Image/Image.ios.js | 20 +- .../__snapshots__/Image-test.js.snap | 9 + Libraries/Text/Text.js | 27 ++- Libraries/Text/TextProps.js | 11 + .../Accessibility/AccessibilityExample.js | 11 + 21 files changed, 549 insertions(+), 127 deletions(-) diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index d396acebf5e..b2bf05b5914 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -147,6 +147,17 @@ type ButtonProps = $ReadOnly<{| onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, accessibilityState?: ?AccessibilityState, + /** + * alias for accessibilityState + * + * see https://reactnative.dev/docs/accessibility#accessibilitystate + */ + 'aria-busy'?: ?boolean, + 'aria-checked'?: ?boolean, + 'aria-disabled'?: ?boolean, + 'aria-expanded'?: ?boolean, + 'aria-selected'?: ?boolean, + /** * [Android] Controlling if a view fires accessibility events and if it is reported to accessibility services. */ @@ -270,6 +281,12 @@ class Button extends React.Component { render(): React.Node { const { accessibilityLabel, + accessibilityState, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, importantForAccessibility, color, onPress, @@ -298,15 +315,23 @@ class Button extends React.Component { } } + let _accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + const disabled = this.props.disabled != null ? this.props.disabled - : this.props.accessibilityState?.disabled; + : _accessibilityState?.disabled; - const accessibilityState = - disabled !== this.props.accessibilityState?.disabled - ? {...this.props.accessibilityState, disabled} - : this.props.accessibilityState; + _accessibilityState = + disabled !== _accessibilityState?.disabled + ? {..._accessibilityState, disabled} + : _accessibilityState; if (disabled) { buttonStyles.push(styles.buttonDisabled); @@ -337,7 +362,7 @@ class Button extends React.Component { accessibilityHint={accessibilityHint} accessibilityLanguage={accessibilityLanguage} accessibilityRole="button" - accessibilityState={accessibilityState} + accessibilityState={_accessibilityState} importantForAccessibility={_importantForAccessibility} hasTVPreferredFocus={hasTVPreferredFocus} nextFocusDown={nextFocusDown} diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index 992378df706..081a960e4a4 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -52,6 +52,17 @@ type Props = $ReadOnly<{| accessibilityValue?: ?AccessibilityValue, accessibilityViewIsModal?: ?boolean, accessible?: ?boolean, + + /** + * alias for accessibilityState + * + * see https://reactnative.dev/docs/accessibility#accessibilitystate + */ + 'aria-busy'?: ?boolean, + 'aria-checked'?: ?boolean, + 'aria-disabled'?: ?boolean, + 'aria-expanded'?: ?boolean, + 'aria-selected'?: ?boolean, /** * A value indicating whether the accessibility elements contained within * this accessibility element are hidden. @@ -179,9 +190,15 @@ type Props = $ReadOnly<{| * LTI update could not be added via codemod */ function Pressable(props: Props, forwardedRef): React.Node { const { - accessible, + accessibilityState, android_disableSound, android_ripple, + accessible, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, cancelable, children, delayHoverIn, @@ -210,16 +227,22 @@ function Pressable(props: Props, forwardedRef): React.Node { const [pressed, setPressed] = usePressState(testOnly_pressed === true); - const accessibilityState = - disabled != null - ? {...props.accessibilityState, disabled} - : props.accessibilityState; + let _accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + + _accessibilityState = + disabled != null ? {..._accessibilityState, disabled} : _accessibilityState; const restPropsWithDefaults: React.ElementConfig = { ...restProps, ...android_rippleConfig?.viewProps, accessible: accessible !== false, - accessibilityState, + accessibilityState: _accessibilityState, focusable: focusable !== false, hitSlop, }; diff --git a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap index 821d4f41ea5..4e03820efa8 100644 --- a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap @@ -2,6 +2,15 @@ exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` should render as expected: should deep render when mocked exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` should be disabled when disabled is true: should be disabled when disabled is true: should be disable should be disable shou shou sh sh { const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = this.state.pressability.getEventHandlers(); + const _accessibilityState = { + busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy, + checked: + this.props['aria-checked'] ?? this.props.accessibilityState?.checked, + disabled: + this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled, + expanded: + this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded, + selected: + this.props['aria-selected'] ?? this.props.accessibilityState?.selected, + }; + return ( { accessibilityHint={this.props.accessibilityHint} accessibilityLanguage={this.props.accessibilityLanguage} accessibilityRole={this.props.accessibilityRole} - accessibilityState={this.props.accessibilityState} + accessibilityState={_accessibilityState} accessibilityActions={this.props.accessibilityActions} onAccessibilityAction={this.props.onAccessibilityAction} accessibilityValue={this.props.accessibilityValue} diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js index 3b6de69fb99..7f305fd7086 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -162,12 +162,14 @@ class TouchableNativeFeedback extends React.Component { }; _createPressabilityConfig(): PressabilityConfig { + const accessibilityStateDisabled = + this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled; return { cancelable: !this.props.rejectResponderTermination, disabled: this.props.disabled != null ? this.props.disabled - : this.props.accessibilityState?.disabled, + : accessibilityStateDisabled, hitSlop: this.props.hitSlop, delayLongPress: this.props.delayLongPress, delayPressIn: this.props.delayPressIn, @@ -251,13 +253,25 @@ class TouchableNativeFeedback extends React.Component { const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = this.state.pressability.getEventHandlers(); - const accessibilityState = + let _accessibilityState = { + busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy, + checked: + this.props['aria-checked'] ?? this.props.accessibilityState?.checked, + disabled: + this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled, + expanded: + this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded, + selected: + this.props['aria-selected'] ?? this.props.accessibilityState?.selected, + }; + + _accessibilityState = this.props.disabled != null ? { - ...this.props.accessibilityState, + ..._accessibilityState, disabled: this.props.disabled, } - : this.props.accessibilityState; + : _accessibilityState; return React.cloneElement( element, @@ -274,7 +288,7 @@ class TouchableNativeFeedback extends React.Component { accessibilityLanguage: this.props.accessibilityLanguage, accessibilityLabel: this.props.accessibilityLabel, accessibilityRole: this.props.accessibilityRole, - accessibilityState: accessibilityState, + accessibilityState: _accessibilityState, accessibilityActions: this.props.accessibilityActions, onAccessibilityAction: this.props.onAccessibilityAction, accessibilityValue: this.props.accessibilityValue, diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 936caceaf16..1d4f392cbde 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -137,7 +137,10 @@ class TouchableOpacity extends React.Component { _createPressabilityConfig(): PressabilityConfig { return { cancelable: !this.props.rejectResponderTermination, - disabled: this.props.disabled ?? this.props.accessibilityState?.disabled, + disabled: + this.props.disabled ?? + this.props['aria-disabled'] ?? + this.props.accessibilityState?.disabled, hitSlop: this.props.hitSlop, delayLongPress: this.props.delayLongPress, delayPressIn: this.props.delayPressIn, @@ -212,13 +215,25 @@ class TouchableOpacity extends React.Component { const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = this.state.pressability.getEventHandlers(); - const accessibilityState = + let _accessibilityState = { + busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy, + checked: + this.props['aria-checked'] ?? this.props.accessibilityState?.checked, + disabled: + this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled, + expanded: + this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded, + selected: + this.props['aria-selected'] ?? this.props.accessibilityState?.selected, + }; + + _accessibilityState = this.props.disabled != null ? { - ...this.props.accessibilityState, + ..._accessibilityState, disabled: this.props.disabled, } - : this.props.accessibilityState; + : _accessibilityState; return ( { accessibilityHint={this.props.accessibilityHint} accessibilityLanguage={this.props.accessibilityLanguage} accessibilityRole={this.props.accessibilityRole} - accessibilityState={accessibilityState} + accessibilityState={_accessibilityState} accessibilityActions={this.props.accessibilityActions} onAccessibilityAction={this.props.onAccessibilityAction} accessibilityValue={this.props.accessibilityValue} diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index f260f2cc9d7..bb46921c8c9 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -42,6 +42,16 @@ type Props = $ReadOnly<{| accessibilityValue?: ?AccessibilityValue, accessibilityViewIsModal?: ?boolean, accessible?: ?boolean, + /** + * alias for accessibilityState + * + * see https://reactnative.dev/docs/accessibility#accessibilitystate + */ + 'aria-busy'?: ?boolean, + 'aria-checked'?: ?boolean, + 'aria-disabled'?: ?boolean, + 'aria-expanded'?: ?boolean, + 'aria-selected'?: ?boolean, 'aria-hidden'?: ?boolean, children?: ?React.Node, delayLongPress?: ?number, @@ -105,6 +115,18 @@ class TouchableWithoutFeedback extends React.Component { } } + let _accessibilityState = { + busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy, + checked: + this.props['aria-checked'] ?? this.props.accessibilityState?.checked, + disabled: + this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled, + expanded: + this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded, + selected: + this.props['aria-selected'] ?? this.props.accessibilityState?.selected, + }; + // BACKWARD-COMPATIBILITY: Focus and blur events were never supported before // adopting `Pressability`, so preserve that behavior. const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = @@ -116,10 +138,10 @@ class TouchableWithoutFeedback extends React.Component { accessibilityState: this.props.disabled != null ? { - ...this.props.accessibilityState, + ..._accessibilityState, disabled: this.props.disabled, } - : this.props.accessibilityState, + : _accessibilityState, focusable: this.props.focusable !== false && this.props.onPress !== undefined, @@ -148,13 +170,16 @@ class TouchableWithoutFeedback extends React.Component { } } -function createPressabilityConfig(props: Props): PressabilityConfig { +function createPressabilityConfig({ + 'aria-disabled': ariaDisabled, + ...props +}: Props): PressabilityConfig { + const accessibilityStateDisabled = + ariaDisabled ?? props.accessibilityState?.disabled; return { cancelable: !props.rejectResponderTermination, disabled: - props.disabled !== null - ? props.disabled - : props.accessibilityState?.disabled, + props.disabled !== null ? props.disabled : accessibilityStateDisabled, hitSlop: props.hitSlop, delayLongPress: props.delayLongPress, delayPressIn: props.delayPressIn, diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap index 64b098803b1..05f56b9147c 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap @@ -1,98 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TouchableWithoutFeedback renders correctly 1`] = ` - - Touchable - -`; - exports[` should render as expected 1`] = ` - -`; - -exports[` should be disabled when disabled is true 1`] = ` -`; - -exports[` should be disabled when disabled is true and accessibilityState is empty 1`] = ` - -`; - -exports[` should keep accessibilityState when disabled is true 1`] = ` - -`; - -exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` - `; + +exports[` should be disabled when disabled is true and accessibilityState is empty 1`] = ` + +`; + +exports[` should keep accessibilityState when disabled is true 1`] = ` + +`; + +exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` + +`; + +exports[` should be disabled when disabled is true 1`] = ` + +`; + +exports[`TouchableWithoutFeedback renders correctly 1`] = ` + + Touchable + +`; diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap index 4d3f11bc92e..92d94f7a163 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap @@ -2,6 +2,15 @@ exports[`TouchableOpacity renders correctly 1`] = ` { + const { + accessibilityState, + 'aria-busy': ariaBusy, + 'aria-checked': ariaChecked, + 'aria-disabled': ariaDisabled, + 'aria-expanded': ariaExpanded, + 'aria-selected': ariaSelected, + ...restProps + } = otherProps; + + const _accessibilityState = { + busy: ariaBusy ?? accessibilityState?.busy, + checked: ariaChecked ?? accessibilityState?.checked, + disabled: ariaDisabled ?? accessibilityState?.disabled, + expanded: ariaExpanded ?? accessibilityState?.expanded, + selected: ariaSelected ?? accessibilityState?.selected, + }; + // Map role values to AccessibilityRole values const roleToAccessibilityRoleMapping = { alert: 'alert', @@ -118,6 +136,7 @@ const View: React.AbstractComponent< , /** - * A value indicating whether the accessibility elements contained within + * alias for accessibilityState + * + * see https://reactnative.dev/docs/accessibility#accessibilitystate + */ + 'aria-busy'?: ?boolean, + 'aria-checked'?: ?boolean, + 'aria-disabled'?: ?boolean, + 'aria-expanded'?: ?boolean, + 'aria-selected'?: ?boolean, + /** A value indicating whether the accessibility elements contained within * this accessibility element are hidden. * * See https://reactnative.dev/docs/view#aria-hidden diff --git a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap b/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap index fb87db9320e..57c18ada171 100644 --- a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap +++ b/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap @@ -5,7 +5,11 @@ exports[`