288 lines
8.6 KiB
JavaScript
288 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 type {AccessibilityActionEvent} from '../../Components/View/ViewAccessibility';
|
|
import type {EdgeInsetsOrSizeProp} from '../../StyleSheet/EdgeInsetsPropType';
|
|
import type {
|
|
BlurEvent,
|
|
FocusEvent,
|
|
GestureResponderEvent,
|
|
LayoutChangeEvent,
|
|
} from '../../Types/CoreEventTypes';
|
|
|
|
import View from '../../Components/View/View';
|
|
import {type AccessibilityProps} from '../../Components/View/ViewAccessibility';
|
|
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
|
|
import usePressability from '../../Pressability/usePressability';
|
|
import {type ViewStyleProp} from '../../StyleSheet/StyleSheet';
|
|
import * as React from 'react';
|
|
import {cloneElement, useMemo} from 'react';
|
|
|
|
export type TouchableWithoutFeedbackPropsIOS = {};
|
|
|
|
export type TouchableWithoutFeedbackPropsAndroid = {
|
|
/**
|
|
* If true, doesn't play a system sound on touch.
|
|
*
|
|
* @platform android
|
|
*/
|
|
touchSoundDisabled?: ?boolean,
|
|
};
|
|
|
|
export type TouchableWithoutFeedbackProps = $ReadOnly<
|
|
{
|
|
children?: ?React.Node,
|
|
/**
|
|
* Delay in ms, from onPressIn, before onLongPress is called.
|
|
*/
|
|
delayLongPress?: ?number,
|
|
/**
|
|
* Delay in ms, from the start of the touch, before onPressIn is called.
|
|
*/
|
|
delayPressIn?: ?number,
|
|
/**
|
|
* Delay in ms, from the release of the touch, before onPressOut is called.
|
|
*/
|
|
delayPressOut?: ?number,
|
|
/**
|
|
* If true, disable all interactions for this component.
|
|
*/
|
|
disabled?: ?boolean,
|
|
/**
|
|
* Whether this View should be focusable with a non-touch input device,
|
|
* eg. receive focus with a hardware keyboard / TV remote.
|
|
*/
|
|
focusable?: ?boolean,
|
|
/**
|
|
* This defines how far your touch can start away from the button.
|
|
* This is added to pressRetentionOffset when moving off of the button.
|
|
* NOTE The touch area never extends past the parent view bounds and
|
|
* the Z-index of sibling views always takes precedence if a touch hits
|
|
* two overlapping views.
|
|
*/
|
|
hitSlop?: ?EdgeInsetsOrSizeProp,
|
|
/**
|
|
* Used to reference react managed views from native code.
|
|
*/
|
|
id?: string,
|
|
importantForAccessibility?: ?(
|
|
| 'auto'
|
|
| 'yes'
|
|
| 'no'
|
|
| 'no-hide-descendants'
|
|
),
|
|
nativeID?: ?string,
|
|
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,
|
|
/**
|
|
* When `accessible` is true (which is the default) this may be called when
|
|
* the OS-specific concept of "blur" occurs, meaning the element lost focus.
|
|
* Some platforms may not have the concept of blur.
|
|
*/
|
|
onBlur?: ?(event: BlurEvent) => mixed,
|
|
/**
|
|
* When `accessible` is true (which is the default) this may be called when
|
|
* the OS-specific concept of "focus" occurs. Some platforms may not have
|
|
* the concept of focus.
|
|
*/
|
|
onFocus?: ?(event: FocusEvent) => mixed,
|
|
/**
|
|
* Invoked on mount and layout changes with
|
|
* {nativeEvent: {layout: {x, y, width, height}}}
|
|
*/
|
|
onLayout?: ?(event: LayoutChangeEvent) => mixed,
|
|
onLongPress?: ?(event: GestureResponderEvent) => mixed,
|
|
/**
|
|
* Called when the touch is released,
|
|
* but not if cancelled (e.g. by a scroll that steals the responder lock).
|
|
*/
|
|
onPress?: ?(event: GestureResponderEvent) => mixed,
|
|
onPressIn?: ?(event: GestureResponderEvent) => mixed,
|
|
onPressOut?: ?(event: GestureResponderEvent) => mixed,
|
|
/**
|
|
* When the scroll view is disabled, this defines how far your
|
|
* touch may move off of the button, before deactivating the button.
|
|
* Once deactivated, try moving it back and you'll see that the button
|
|
* is once again reactivated! Move it back and forth several times
|
|
* while the scroll view is disabled. Ensure you pass in a constant
|
|
* to reduce memory allocations.
|
|
*/
|
|
pressRetentionOffset?: ?EdgeInsetsOrSizeProp,
|
|
rejectResponderTermination?: ?boolean,
|
|
/**
|
|
* Used to locate this view in end-to-end tests.
|
|
*/
|
|
testID?: ?string,
|
|
/**
|
|
* //FIXME: not in doc but available in examples
|
|
*/
|
|
style?: ?ViewStyleProp,
|
|
} & TouchableWithoutFeedbackPropsAndroid &
|
|
TouchableWithoutFeedbackPropsIOS &
|
|
AccessibilityProps,
|
|
>;
|
|
|
|
const PASSTHROUGH_PROPS = [
|
|
'accessibilityActions',
|
|
'accessibilityElementsHidden',
|
|
'accessibilityHint',
|
|
'accessibilityLanguage',
|
|
'accessibilityIgnoresInvertColors',
|
|
'accessibilityLabel',
|
|
'accessibilityLiveRegion',
|
|
'accessibilityRole',
|
|
'accessibilityValue',
|
|
'aria-valuemax',
|
|
'aria-valuemin',
|
|
'aria-valuenow',
|
|
'aria-valuetext',
|
|
'accessibilityViewIsModal',
|
|
'aria-modal',
|
|
'hitSlop',
|
|
'importantForAccessibility',
|
|
'nativeID',
|
|
'onAccessibilityAction',
|
|
'onBlur',
|
|
'onFocus',
|
|
'onLayout',
|
|
'testID',
|
|
] as const;
|
|
|
|
/**
|
|
* Do not use unless you have a very good reason.
|
|
* All the elements that respond to press should have a visual feedback when touched.
|
|
* This is one of the primary reason a "web" app doesn't feel "native".
|
|
*
|
|
* @see https://reactnative.dev/docs/touchablewithoutfeedback
|
|
*/
|
|
export default function TouchableWithoutFeedback(
|
|
props: TouchableWithoutFeedbackProps,
|
|
): React.Node {
|
|
const {
|
|
disabled,
|
|
rejectResponderTermination,
|
|
'aria-disabled': ariaDisabled,
|
|
accessibilityState,
|
|
hitSlop,
|
|
delayLongPress,
|
|
delayPressIn,
|
|
delayPressOut,
|
|
pressRetentionOffset,
|
|
touchSoundDisabled,
|
|
onBlur: _onBlur,
|
|
onFocus: _onFocus,
|
|
onLongPress,
|
|
onPress,
|
|
onPressIn,
|
|
onPressOut,
|
|
} = props;
|
|
|
|
const pressabilityConfig = useMemo(
|
|
() => ({
|
|
cancelable: !rejectResponderTermination,
|
|
disabled:
|
|
disabled !== null
|
|
? disabled
|
|
: ariaDisabled ?? accessibilityState?.disabled,
|
|
hitSlop: hitSlop,
|
|
delayLongPress: delayLongPress,
|
|
delayPressIn: delayPressIn,
|
|
delayPressOut: delayPressOut,
|
|
minPressDuration: 0,
|
|
pressRectOffset: pressRetentionOffset,
|
|
android_disableSound: touchSoundDisabled,
|
|
onBlur: _onBlur,
|
|
onFocus: _onFocus,
|
|
onLongPress: onLongPress,
|
|
onPress: onPress,
|
|
onPressIn: onPressIn,
|
|
onPressOut: onPressOut,
|
|
}),
|
|
[
|
|
rejectResponderTermination,
|
|
disabled,
|
|
ariaDisabled,
|
|
accessibilityState?.disabled,
|
|
hitSlop,
|
|
delayLongPress,
|
|
delayPressIn,
|
|
delayPressOut,
|
|
pressRetentionOffset,
|
|
touchSoundDisabled,
|
|
_onBlur,
|
|
_onFocus,
|
|
onLongPress,
|
|
onPress,
|
|
onPressIn,
|
|
onPressOut,
|
|
],
|
|
);
|
|
|
|
const eventHandlers = usePressability(pressabilityConfig);
|
|
|
|
const element = React.Children.only<$FlowFixMe>(props.children);
|
|
const children: Array<React.Node> = [element.props.children];
|
|
const ariaLive = props['aria-live'];
|
|
|
|
if (__DEV__) {
|
|
if (element.type === View) {
|
|
children.push(
|
|
<PressabilityDebugView color="red" hitSlop={props.hitSlop} />,
|
|
);
|
|
}
|
|
}
|
|
|
|
let _accessibilityState = {
|
|
busy: props['aria-busy'] ?? props.accessibilityState?.busy,
|
|
checked: props['aria-checked'] ?? props.accessibilityState?.checked,
|
|
disabled: props['aria-disabled'] ?? props.accessibilityState?.disabled,
|
|
expanded: props['aria-expanded'] ?? props.accessibilityState?.expanded,
|
|
selected: props['aria-selected'] ?? props.accessibilityState?.selected,
|
|
};
|
|
|
|
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
|
|
// adopting `Pressability`, so preserve that behavior.
|
|
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} = eventHandlers;
|
|
|
|
const elementProps: {[string]: mixed, ...} = {
|
|
...eventHandlersWithoutBlurAndFocus,
|
|
accessible: props.accessible !== false,
|
|
accessibilityState:
|
|
props.disabled != null
|
|
? {
|
|
..._accessibilityState,
|
|
disabled: props.disabled,
|
|
}
|
|
: _accessibilityState,
|
|
focusable:
|
|
props.focusable !== false &&
|
|
props.onPress !== undefined &&
|
|
!props.disabled,
|
|
|
|
accessibilityElementsHidden:
|
|
props['aria-hidden'] ?? props.accessibilityElementsHidden,
|
|
importantForAccessibility:
|
|
props['aria-hidden'] === true
|
|
? 'no-hide-descendants'
|
|
: props.importantForAccessibility,
|
|
accessibilityLiveRegion:
|
|
ariaLive === 'off' ? 'none' : ariaLive ?? props.accessibilityLiveRegion,
|
|
nativeID: props.id ?? props.nativeID,
|
|
};
|
|
|
|
for (const prop of PASSTHROUGH_PROPS) {
|
|
if (props[prop] !== undefined) {
|
|
elementProps[prop] = props[prop];
|
|
}
|
|
}
|
|
|
|
// $FlowFixMe[incompatible-call]
|
|
return cloneElement(element, elementProps, ...children);
|
|
}
|