Pressable.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow strict-local
  8. * @format
  9. */
  10. 'use strict';
  11. import * as React from 'react';
  12. import {useMemo, useState, useRef, useImperativeHandle} from 'react';
  13. import useAndroidRippleForView, {
  14. type RippleConfig,
  15. } from './useAndroidRippleForView';
  16. import type {
  17. AccessibilityActionEvent,
  18. AccessibilityActionInfo,
  19. AccessibilityRole,
  20. AccessibilityState,
  21. AccessibilityValue,
  22. } from '../View/ViewAccessibility';
  23. import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
  24. import usePressability from '../../Pressability/usePressability';
  25. import {normalizeRect, type RectOrSize} from '../../StyleSheet/Rect';
  26. import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
  27. import type {LayoutEvent, PressEvent} from '../../Types/CoreEventTypes';
  28. import View from '../View/View';
  29. type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, 'style'>;
  30. export type StateCallbackType = $ReadOnly<{|
  31. pressed: boolean,
  32. |}>;
  33. type Props = $ReadOnly<{|
  34. /**
  35. * Accessibility.
  36. */
  37. accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
  38. accessibilityElementsHidden?: ?boolean,
  39. accessibilityHint?: ?Stringish,
  40. accessibilityIgnoresInvertColors?: ?boolean,
  41. accessibilityLabel?: ?Stringish,
  42. accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'),
  43. accessibilityRole?: ?AccessibilityRole,
  44. accessibilityState?: ?AccessibilityState,
  45. accessibilityValue?: ?AccessibilityValue,
  46. accessibilityViewIsModal?: ?boolean,
  47. accessible?: ?boolean,
  48. focusable?: ?boolean,
  49. importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
  50. onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,
  51. /**
  52. * Either children or a render prop that receives a boolean reflecting whether
  53. * the component is currently pressed.
  54. */
  55. children: React.Node | ((state: StateCallbackType) => React.Node),
  56. /**
  57. * Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
  58. */
  59. delayLongPress?: ?number,
  60. /**
  61. * Whether the press behavior is disabled.
  62. */
  63. disabled?: ?boolean,
  64. /**
  65. * Additional distance outside of this view in which a press is detected.
  66. */
  67. hitSlop?: ?RectOrSize,
  68. /**
  69. * Additional distance outside of this view in which a touch is considered a
  70. * press before `onPressOut` is triggered.
  71. */
  72. pressRetentionOffset?: ?RectOrSize,
  73. /**
  74. * Called when this view's layout changes.
  75. */
  76. onLayout?: ?(event: LayoutEvent) => void,
  77. /**
  78. * Called when a long-tap gesture is detected.
  79. */
  80. onLongPress?: ?(event: PressEvent) => void,
  81. /**
  82. * Called when a single tap gesture is detected.
  83. */
  84. onPress?: ?(event: PressEvent) => void,
  85. /**
  86. * Called when a touch is engaged before `onPress`.
  87. */
  88. onPressIn?: ?(event: PressEvent) => void,
  89. /**
  90. * Called when a touch is released before `onPress`.
  91. */
  92. onPressOut?: ?(event: PressEvent) => void,
  93. /**
  94. * Either view styles or a function that receives a boolean reflecting whether
  95. * the component is currently pressed and returns view styles.
  96. */
  97. style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),
  98. /**
  99. * Identifier used to find this view in tests.
  100. */
  101. testID?: ?string,
  102. /**
  103. * If true, doesn't play system sound on touch.
  104. */
  105. android_disableSound?: ?boolean,
  106. /**
  107. * Enables the Android ripple effect and configures its color.
  108. */
  109. android_ripple?: ?RippleConfig,
  110. /**
  111. * Used only for documentation or testing (e.g. snapshot testing).
  112. */
  113. testOnly_pressed?: ?boolean,
  114. |}>;
  115. /**
  116. * Component used to build display components that should respond to whether the
  117. * component is currently pressed or not.
  118. */
  119. function Pressable(props: Props, forwardedRef): React.Node {
  120. const {
  121. accessible,
  122. android_disableSound,
  123. android_ripple,
  124. children,
  125. delayLongPress,
  126. disabled,
  127. focusable,
  128. onLongPress,
  129. onPress,
  130. onPressIn,
  131. onPressOut,
  132. pressRetentionOffset,
  133. style,
  134. testOnly_pressed,
  135. ...restProps
  136. } = props;
  137. const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
  138. useImperativeHandle(forwardedRef, () => viewRef.current);
  139. const android_rippleConfig = useAndroidRippleForView(android_ripple, viewRef);
  140. const [pressed, setPressed] = usePressState(testOnly_pressed === true);
  141. const hitSlop = normalizeRect(props.hitSlop);
  142. const config = useMemo(
  143. () => ({
  144. disabled,
  145. hitSlop,
  146. pressRectOffset: pressRetentionOffset,
  147. android_disableSound,
  148. delayLongPress,
  149. onLongPress,
  150. onPress,
  151. onPressIn(event: PressEvent): void {
  152. if (android_rippleConfig != null) {
  153. android_rippleConfig.onPressIn(event);
  154. }
  155. setPressed(true);
  156. if (onPressIn != null) {
  157. onPressIn(event);
  158. }
  159. },
  160. onPressMove: android_rippleConfig?.onPressMove,
  161. onPressOut(event: PressEvent): void {
  162. if (android_rippleConfig != null) {
  163. android_rippleConfig.onPressOut(event);
  164. }
  165. setPressed(false);
  166. if (onPressOut != null) {
  167. onPressOut(event);
  168. }
  169. },
  170. }),
  171. [
  172. android_disableSound,
  173. android_rippleConfig,
  174. delayLongPress,
  175. disabled,
  176. hitSlop,
  177. onLongPress,
  178. onPress,
  179. onPressIn,
  180. onPressOut,
  181. pressRetentionOffset,
  182. setPressed,
  183. ],
  184. );
  185. const eventHandlers = usePressability(config);
  186. return (
  187. <View
  188. {...restProps}
  189. {...eventHandlers}
  190. {...android_rippleConfig?.viewProps}
  191. accessible={accessible !== false}
  192. focusable={focusable !== false}
  193. hitSlop={hitSlop}
  194. ref={viewRef}
  195. style={typeof style === 'function' ? style({pressed}) : style}>
  196. {typeof children === 'function' ? children({pressed}) : children}
  197. {__DEV__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
  198. </View>
  199. );
  200. }
  201. function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
  202. const [pressed, setPressed] = useState(false);
  203. return [pressed || forcePressed, setPressed];
  204. }
  205. const MemoedPressable = React.memo(React.forwardRef(Pressable));
  206. MemoedPressable.displayName = 'Pressable';
  207. export default (MemoedPressable: React.AbstractComponent<
  208. Props,
  209. React.ElementRef<typeof View>,
  210. >);