react-native-gesture-handler 3.2.0-nightly-20260812-bd853311a → 3.2.1

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 (51) hide show
  1. package/android/build.gradle +21 -6
  2. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +4 -2
  3. package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +1 -2
  4. package/lib/module/components/GestureHandlerButton.js.map +1 -1
  5. package/lib/module/components/Pressable/Pressable.js +2 -0
  6. package/lib/module/components/Pressable/Pressable.js.map +1 -1
  7. package/lib/module/specs/RNGestureHandlerDetectorNativeComponent.ts +1 -1
  8. package/lib/module/specs/RNGestureHandlerRootViewNativeComponent.ts +1 -1
  9. package/lib/module/v3/components/Pressable.js +23 -312
  10. package/lib/module/v3/components/Pressable.js.map +1 -1
  11. package/lib/module/v3/components/PressableWithTouchable.js +267 -0
  12. package/lib/module/v3/components/PressableWithTouchable.js.map +1 -0
  13. package/lib/module/v3/components/StatefulPressable.js +340 -0
  14. package/lib/module/v3/components/StatefulPressable.js.map +1 -0
  15. package/lib/module/v3/components/pointerStyle.js +6 -0
  16. package/lib/module/v3/components/pointerStyle.js.map +1 -0
  17. package/lib/module/v3/components/pointerStyle.web.js +9 -0
  18. package/lib/module/v3/components/pointerStyle.web.js.map +1 -0
  19. package/lib/typescript/components/GestureButtonsProps.d.ts +10 -0
  20. package/lib/typescript/components/GestureButtonsProps.d.ts.map +1 -1
  21. package/lib/typescript/components/GestureHandlerButton.d.ts +10 -0
  22. package/lib/typescript/components/GestureHandlerButton.d.ts.map +1 -1
  23. package/lib/typescript/components/Pressable/Pressable.d.ts.map +1 -1
  24. package/lib/typescript/components/Pressable/PressableProps.d.ts +3 -3
  25. package/lib/typescript/components/Pressable/PressableProps.d.ts.map +1 -1
  26. package/lib/typescript/specs/RNGestureHandlerDetectorNativeComponent.d.ts +1 -1
  27. package/lib/typescript/specs/RNGestureHandlerDetectorNativeComponent.d.ts.map +1 -1
  28. package/lib/typescript/specs/RNGestureHandlerRootViewNativeComponent.d.ts +1 -1
  29. package/lib/typescript/specs/RNGestureHandlerRootViewNativeComponent.d.ts.map +1 -1
  30. package/lib/typescript/v3/components/Pressable.d.ts +13 -0
  31. package/lib/typescript/v3/components/Pressable.d.ts.map +1 -1
  32. package/lib/typescript/v3/components/PressableWithTouchable.d.ts +5 -0
  33. package/lib/typescript/v3/components/PressableWithTouchable.d.ts.map +1 -0
  34. package/lib/typescript/v3/components/StatefulPressable.d.ts +5 -0
  35. package/lib/typescript/v3/components/StatefulPressable.d.ts.map +1 -0
  36. package/lib/typescript/v3/components/pointerStyle.d.ts +3 -0
  37. package/lib/typescript/v3/components/pointerStyle.d.ts.map +1 -0
  38. package/lib/typescript/v3/components/pointerStyle.web.d.ts +3 -0
  39. package/lib/typescript/v3/components/pointerStyle.web.d.ts.map +1 -0
  40. package/package.json +1 -1
  41. package/src/components/GestureButtonsProps.ts +14 -0
  42. package/src/components/GestureHandlerButton.tsx +14 -0
  43. package/src/components/Pressable/Pressable.tsx +3 -1
  44. package/src/components/Pressable/PressableProps.tsx +3 -3
  45. package/src/specs/RNGestureHandlerDetectorNativeComponent.ts +1 -1
  46. package/src/specs/RNGestureHandlerRootViewNativeComponent.ts +1 -1
  47. package/src/v3/components/Pressable.tsx +28 -448
  48. package/src/v3/components/PressableWithTouchable.tsx +335 -0
  49. package/src/v3/components/StatefulPressable.tsx +478 -0
  50. package/src/v3/components/pointerStyle.ts +5 -0
  51. package/src/v3/components/pointerStyle.web.ts +6 -0
