mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
98d84e571d
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 <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> Pull Request resolved: https://github.com/facebook/react-native/pull/34524 Test Plan: ```js <View aria-disabled={true} aria-selected={false} aria-checked={true} aria-expanded={true} aria-busy={true} style={{backgroundColor: '#527FE4', padding: 5}}> <Text style={{fontSize: 11}}>Blue background</Text> </View> ``` Reviewed By: cipolleschi Differential Revision: D39137790 Pulled By: jacdebug fbshipit-source-id: 27b5c56e91731ba36bb4754d9862286a7a8191bc
332 lines
8.6 KiB
JavaScript
332 lines
8.6 KiB
JavaScript
/**
|
|
* 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.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
|
|
import * as React from 'react';
|
|
import {useMemo, useState, useRef, useImperativeHandle} from 'react';
|
|
import useAndroidRippleForView, {
|
|
type RippleConfig,
|
|
} from './useAndroidRippleForView';
|
|
import type {
|
|
AccessibilityActionEvent,
|
|
AccessibilityActionInfo,
|
|
AccessibilityRole,
|
|
AccessibilityState,
|
|
AccessibilityValue,
|
|
} from '../View/ViewAccessibility';
|
|
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
|
|
import usePressability from '../../Pressability/usePressability';
|
|
import {type RectOrSize} from '../../StyleSheet/Rect';
|
|
import type {
|
|
LayoutEvent,
|
|
MouseEvent,
|
|
PressEvent,
|
|
} from '../../Types/CoreEventTypes';
|
|
import View from '../View/View';
|
|
|
|
type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, 'style'>;
|
|
|
|
export type StateCallbackType = $ReadOnly<{|
|
|
pressed: boolean,
|
|
|}>;
|
|
|
|
type Props = $ReadOnly<{|
|
|
/**
|
|
* Accessibility.
|
|
*/
|
|
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
|
|
accessibilityElementsHidden?: ?boolean,
|
|
accessibilityHint?: ?Stringish,
|
|
accessibilityLanguage?: ?Stringish,
|
|
accessibilityIgnoresInvertColors?: ?boolean,
|
|
accessibilityLabel?: ?Stringish,
|
|
accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'),
|
|
accessibilityRole?: ?AccessibilityRole,
|
|
accessibilityState?: ?AccessibilityState,
|
|
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.
|
|
*/
|
|
'aria-hidden'?: ?boolean,
|
|
focusable?: ?boolean,
|
|
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
|
|
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,
|
|
|
|
/**
|
|
* Whether a press gesture can be interrupted by a parent gesture such as a
|
|
* scroll event. Defaults to true.
|
|
*/
|
|
cancelable?: ?boolean,
|
|
|
|
/**
|
|
* Either children or a render prop that receives a boolean reflecting whether
|
|
* the component is currently pressed.
|
|
*/
|
|
children: React.Node | ((state: StateCallbackType) => React.Node),
|
|
|
|
/**
|
|
* Duration to wait after hover in before calling `onHoverIn`.
|
|
*/
|
|
delayHoverIn?: ?number,
|
|
|
|
/**
|
|
* Duration to wait after hover out before calling `onHoverOut`.
|
|
*/
|
|
delayHoverOut?: ?number,
|
|
|
|
/**
|
|
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
|
|
*/
|
|
delayLongPress?: ?number,
|
|
|
|
/**
|
|
* Whether the press behavior is disabled.
|
|
*/
|
|
disabled?: ?boolean,
|
|
|
|
/**
|
|
* Additional distance outside of this view in which a press is detected.
|
|
*/
|
|
hitSlop?: ?RectOrSize,
|
|
|
|
/**
|
|
* Additional distance outside of this view in which a touch is considered a
|
|
* press before `onPressOut` is triggered.
|
|
*/
|
|
pressRetentionOffset?: ?RectOrSize,
|
|
|
|
/**
|
|
* Called when this view's layout changes.
|
|
*/
|
|
onLayout?: ?(event: LayoutEvent) => mixed,
|
|
|
|
/**
|
|
* Called when the hover is activated to provide visual feedback.
|
|
*/
|
|
onHoverIn?: ?(event: MouseEvent) => mixed,
|
|
|
|
/**
|
|
* Called when the hover is deactivated to undo visual feedback.
|
|
*/
|
|
onHoverOut?: ?(event: MouseEvent) => mixed,
|
|
|
|
/**
|
|
* Called when a long-tap gesture is detected.
|
|
*/
|
|
onLongPress?: ?(event: PressEvent) => mixed,
|
|
|
|
/**
|
|
* Called when a single tap gesture is detected.
|
|
*/
|
|
onPress?: ?(event: PressEvent) => mixed,
|
|
|
|
/**
|
|
* Called when a touch is engaged before `onPress`.
|
|
*/
|
|
onPressIn?: ?(event: PressEvent) => mixed,
|
|
|
|
/**
|
|
* Called when a touch is released before `onPress`.
|
|
*/
|
|
onPressOut?: ?(event: PressEvent) => mixed,
|
|
|
|
/**
|
|
* Either view styles or a function that receives a boolean reflecting whether
|
|
* the component is currently pressed and returns view styles.
|
|
*/
|
|
style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
|
|
|
|
/**
|
|
* Identifier used to find this view in tests.
|
|
*/
|
|
testID?: ?string,
|
|
|
|
/**
|
|
* If true, doesn't play system sound on touch.
|
|
*/
|
|
android_disableSound?: ?boolean,
|
|
|
|
/**
|
|
* Enables the Android ripple effect and configures its color.
|
|
*/
|
|
android_ripple?: ?RippleConfig,
|
|
|
|
/**
|
|
* Used only for documentation or testing (e.g. snapshot testing).
|
|
*/
|
|
testOnly_pressed?: ?boolean,
|
|
|
|
/**
|
|
* Duration to wait after press down before calling `onPressIn`.
|
|
*/
|
|
unstable_pressDelay?: ?number,
|
|
|}>;
|
|
|
|
/**
|
|
* Component used to build display components that should respond to whether the
|
|
* component is currently pressed or not.
|
|
*/
|
|
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
|
|
* LTI update could not be added via codemod */
|
|
function Pressable(props: Props, forwardedRef): React.Node {
|
|
const {
|
|
accessibilityState,
|
|
android_disableSound,
|
|
android_ripple,
|
|
accessible,
|
|
'aria-busy': ariaBusy,
|
|
'aria-checked': ariaChecked,
|
|
'aria-disabled': ariaDisabled,
|
|
'aria-expanded': ariaExpanded,
|
|
'aria-selected': ariaSelected,
|
|
cancelable,
|
|
children,
|
|
delayHoverIn,
|
|
delayHoverOut,
|
|
delayLongPress,
|
|
disabled,
|
|
focusable,
|
|
hitSlop,
|
|
onHoverIn,
|
|
onHoverOut,
|
|
onLongPress,
|
|
onPress,
|
|
onPressIn,
|
|
onPressOut,
|
|
pressRetentionOffset,
|
|
style,
|
|
testOnly_pressed,
|
|
unstable_pressDelay,
|
|
...restProps
|
|
} = props;
|
|
|
|
const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
|
|
useImperativeHandle(forwardedRef, () => viewRef.current);
|
|
|
|
const android_rippleConfig = useAndroidRippleForView(android_ripple, viewRef);
|
|
|
|
const [pressed, setPressed] = usePressState(testOnly_pressed === true);
|
|
|
|
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<typeof View> = {
|
|
...restProps,
|
|
...android_rippleConfig?.viewProps,
|
|
accessible: accessible !== false,
|
|
accessibilityState: _accessibilityState,
|
|
focusable: focusable !== false,
|
|
hitSlop,
|
|
};
|
|
|
|
const config = useMemo(
|
|
() => ({
|
|
cancelable,
|
|
disabled,
|
|
hitSlop,
|
|
pressRectOffset: pressRetentionOffset,
|
|
android_disableSound,
|
|
delayHoverIn,
|
|
delayHoverOut,
|
|
delayLongPress,
|
|
delayPressIn: unstable_pressDelay,
|
|
onHoverIn,
|
|
onHoverOut,
|
|
onLongPress,
|
|
onPress,
|
|
onPressIn(event: PressEvent): void {
|
|
if (android_rippleConfig != null) {
|
|
android_rippleConfig.onPressIn(event);
|
|
}
|
|
setPressed(true);
|
|
if (onPressIn != null) {
|
|
onPressIn(event);
|
|
}
|
|
},
|
|
onPressMove: android_rippleConfig?.onPressMove,
|
|
onPressOut(event: PressEvent): void {
|
|
if (android_rippleConfig != null) {
|
|
android_rippleConfig.onPressOut(event);
|
|
}
|
|
setPressed(false);
|
|
if (onPressOut != null) {
|
|
onPressOut(event);
|
|
}
|
|
},
|
|
}),
|
|
[
|
|
android_disableSound,
|
|
android_rippleConfig,
|
|
cancelable,
|
|
delayHoverIn,
|
|
delayHoverOut,
|
|
delayLongPress,
|
|
disabled,
|
|
hitSlop,
|
|
onHoverIn,
|
|
onHoverOut,
|
|
onLongPress,
|
|
onPress,
|
|
onPressIn,
|
|
onPressOut,
|
|
pressRetentionOffset,
|
|
setPressed,
|
|
unstable_pressDelay,
|
|
],
|
|
);
|
|
const eventHandlers = usePressability(config);
|
|
|
|
return (
|
|
<View
|
|
{...restPropsWithDefaults}
|
|
{...eventHandlers}
|
|
ref={viewRef}
|
|
style={typeof style === 'function' ? style({pressed}) : style}
|
|
collapsable={false}>
|
|
{typeof children === 'function' ? children({pressed}) : children}
|
|
{__DEV__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
|
|
const [pressed, setPressed] = useState(false);
|
|
return [pressed || forcePressed, setPressed];
|
|
}
|
|
|
|
const MemoedPressable = React.memo(React.forwardRef(Pressable));
|
|
MemoedPressable.displayName = 'Pressable';
|
|
|
|
export default (MemoedPressable: React.AbstractComponent<
|
|
Props,
|
|
React.ElementRef<typeof View>,
|
|
>);
|