From f8098501fcaa77a02ae7810f9dbc9a8b05d111c8 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 | 67 ++++++++++++++----- .../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, 475 insertions(+), 53 deletions(-) diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index d396acebf5e59f..b2bf05b5914c39 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 992378df706985..081a960e4a4ddb 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 821d4f41ea56e5..4e03820efa8ea3 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 3b6de69fb9991f..7f305fd70869a9 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 936caceaf16bf7..1d4f392cbde3c2 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 f260f2cc9d7334..bb46921c8c9c75 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 64b098803b11c3..05f56b9147c90f 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap @@ -1,7 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TouchableWithoutFeedback renders correctly 1`] = ` - should render as expected 1`] = ` + - Touchable - +/> `; -exports[` should render as expected 1`] = ` +exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` 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`] = ` should be disabled when disab /> `; -exports[` should be disabled when disabled is true and accessibilityState is empty 1`] = ` +exports[` should keep accessibilityState when disabled is true 1`] = ` shoul /> `; -exports[` should keep accessibilityState when disabled is true 1`] = ` +exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` `; -exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` +exports[` should be disabled when disabled is true 1`] = ` `; -exports[` should overwrite accessibilityState with value of disabled prop 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 4d3f11bc92ef35..92d94f7a1630cb 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 fb87db9320e845..57c18ada171a0d 100644 --- a/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap +++ b/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap @@ -5,7 +5,11 @@ exports[`