react-native-gesture-handler 2.24.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +3 -0
  2. package/android/build.gradle +39 -12
  3. package/android/fabric/src/main/java/com/swmansion/gesturehandler/ReactContextExtensions.kt +2 -0
  4. package/android/nosvg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt +13 -0
  5. package/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +6 -1
  6. package/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt +13 -3
  7. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt +1 -1
  8. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt +8 -2
  9. package/android/svg/src/main/java/com/swmansion/gesturehandler/RNSVGHitTester.kt +67 -0
  10. package/apple/RNGestureHandlerButtonComponentView.h +2 -1
  11. package/apple/RNGestureHandlerButtonComponentView.mm +95 -0
  12. package/lib/commonjs/components/GestureButtons.js +5 -4
  13. package/lib/commonjs/components/GestureButtons.js.map +1 -1
  14. package/lib/commonjs/components/Pressable/Pressable.js +76 -40
  15. package/lib/commonjs/components/Pressable/Pressable.js.map +1 -1
  16. package/lib/commonjs/handlers/createHandler.js +5 -8
  17. package/lib/commonjs/handlers/createHandler.js.map +1 -1
  18. package/lib/commonjs/handlers/gestures/GestureDetector/updateHandlers.js +12 -5
  19. package/lib/commonjs/handlers/gestures/GestureDetector/updateHandlers.js.map +1 -1
  20. package/lib/commonjs/handlers/gestures/GestureDetector/utils.js +3 -7
  21. package/lib/commonjs/handlers/gestures/GestureDetector/utils.js.map +1 -1
  22. package/lib/commonjs/web/tools/GestureHandlerOrchestrator.js +0 -1
  23. package/lib/commonjs/web/tools/GestureHandlerOrchestrator.js.map +1 -1
  24. package/lib/commonjs/web/tools/InteractionManager.js +1 -3
  25. package/lib/commonjs/web/tools/InteractionManager.js.map +1 -1
  26. package/lib/module/components/GestureButtons.js +4 -3
  27. package/lib/module/components/GestureButtons.js.map +1 -1
  28. package/lib/module/components/Pressable/Pressable.js +75 -39
  29. package/lib/module/components/Pressable/Pressable.js.map +1 -1
  30. package/lib/module/handlers/createHandler.js +5 -8
  31. package/lib/module/handlers/createHandler.js.map +1 -1
  32. package/lib/module/handlers/gestures/GestureDetector/updateHandlers.js +12 -5
  33. package/lib/module/handlers/gestures/GestureDetector/updateHandlers.js.map +1 -1
  34. package/lib/module/handlers/gestures/GestureDetector/utils.js +3 -6
  35. package/lib/module/handlers/gestures/GestureDetector/utils.js.map +1 -1
  36. package/lib/module/web/tools/GestureHandlerOrchestrator.js +0 -1
  37. package/lib/module/web/tools/GestureHandlerOrchestrator.js.map +1 -1
  38. package/lib/module/web/tools/InteractionManager.js +1 -2
  39. package/lib/module/web/tools/InteractionManager.js.map +1 -1
  40. package/lib/typescript/components/GestureButtons.d.ts +3 -3
  41. package/lib/typescript/components/Pressable/Pressable.d.ts +3 -1
  42. package/package.json +4 -2
  43. package/src/components/GestureButtons.tsx +13 -9
  44. package/src/components/Pressable/Pressable.tsx +400 -351
  45. package/src/handlers/createHandler.tsx +4 -8
  46. package/src/handlers/gestures/GestureDetector/updateHandlers.ts +11 -3
  47. package/src/handlers/gestures/GestureDetector/utils.ts +3 -7
  48. package/src/web/tools/GestureHandlerOrchestrator.ts +0 -1
  49. package/src/web/tools/InteractionManager.ts +1 -2
  50. package/lib/commonjs/getReactNativeVersion.js +0 -22
  51. package/lib/commonjs/getReactNativeVersion.js.map +0 -1
  52. package/lib/commonjs/getReactNativeVersion.web.js +0 -11
  53. package/lib/commonjs/getReactNativeVersion.web.js.map +0 -1
  54. package/lib/module/getReactNativeVersion.js +0 -10
  55. package/lib/module/getReactNativeVersion.js.map +0 -1
  56. package/lib/module/getReactNativeVersion.web.js +0 -4
  57. package/lib/module/getReactNativeVersion.web.js.map +0 -1
  58. package/lib/typescript/getReactNativeVersion.d.ts +0 -4
  59. package/lib/typescript/getReactNativeVersion.web.d.ts +0 -1
  60. package/src/getReactNativeVersion.ts +0 -11
  61. package/src/getReactNativeVersion.web.ts +0 -3