@@ -0,0 +1,335 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import type { Insets, LayoutChangeEvent } from 'react-native';
3
+
4
+ import type { ButtonEvent } from '../../components/GestureHandlerButton';
5
+ import type {
6
+ InnerPressableEvent,
7
+ PressableDimensions,
8
+ PressableProps,
9
+ } from '../../components/Pressable/PressableProps';
10
+ import {
11
+ addInsets,
12
+ isTouchWithinInset,
13
+ numberAsInset,
14
+ } from '../../components/Pressable/utils';
15
+ import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
16
+ import { isTestEnv } from '../../utils';
17
+ import { pointerStyle } from './pointerStyle';
18
+ import { Touchable } from './Touchable/Touchable';
19
+
20
+ const IS_TEST_ENV = isTestEnv();
21
+
22
+ // RN's Pressable default. Touchable's own default is 600ms
23
+ const DEFAULT_LONG_PRESS_DURATION = 500;
24
+
25
+ type Timers = {
26
+ press: ReturnType<typeof setTimeout> | null;
27
+ hoverIn: ReturnType<typeof setTimeout> | null;
28
+ hoverOut: ReturnType<typeof setTimeout> | null;
29
+ };
30
+
31
+ function normalizeInset(value: Insets | number | null | undefined): Insets {
32
+ return typeof value === 'number'
33
+ ? numberAsInset(value)
34
+ : (value ?? numberAsInset(0));
35
+ }
36
+
37
+ function buttonToInner(event: ButtonEvent): InnerPressableEvent {
38
+ return {
39
+ identifier: 0,
40
+ locationX: event.x,
41
+ locationY: event.y,
42
+ pageX: event.absoluteX,
43
+ pageY: event.absoluteY,
44
+ target: 0,
45
+ timestamp: Date.now(),
46
+ touches: [],
47
+ changedTouches: [],
48
+ force: undefined,
49
+ };
50
+ }
51
+
52
+ function buttonToPressableEvent(event: ButtonEvent) {
53
+ const inner = buttonToInner(event);
54
+
55
+ return {
56
+ nativeEvent: {
57
+ ...inner,
58
+ touches: [inner],
59
+ changedTouches: [inner],
60
+ },
61
+ };
62
+ }
63
+
64
+ const PressableWithTouchable = (props: PressableProps) => {
65
+ const {
66
+ testOnly_pressed,
67
+ hitSlop,
68
+ pressRetentionOffset,
69
+ delayHoverIn,
70
+ delayHoverOut,
71
+ delayLongPress,
72
+ unstable_pressDelay,
73
+ onHoverIn,
74
+ onHoverOut,
75
+ onPress,
76
+ onPressIn,
77
+ onPressOut,
78
+ onLongPress,
79
+ onLayout,
80
+ style,
81
+ children,
82
+ android_disableSound,
83
+ android_ripple,
84
+ disabled,
85
+ accessible,
86
+ ref,
87
+ ...rest
88
+ } = props;
89
+
90
+ // Drop props Touchable doesn't take: `cancelable`/`dimensionsAfterResize` are
91
+ // unsupported; the relation props are handled by the wrapper.
92
+ //
93
+ /* eslint-disable @typescript-eslint/no-unused-vars */
94
+ const {
95
+ cancelable,
96
+ dimensionsAfterResize,
97
+ simultaneousWith,
98
+ requireToFail,
99
+ block,
100
+ ...remainingProps
101
+ } = rest;
102
+ /* eslint-enable @typescript-eslint/no-unused-vars */
103
+
104
+ const [pressed, setPressed] = useState(false);
105
+ const timers = useRef<Timers>({ press: null, hoverIn: null, hoverOut: null });
106
+ const dimensions = useRef<PressableDimensions>({ width: 0, height: 0 });
107
+
108
+ // Whether the in-progress press activated within hitSlop (see handlePressIn).
109
+ const isActive = useRef(false);
110
+
111
+ useEffect(
112
+ () => () => {
113
+ const pending = timers.current;
114
+
115
+ if (pending.press) {
116
+ clearTimeout(pending.press);
117
+ }
118
+
119
+ if (pending.hoverIn) {
120
+ clearTimeout(pending.hoverIn);
121
+ }
122
+
123
+ if (pending.hoverOut) {
124
+ clearTimeout(pending.hoverOut);
125
+ }
126
+ },
127
+ []
128
+ );
129
+
130
+ // RN's ripple config allows `null` on every field; Touchable's excludes it.
131
+ // Normalize `null` → `undefined` so the types line up.
132
+ const androidRipple = android_ripple
133
+ ? {
134
+ color: android_ripple.color ?? undefined,
135
+ borderless: android_ripple.borderless ?? undefined,
136
+ radius: android_ripple.radius ?? undefined,
137
+ foreground: android_ripple.foreground ?? undefined,
138
+ }
139
+ : undefined;
140
+
141
+ // Activation is gated to `normalizedHitSlop` (see handlePressIn); the native
142
+ // button gets the wider `appliedHitSlop` so an active press is retained out
143
+ // to hitSlop + pressRetentionOffset before it cancels.
144
+ const normalizedHitSlop = normalizeInset(hitSlop);
145
+ const appliedHitSlop = addInsets(
146
+ normalizedHitSlop,
147
+ normalizeInset(pressRetentionOffset)
148
+ );
149
+
150
+ // Long press is measured from onPressIn, which `unstable_pressDelay` defers,
151
+ // so fold it in to match RN / StatefulPressable.
152
+ const resolvedDelayLongPress =
153
+ (delayLongPress ?? DEFAULT_LONG_PRESS_DURATION) +
154
+ (unstable_pressDelay ?? 0);
155
+
156
+ const handleLayout = (event: LayoutChangeEvent) => {
157
+ onLayout?.(event);
158
+ dimensions.current = event.nativeEvent.layout;
159
+ };
160
+
161
+ const firePressIn = (event: ButtonEvent) => {
162
+ setPressed(true);
163
+ onPressIn?.(buttonToPressableEvent(event));
164
+ };
165
+
166
+ const handlePressIn = (event: ButtonEvent) => {
167
+ // A down in the retention-only zone is held by the button but isn't a press.
168
+ isActive.current = isTouchWithinInset(
169
+ dimensions.current,
170
+ normalizedHitSlop,
171
+ buttonToInner(event)
172
+ );
173
+
174
+ if (!isActive.current) {
175
+ return;
176
+ }
177
+
178
+ if (unstable_pressDelay) {
179
+ // Drop a still-pending timer so a re-entrant press can't double-fire.
180
+ if (timers.current.press) {
181
+ clearTimeout(timers.current.press);
182
+ }
183
+
184
+ timers.current.press = setTimeout(() => {
185
+ timers.current.press = null;
186
+ firePressIn(event);
187
+ }, unstable_pressDelay);
188
+ return;
189
+ }
190
+
191
+ firePressIn(event);
192
+ };
193
+
194
+ const handlePressOut = (event: ButtonEvent) => {
195
+ // Not cleared here: onPress fires after onPressOut and must stay suppressed
196
+ // too; isActive resets on the next press-in.
197
+ if (!isActive.current) {
198
+ return;
199
+ }
200
+
201
+ // If the touch is released before `unstable_pressDelay` elapses, RN still
202
+ // emits the deferred `onPressIn` before `onPressOut` — flush it now.
203
+ if (timers.current.press) {
204
+ clearTimeout(timers.current.press);
205
+ timers.current.press = null;
206
+ firePressIn(event);
207
+ }
208
+
209
+ setPressed(false);
210
+ onPressOut?.(buttonToPressableEvent(event));
211
+ };
212
+
213
+ const handlePress = onPress
214
+ ? (event: ButtonEvent) => {
215
+ if (isActive.current) {
216
+ onPress(buttonToPressableEvent(event));
217
+ }
218
+ }
219
+ : undefined;
220
+
221
+ const handleLongPress = onLongPress
222
+ ? (event: ButtonEvent) => {
223
+ if (!isActive.current) {
224
+ return;
225
+ }
226
+ // Flush the deferred onPressIn so it can't arrive after onLongPress
227
+ // (e.g. delayLongPress={0} makes the two timers coincide).
228
+ if (timers.current.press) {
229
+ clearTimeout(timers.current.press);
230
+ timers.current.press = null;
231
+ firePressIn(event);
232
+ }
233
+ onLongPress(buttonToPressableEvent(event));
234
+ }
235
+ : undefined;
236
+
237
+ // Wire each handler whenever the opposite side can leave a pending timer, so a
238
+ // delayed hover callback is cancelled once the pointer leaves/re-enters.
239
+ const needsHoverIn =
240
+ onHoverIn != null || (onHoverOut != null && !!delayHoverOut);
241
+ const needsHoverOut =
242
+ onHoverOut != null || (onHoverIn != null && !!delayHoverIn);
243
+
244
+ const handleHoverIn = needsHoverIn
245
+ ? (event: ButtonEvent) => {
246
+ if (timers.current.hoverOut) {
247
+ clearTimeout(timers.current.hoverOut);
248
+ timers.current.hoverOut = null;
249
+ }
250
+
251
+ if (!onHoverIn) {
252
+ return;
253
+ }
254
+
255
+ if (delayHoverIn) {
256
+ timers.current.hoverIn = setTimeout(() => {
257
+ timers.current.hoverIn = null;
258
+ onHoverIn(buttonToPressableEvent(event));
259
+ }, delayHoverIn);
260
+ return;
261
+ }
262
+
263
+ onHoverIn(buttonToPressableEvent(event));
264
+ }
265
+ : undefined;
266
+
267
+ const handleHoverOut = needsHoverOut
268
+ ? (event: ButtonEvent) => {
269
+ if (timers.current.hoverIn) {
270
+ clearTimeout(timers.current.hoverIn);
271
+ timers.current.hoverIn = null;
272
+ }
273
+
274
+ if (!onHoverOut) {
275
+ return;
276
+ }
277
+
278
+ if (delayHoverOut) {
279
+ timers.current.hoverOut = setTimeout(() => {
280
+ timers.current.hoverOut = null;
281
+ onHoverOut(buttonToPressableEvent(event));
282
+ }, delayHoverOut);
283
+ return;
284
+ }
285
+
286
+ onHoverOut(buttonToPressableEvent(event));
287
+ }
288
+ : undefined;
289
+
290
+ // `testOnly_pressed` forces the pressed state for snapshots/tests. Derive the
291
+ // displayed value from it each render, keeping the interactive `pressed` state
292
+ // independent (seeded to false) so clearing the prop doesn't leave it stuck.
293
+ const displayPressed = testOnly_pressed ?? pressed;
294
+
295
+ const resolvedStyle =
296
+ typeof style === 'function' ? style({ pressed: displayPressed }) : style;
297
+
298
+ const resolvedChildren =
299
+ typeof children === 'function'
300
+ ? children({ pressed: displayPressed })
301
+ : children;
302
+
303
+ return (
304
+ <Touchable
305
+ {...remainingProps}
306
+ ref={ref}
307
+ accessible={accessible !== false}
308
+ disabled={disabled === true}
309
+ hitSlop={appliedHitSlop}
310
+ onLayout={handleLayout}
311
+ androidRipple={androidRipple}
312
+ touchSoundDisabled={android_disableSound ?? undefined}
313
+ delayLongPress={resolvedDelayLongPress}
314
+ style={[pointerStyle, resolvedStyle]}
315
+ onPressIn={handlePressIn}
316
+ onPressOut={handlePressOut}
317
+ onPress={handlePress}
318
+ onLongPress={handleLongPress}
319
+ onHoverIn={handleHoverIn}
320
+ onHoverOut={handleHoverOut}
321
+ testOnly_onPress={IS_TEST_ENV ? onPress : undefined}
322
+ testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined}
323
+ testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined}
324
+ testOnly_onLongPress={IS_TEST_ENV ? onLongPress : undefined}
325
+ testOnly_onHoverIn={IS_TEST_ENV ? onHoverIn : undefined}
326
+ testOnly_onHoverOut={IS_TEST_ENV ? onHoverOut : undefined}>
327
+ {resolvedChildren}
328
+ {__DEV__ ? (
329
+ <PressabilityDebugView color="red" hitSlop={normalizedHitSlop} />
330
+ ) : null}
331
+ </Touchable>
332
+ );
333
+ };
334
+
335
+ export default PressableWithTouchable;