@@ -1,4 +1,12 @@
1
- import React, { useCallback, useMemo, useRef, useState } from 'react';
1
+ import React, {
2
+ ForwardedRef,
3
+ forwardRef,
4
+ RefObject,
5
+ useCallback,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
2
10
  import { GestureObjects as Gesture } from '../../handlers/gestures/gestureObjects';
3
11
  import { GestureDetector } from '../../handlers/gestures/GestureDetector';
4
12
  import { PressableEvent, PressableProps } from './PressableProps';
@@ -27,392 +35,433 @@ const IS_TEST_ENV = isTestEnv();
27
35
 
28
36
  let IS_FABRIC: null | boolean = null;
29
37
 
30
- export default function Pressable(props: PressableProps) {
31
- const {
32
- testOnly_pressed,
33
- hitSlop,
34
- pressRetentionOffset,
35
- delayHoverIn,
36
- onHoverIn,
37
- delayHoverOut,
38
- onHoverOut,
39
- delayLongPress,
40
- unstable_pressDelay,
41
- onPress,
42
- onPressIn,
43
- onPressOut,
44
- onLongPress,
45
- style,
46
- children,
47
- android_disableSound,
48
- android_ripple,
49
- disabled,
50
- ...remainingProps
51
- } = props;
52
-
53
- const [pressedState, setPressedState] = useState(testOnly_pressed ?? false);
54
-
55
- const pressableRef = useRef<View>(null);
56
-
57
- // Disabled when onLongPress has been called
58
- const isPressCallbackEnabled = useRef<boolean>(true);
59
- const hasPassedBoundsChecks = useRef<boolean>(false);
60
- const shouldPreventNativeEffects = useRef<boolean>(false);
61
-
62
- const normalizedHitSlop: Insets = useMemo(
63
- () =>
64
- typeof hitSlop === 'number' ? numberAsInset(hitSlop) : (hitSlop ?? {}),
65
- [hitSlop]
66
- );
67
-
68
- const normalizedPressRetentionOffset: Insets = useMemo(
69
- () =>
70
- typeof pressRetentionOffset === 'number'
71
- ? numberAsInset(pressRetentionOffset)
72
- : (pressRetentionOffset ?? {}),
73
- [pressRetentionOffset]
74
- );
75
-
76
- const hoverInTimeout = useRef<number | null>(null);
77
- const hoverOutTimeout = useRef<number | null>(null);
78
-
79
- const hoverGesture = useMemo(
80
- () =>
81
- Gesture.Hover()
82
- .manualActivation(true) // Stops Hover from blocking Native gesture activation on web
83
- .cancelsTouchesInView(false)
84
- .onBegin((event) => {
85
- if (hoverOutTimeout.current) {
86
- clearTimeout(hoverOutTimeout.current);
87
- }
88
- if (delayHoverIn) {
89
- hoverInTimeout.current = setTimeout(
90
- () => onHoverIn?.(gestureToPressableEvent(event)),
91
- delayHoverIn
92
- );
93
- return;
94
- }
95
- onHoverIn?.(gestureToPressableEvent(event));
96
- })
97
- .onFinalize((event) => {
98
- if (hoverInTimeout.current) {
99
- clearTimeout(hoverInTimeout.current);
100
- }
101
- if (delayHoverOut) {
102
- hoverOutTimeout.current = setTimeout(
103
- () => onHoverOut?.(gestureToPressableEvent(event)),
104
- delayHoverOut
105
- );
106
- return;
107
- }
108
- onHoverOut?.(gestureToPressableEvent(event));
109
- }),
110
- [delayHoverIn, delayHoverOut, onHoverIn, onHoverOut]
111
- );
112
-
113
- const pressDelayTimeoutRef = useRef<number | null>(null);
114
- const isTouchPropagationAllowed = useRef<boolean>(false);
115
-
116
- // iOS only: due to varying flow of gestures, events sometimes have to be saved for later use
117
- const deferredEventPayload = useRef<PressableEvent | null>(null);
38
+ const Pressable = forwardRef(
39
+ (props: PressableProps, pressableRef: ForwardedRef<View>) => {
40
+ const {
41
+ testOnly_pressed,
42
+ hitSlop,
43
+ pressRetentionOffset,
44
+ delayHoverIn,
45
+ onHoverIn,
46
+ delayHoverOut,
47
+ onHoverOut,
48
+ delayLongPress,
49
+ unstable_pressDelay,
50
+ onPress,
51
+ onPressIn,
52
+ onPressOut,
53
+ onLongPress,
54
+ style,
55
+ children,
56
+ android_disableSound,
57
+ android_ripple,
58
+ disabled,
59
+ accessible,
60
+ ...remainingProps
61
+ } = props;
62
+
63
+ const [pressedState, setPressedState] = useState(testOnly_pressed ?? false);
64
+
65
+ // Disabled when onLongPress has been called
66
+ const isPressCallbackEnabled = useRef<boolean>(true);
67
+ const hasPassedBoundsChecks = useRef<boolean>(false);
68
+ const shouldPreventNativeEffects = useRef<boolean>(false);
69
+
70
+ const normalizedHitSlop: Insets = useMemo(
71
+ () =>
72
+ typeof hitSlop === 'number' ? numberAsInset(hitSlop) : (hitSlop ?? {}),
73
+ [hitSlop]
74
+ );
75
+
76
+ const normalizedPressRetentionOffset: Insets = useMemo(
77
+ () =>
78
+ typeof pressRetentionOffset === 'number'
79
+ ? numberAsInset(pressRetentionOffset)
80
+ : (pressRetentionOffset ?? {}),
81
+ [pressRetentionOffset]
82
+ );
83
+
84
+ const hoverInTimeout = useRef<number | null>(null);
85
+ const hoverOutTimeout = useRef<number | null>(null);
86
+
87
+ const hoverGesture = useMemo(
88
+ () =>
89
+ Gesture.Hover()
90
+ .manualActivation(true) // Stops Hover from blocking Native gesture activation on web
91
+ .cancelsTouchesInView(false)
92
+ .onBegin((event) => {
93
+ if (hoverOutTimeout.current) {
94
+ clearTimeout(hoverOutTimeout.current);
95
+ }
96
+ if (delayHoverIn) {
97
+ hoverInTimeout.current = setTimeout(
98
+ () => onHoverIn?.(gestureToPressableEvent(event)),
99
+ delayHoverIn
100
+ );
101
+ return;
102
+ }
103
+ onHoverIn?.(gestureToPressableEvent(event));
104
+ })
105
+ .onFinalize((event) => {
106
+ if (hoverInTimeout.current) {
107
+ clearTimeout(hoverInTimeout.current);
108
+ }
109
+ if (delayHoverOut) {
110
+ hoverOutTimeout.current = setTimeout(
111
+ () => onHoverOut?.(gestureToPressableEvent(event)),
112
+ delayHoverOut
113
+ );
114
+ return;
115
+ }
116
+ onHoverOut?.(gestureToPressableEvent(event));
117
+ }),
118
+ [delayHoverIn, delayHoverOut, onHoverIn, onHoverOut]
119
+ );
118
120
 
119
- const pressInHandler = useCallback(
120
- (event: PressableEvent) => {
121
- if (handlingOnTouchesDown.current) {
122
- deferredEventPayload.current = event;
123
- }
121
+ const pressDelayTimeoutRef = useRef<number | null>(null);
122
+ const isTouchPropagationAllowed = useRef<boolean>(false);
124
123
 
125
- if (!isTouchPropagationAllowed.current) {
126
- return;
127
- }
124
+ // iOS only: due to varying flow of gestures, events sometimes have to be saved for later use
125
+ const deferredEventPayload = useRef<PressableEvent | null>(null);
128
126
 
129
- deferredEventPayload.current = null;
130
-
131
- onPressIn?.(event);
132
- isPressCallbackEnabled.current = true;
133
- pressDelayTimeoutRef.current = null;
134
- setPressedState(true);
135
- },
136
- [onPressIn]
137
- );
138
-
139
- const pressOutHandler = useCallback(
140
- (event: PressableEvent) => {
141
- if (
142
- !hasPassedBoundsChecks.current ||
143
- event.nativeEvent.touches.length >
144
- event.nativeEvent.changedTouches.length
145
- ) {
146
- return;
147
- }
127
+ const pressInHandler = useCallback(
128
+ (event: PressableEvent) => {
129
+ if (handlingOnTouchesDown.current) {
130
+ deferredEventPayload.current = event;
131
+ }
148
132
 
149
- if (unstable_pressDelay && pressDelayTimeoutRef.current !== null) {
150
- // When delay is preemptively finished by lifting touches,
151
- // we want to immediately activate it's effects - pressInHandler,
152
- // even though we are located at the pressOutHandler
153
- clearTimeout(pressDelayTimeoutRef.current);
154
- pressInHandler(event);
155
- }
133
+ if (!isTouchPropagationAllowed.current) {
134
+ return;
135
+ }
156
136
 
157
- if (deferredEventPayload.current) {
158
- onPressIn?.(deferredEventPayload.current);
159
137
  deferredEventPayload.current = null;
160
- }
161
-
162
- onPressOut?.(event);
163
138
 
164
- if (isPressCallbackEnabled.current) {
165
- onPress?.(event);
166
- }
167
-
168
- if (longPressTimeoutRef.current) {
169
- clearTimeout(longPressTimeoutRef.current);
170
- longPressTimeoutRef.current = null;
171
- }
139
+ onPressIn?.(event);
140
+ isPressCallbackEnabled.current = true;
141
+ pressDelayTimeoutRef.current = null;
142
+ setPressedState(true);
143
+ },
144
+ [onPressIn]
145
+ );
146
+
147
+ const pressOutHandler = useCallback(
148
+ (event: PressableEvent) => {
149
+ if (!isTouchPropagationAllowed.current) {
150
+ hasPassedBoundsChecks.current = false;
151
+ isPressCallbackEnabled.current = true;
152
+ deferredEventPayload.current = null;
153
+
154
+ if (longPressTimeoutRef.current) {
155
+ clearTimeout(longPressTimeoutRef.current);
156
+ longPressTimeoutRef.current = null;
157
+ }
172
158
 
173
- isTouchPropagationAllowed.current = false;
174
- hasPassedBoundsChecks.current = false;
175
- isPressCallbackEnabled.current = true;
176
- setPressedState(false);
177
- },
178
- [onPress, onPressIn, onPressOut, pressInHandler, unstable_pressDelay]
179
- );
180
-
181
- const handlingOnTouchesDown = useRef<boolean>(false);
182
- const onEndHandlingTouchesDown = useRef<(() => void) | null>(null);
183
- const cancelledMidPress = useRef<boolean>(false);
184
-
185
- const activateLongPress = useCallback(
186
- (event: GestureTouchEvent) => {
187
- if (!isTouchPropagationAllowed.current) {
188
- return;
189
- }
159
+ if (pressDelayTimeoutRef.current) {
160
+ clearTimeout(pressDelayTimeoutRef.current);
161
+ pressDelayTimeoutRef.current = null;
162
+ }
190
163
 
191
- if (hasPassedBoundsChecks.current) {
192
- onLongPress?.(gestureTouchToPressableEvent(event));
193
- isPressCallbackEnabled.current = false;
194
- }
164
+ return;
165
+ }
166
+
167
+ if (
168
+ !hasPassedBoundsChecks.current ||
169
+ event.nativeEvent.touches.length >
170
+ event.nativeEvent.changedTouches.length
171
+ ) {
172
+ return;
173
+ }
174
+
175
+ if (unstable_pressDelay && pressDelayTimeoutRef.current !== null) {
176
+ // When delay is preemptively finished by lifting touches,
177
+ // we want to immediately activate it's effects - pressInHandler,
178
+ // even though we are located at the pressOutHandler
179
+ clearTimeout(pressDelayTimeoutRef.current);
180
+ pressInHandler(event);
181
+ }
182
+
183
+ if (deferredEventPayload.current) {
184
+ onPressIn?.(deferredEventPayload.current);
185
+ deferredEventPayload.current = null;
186
+ }
187
+
188
+ onPressOut?.(event);
189
+
190
+ if (isPressCallbackEnabled.current) {
191
+ onPress?.(event);
192
+ }
193
+
194
+ if (longPressTimeoutRef.current) {
195
+ clearTimeout(longPressTimeoutRef.current);
196
+ longPressTimeoutRef.current = null;
197
+ }
198
+
199
+ isTouchPropagationAllowed.current = false;
200
+ hasPassedBoundsChecks.current = false;
201
+ isPressCallbackEnabled.current = true;
202
+ setPressedState(false);
203
+ },
204
+ [onPress, onPressIn, onPressOut, pressInHandler, unstable_pressDelay]
205
+ );
206
+
207
+ const handlingOnTouchesDown = useRef<boolean>(false);
208
+ const onEndHandlingTouchesDown = useRef<(() => void) | null>(null);
209
+ const cancelledMidPress = useRef<boolean>(false);
210
+
211
+ const activateLongPress = useCallback(
212
+ (event: GestureTouchEvent) => {
213
+ if (!isTouchPropagationAllowed.current) {
214
+ return;
215
+ }
216
+
217
+ if (hasPassedBoundsChecks.current) {
218
+ onLongPress?.(gestureTouchToPressableEvent(event));
219
+ isPressCallbackEnabled.current = false;
220
+ }
221
+
222
+ if (longPressTimeoutRef.current) {
223
+ clearTimeout(longPressTimeoutRef.current);
224
+ longPressTimeoutRef.current = null;
225
+ }
226
+ },
227
+ [onLongPress]
228
+ );
229
+
230
+ const longPressTimeoutRef = useRef<number | null>(null);
231
+ const longPressMinDuration =
232
+ (delayLongPress ?? DEFAULT_LONG_PRESS_DURATION) +
233
+ (unstable_pressDelay ?? 0);
234
+
235
+ const innerPressableRef = useRef<View>(null);
236
+
237
+ const measureCallback = useCallback(
238
+ (width: number, height: number, event: GestureTouchEvent) => {
239
+ if (
240
+ !isTouchWithinInset(
241
+ {
242
+ width,
243
+ height,
244
+ },
245
+ normalizedHitSlop,
246
+ event.changedTouches.at(-1)
247
+ ) ||
248
+ hasPassedBoundsChecks.current ||
249
+ cancelledMidPress.current
250
+ ) {
251
+ cancelledMidPress.current = false;
252
+ onEndHandlingTouchesDown.current = null;
253
+ handlingOnTouchesDown.current = false;
254
+ return;
255
+ }
256
+
257
+ hasPassedBoundsChecks.current = true;
258
+
259
+ // In case of multiple touches, the first one starts long press gesture
260
+ if (longPressTimeoutRef.current === null) {
261
+ // Start long press gesture timer
262
+ longPressTimeoutRef.current = setTimeout(
263
+ () => activateLongPress(event),
264
+ longPressMinDuration
265
+ );
266
+ }
267
+
268
+ if (unstable_pressDelay) {
269
+ pressDelayTimeoutRef.current = setTimeout(() => {
270
+ pressInHandler(gestureTouchToPressableEvent(event));
271
+ }, unstable_pressDelay);
272
+ } else {
273
+ pressInHandler(gestureTouchToPressableEvent(event));
274
+ }
275
+
276
+ onEndHandlingTouchesDown.current?.();
277
+ onEndHandlingTouchesDown.current = null;
278
+ handlingOnTouchesDown.current = false;
279
+ },
280
+ [
281
+ activateLongPress,
282
+ longPressMinDuration,
283
+ normalizedHitSlop,
284
+ pressInHandler,
285
+ unstable_pressDelay,
286
+ ]
287
+ );
288
+
289
+ const pressAndTouchGesture = useMemo(
290
+ () =>
291
+ Gesture.LongPress()
292
+ .minDuration(INT32_MAX) // Stops long press from blocking native gesture
293
+ .maxDistance(INT32_MAX) // Stops long press from cancelling after set distance
294
+ .cancelsTouchesInView(false)
295
+ .onTouchesDown((event) => {
296
+ handlingOnTouchesDown.current = true;
297
+ if (pressableRef) {
298
+ (pressableRef as RefObject<View>).current?.measure(
299
+ (_x, _y, width, height) => {
300
+ measureCallback(width, height, event);
301
+ }
302
+ );
303
+ } else {
304
+ innerPressableRef.current?.measure((_x, _y, width, height) => {
305
+ measureCallback(width, height, event);
306
+ });
307
+ }
308
+ })
309
+ .onTouchesUp((event) => {
310
+ if (handlingOnTouchesDown.current) {
311
+ onEndHandlingTouchesDown.current = () =>
312
+ pressOutHandler(gestureTouchToPressableEvent(event));
313
+ return;
314
+ }
315
+ // On iOS, short taps will make LongPress gesture call onTouchesUp before Native gesture calls onStart
316
+ // This variable ensures that onStart isn't detected as the first gesture since Pressable is pressed.
317
+ if (deferredEventPayload.current !== null) {
318
+ shouldPreventNativeEffects.current = true;
319
+ }
320
+ pressOutHandler(gestureTouchToPressableEvent(event));
321
+ })
322
+ .onTouchesCancelled((event) => {
323
+ isPressCallbackEnabled.current = false;
324
+
325
+ if (handlingOnTouchesDown.current) {
326
+ cancelledMidPress.current = true;
327
+ onEndHandlingTouchesDown.current = () =>
328
+ pressOutHandler(gestureTouchToPressableEvent(event));
329
+ return;
330
+ }
195
331
 
196
- if (longPressTimeoutRef.current) {
197
- clearTimeout(longPressTimeoutRef.current);
198
- longPressTimeoutRef.current = null;
199
- }
200
- },
201
- [onLongPress]
202
- );
203
-
204
- const longPressTimeoutRef = useRef<number | null>(null);
205
- const longPressMinDuration =
206
- (delayLongPress ?? DEFAULT_LONG_PRESS_DURATION) +
207
- (unstable_pressDelay ?? 0);
208
-
209
- const pressAndTouchGesture = useMemo(
210
- () =>
211
- Gesture.LongPress()
212
- .minDuration(INT32_MAX) // Stops long press from blocking native gesture
213
- .maxDistance(INT32_MAX) // Stops long press from cancelling after set distance
214
- .cancelsTouchesInView(false)
215
- .onTouchesDown((event) => {
216
- handlingOnTouchesDown.current = true;
217
- pressableRef.current?.measure((_x, _y, width, height) => {
218
332
  if (
219
- !isTouchWithinInset(
220
- {
221
- width,
222
- height,
223
- },
224
- normalizedHitSlop,
225
- event.changedTouches.at(-1)
226
- ) ||
227
- hasPassedBoundsChecks.current ||
228
- cancelledMidPress.current
333
+ !hasPassedBoundsChecks.current ||
334
+ event.allTouches.length > event.changedTouches.length
229
335
  ) {
230
- cancelledMidPress.current = false;
231
- onEndHandlingTouchesDown.current = null;
232
- handlingOnTouchesDown.current = false;
233
336
  return;
234
337
  }
235
338
 
236
- hasPassedBoundsChecks.current = true;
237
-
238
- // In case of multiple touches, the first one starts long press gesture
239
- if (longPressTimeoutRef.current === null) {
240
- // Start long press gesture timer
241
- longPressTimeoutRef.current = setTimeout(
242
- () => activateLongPress(event),
243
- longPressMinDuration
244
- );
339
+ pressOutHandler(gestureTouchToPressableEvent(event));
340
+ }),
341
+ [pressableRef, measureCallback, pressOutHandler]
342
+ );
343
+
344
+ // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events
345
+ const buttonGesture = useMemo(
346
+ () =>
347
+ Gesture.Native()
348
+ .onBegin(() => {
349
+ // Android sets BEGAN state on press down
350
+ if (Platform.OS === 'android' || Platform.OS === 'macos') {
351
+ isTouchPropagationAllowed.current = true;
245
352
  }
246
-
247
- if (unstable_pressDelay) {
248
- pressDelayTimeoutRef.current = setTimeout(() => {
249
- pressInHandler(gestureTouchToPressableEvent(event));
250
- }, unstable_pressDelay);
251
- } else {
252
- pressInHandler(gestureTouchToPressableEvent(event));
353
+ })
354
+ .onStart(() => {
355
+ if (Platform.OS === 'web') {
356
+ isTouchPropagationAllowed.current = true;
253
357
  }
254
358
 
255
- onEndHandlingTouchesDown.current?.();
256
- onEndHandlingTouchesDown.current = null;
257
- handlingOnTouchesDown.current = false;
258
- });
259
- })
260
- .onTouchesUp((event) => {
261
- if (handlingOnTouchesDown.current) {
262
- onEndHandlingTouchesDown.current = () =>
263
- pressOutHandler(gestureTouchToPressableEvent(event));
264
- return;
265
- }
266
- // On iOS, short taps will make LongPress gesture call onTouchesUp before Native gesture calls onStart
267
- // This variable ensures that onStart isn't detected as the first gesture since Pressable is pressed.
268
- if (deferredEventPayload.current !== null) {
269
- shouldPreventNativeEffects.current = true;
270
- }
271
- pressOutHandler(gestureTouchToPressableEvent(event));
272
- })
273
- .onTouchesCancelled((event) => {
274
- isPressCallbackEnabled.current = false;
275
-
276
- if (handlingOnTouchesDown.current) {
277
- cancelledMidPress.current = true;
278
- onEndHandlingTouchesDown.current = () =>
279
- pressOutHandler(gestureTouchToPressableEvent(event));
280
- return;
281
- }
359
+ // iOS sets ACTIVE state on press down
360
+ if (Platform.OS !== 'ios') {
361
+ return;
362
+ }
282
363
 
283
- if (
284
- !hasPassedBoundsChecks.current ||
285
- event.allTouches.length > event.changedTouches.length
286
- ) {
287
- return;
288
- }
364
+ if (deferredEventPayload.current) {
365
+ isTouchPropagationAllowed.current = true;
289
366
 
290
- pressOutHandler(gestureTouchToPressableEvent(event));
291
- }),
292
- [
293
- activateLongPress,
294
- longPressMinDuration,
295
- normalizedHitSlop,
296
- pressInHandler,
297
- pressOutHandler,
298
- unstable_pressDelay,
299
- ]
300
- );
301
-
302
- // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events
303
- const buttonGesture = useMemo(
304
- () =>
305
- Gesture.Native()
306
- .onBegin(() => {
307
- // Android sets BEGAN state on press down
308
- if (Platform.OS === 'android' || Platform.OS === 'macos') {
309
- isTouchPropagationAllowed.current = true;
310
- }
311
- })
312
- .onStart(() => {
313
- if (Platform.OS === 'web') {
314
- isTouchPropagationAllowed.current = true;
315
- }
367
+ if (hasPassedBoundsChecks.current) {
368
+ pressInHandler(deferredEventPayload.current);
369
+ deferredEventPayload.current = null;
370
+ } else {
371
+ pressOutHandler(deferredEventPayload.current);
372
+ isTouchPropagationAllowed.current = false;
373
+ }
316
374
 
317
- // iOS sets ACTIVE state on press down
318
- if (Platform.OS !== 'ios') {
319
- return;
320
- }
321
-
322
- if (deferredEventPayload.current) {
323
- isTouchPropagationAllowed.current = true;
375
+ return;
376
+ }
324
377
 
325
378
  if (hasPassedBoundsChecks.current) {
326
- pressInHandler(deferredEventPayload.current);
327
- deferredEventPayload.current = null;
328
- } else {
329
- pressOutHandler(deferredEventPayload.current);
330
- isTouchPropagationAllowed.current = false;
379
+ isTouchPropagationAllowed.current = true;
380
+ return;
331
381
  }
332
382
 
333
- return;
334
- }
383
+ if (shouldPreventNativeEffects.current) {
384
+ shouldPreventNativeEffects.current = false;
385
+ if (!handlingOnTouchesDown.current) {
386
+ return;
387
+ }
388
+ }
335
389
 
336
- if (hasPassedBoundsChecks.current) {
337
390
  isTouchPropagationAllowed.current = true;
338
- return;
339
- }
391
+ }),
392
+ [pressInHandler, pressOutHandler]
393
+ );
340
394
 
341
- if (shouldPreventNativeEffects.current) {
342
- shouldPreventNativeEffects.current = false;
343
- return;
344
- }
395
+ const appliedHitSlop = addInsets(
396
+ normalizedHitSlop,
397
+ normalizedPressRetentionOffset
398
+ );
345
399
 
346
- isTouchPropagationAllowed.current = true;
347
- }),
348
- [pressInHandler, pressOutHandler]
349
- );
400
+ const isPressableEnabled = disabled !== true;
350
401
 
351
- const appliedHitSlop = addInsets(
352
- normalizedHitSlop,
353
- normalizedPressRetentionOffset
354
- );
402
+ const gestures = [buttonGesture, pressAndTouchGesture, hoverGesture];
355
403
 
356
- const isPressableEnabled = disabled !== true;
404
+ for (const gesture of gestures) {
405
+ gesture.enabled(isPressableEnabled);
406
+ gesture.runOnJS(true);
407
+ gesture.hitSlop(appliedHitSlop);
408
+ gesture.shouldCancelWhenOutside(Platform.OS === 'web' ? false : true);
409
+ }
357
410
 
358
- const gestures = [buttonGesture, pressAndTouchGesture, hoverGesture];
411
+ // Uses different hitSlop, to activate on hitSlop area instead of pressRetentionOffset area
412
+ buttonGesture.hitSlop(normalizedHitSlop);
359
413
 
360
- for (const gesture of gestures) {
361
- gesture.enabled(isPressableEnabled);
362
- gesture.runOnJS(true);
363
- gesture.hitSlop(appliedHitSlop);
364
- gesture.shouldCancelWhenOutside(Platform.OS === 'web' ? false : true);
365
- }
414
+ const gesture = Gesture.Simultaneous(...gestures);
366
415
 
367
- // Uses different hitSlop, to activate on hitSlop area instead of pressRetentionOffset area
368
- buttonGesture.hitSlop(normalizedHitSlop);
416
+ // `cursor: 'pointer'` on `RNButton` crashes iOS
417
+ const pointerStyle: StyleProp<ViewStyle> =
418
+ Platform.OS === 'web' ? { cursor: 'pointer' } : {};
369
419
 
370
- const gesture = Gesture.Simultaneous(...gestures);
420
+ const styleProp =
421
+ typeof style === 'function' ? style({ pressed: pressedState }) : style;
371
422
 
372
- // `cursor: 'pointer'` on `RNButton` crashes iOS
373
- const pointerStyle: StyleProp<ViewStyle> =
374
- Platform.OS === 'web' ? { cursor: 'pointer' } : {};
423
+ const childrenProp =
424
+ typeof children === 'function'
425
+ ? children({ pressed: pressedState })
426
+ : children;
375
427
 
376
- const styleProp =
377
- typeof style === 'function' ? style({ pressed: pressedState }) : style;
378
-
379
- const childrenProp =
380
- typeof children === 'function'
381
- ? children({ pressed: pressedState })
382
- : children;
428
+ const rippleColor = useMemo(() => {
429
+ if (IS_FABRIC === null) {
430
+ IS_FABRIC = isFabric();
431
+ }
383
432
 
384
- const rippleColor = useMemo(() => {
385
- if (IS_FABRIC === null) {
386
- IS_FABRIC = isFabric();
387
- }
433
+ const defaultRippleColor = android_ripple ? undefined : 'transparent';
434
+ const unprocessedRippleColor =
435
+ android_ripple?.color ?? defaultRippleColor;
436
+ return IS_FABRIC
437
+ ? unprocessedRippleColor
438
+ : processColor(unprocessedRippleColor);
439
+ }, [android_ripple]);
440
+
441
+ return (
442
+ <GestureDetector gesture={gesture}>
443
+ <NativeButton
444
+ {...remainingProps}
445
+ ref={pressableRef ?? innerPressableRef}
446
+ accessible={accessible !== false}
447
+ hitSlop={appliedHitSlop}
448
+ enabled={isPressableEnabled}
449
+ touchSoundDisabled={android_disableSound ?? undefined}
450
+ rippleColor={rippleColor}
451
+ rippleRadius={android_ripple?.radius ?? undefined}
452
+ style={[pointerStyle, styleProp]}
453
+ testOnly_onPress={IS_TEST_ENV ? onPress : undefined}
454
+ testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined}
455
+ testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined}
456
+ testOnly_onLongPress={IS_TEST_ENV ? onLongPress : undefined}>
457
+ {childrenProp}
458
+ {__DEV__ ? (
459
+ <PressabilityDebugView color="red" hitSlop={normalizedHitSlop} />
460
+ ) : null}
461
+ </NativeButton>
462
+ </GestureDetector>
463
+ );
464
+ }
465
+ );
388
466
 
389
- const defaultRippleColor = android_ripple ? undefined : 'transparent';
390
- const unprocessedRippleColor = android_ripple?.color ?? defaultRippleColor;
391
- return IS_FABRIC
392
- ? unprocessedRippleColor
393
- : processColor(unprocessedRippleColor);
394
- }, [android_ripple]);
395
-
396
- return (
397
- <GestureDetector gesture={gesture}>
398
- <NativeButton
399
- {...remainingProps}
400
- ref={pressableRef}
401
- hitSlop={appliedHitSlop}
402
- enabled={isPressableEnabled}
403
- touchSoundDisabled={android_disableSound ?? undefined}
404
- rippleColor={rippleColor}
405
- rippleRadius={android_ripple?.radius ?? undefined}
406
- style={[pointerStyle, styleProp]}
407
- testOnly_onPress={IS_TEST_ENV ? onPress : undefined}
408
- testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined}
409
- testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined}
410
- testOnly_onLongPress={IS_TEST_ENV ? onLongPress : undefined}>
411
- {childrenProp}
412
- {__DEV__ ? (
413
- <PressabilityDebugView color="red" hitSlop={normalizedHitSlop} />
414
- ) : null}
415
- </NativeButton>
416
- </GestureDetector>
417
- );
418
- }
467
+ export default Pressable;