react-native-gesture-handler 3.2.0-nightly-20260811-cc704f09a → 3.2.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.
- package/android/build.gradle +21 -6
- package/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt +30 -66
- package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt +4 -2
- package/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerDetectorView.kt +1 -2
- package/apple/RNGestureHandler.mm +31 -23
- package/apple/RNGestureHandlerButtonComponentView.mm +10 -6
- package/lib/module/components/GestureHandlerButton.web.js +3 -2
- package/lib/module/components/GestureHandlerButton.web.js.map +1 -1
- package/lib/module/components/ReanimatedDrawerLayout.js +3 -0
- package/lib/module/components/ReanimatedDrawerLayout.js.map +1 -1
- package/lib/module/components/touchables/TouchableOpacity.js +1 -0
- package/lib/module/components/touchables/TouchableOpacity.js.map +1 -1
- package/lib/module/handlers/hitSlop.js +101 -0
- package/lib/module/handlers/hitSlop.js.map +1 -0
- package/lib/module/handlers/utils.js +3 -7
- package/lib/module/handlers/utils.js.map +1 -1
- package/lib/module/specs/RNGestureHandlerDetectorNativeComponent.ts +1 -1
- package/lib/module/specs/RNGestureHandlerRootViewNativeComponent.ts +1 -1
- package/lib/module/v3/components/Pressable.js +23 -312
- package/lib/module/v3/components/Pressable.js.map +1 -1
- package/lib/module/v3/components/PressableWithTouchable.js +259 -0
- package/lib/module/v3/components/PressableWithTouchable.js.map +1 -0
- package/lib/module/v3/components/StatefulPressable.js +338 -0
- package/lib/module/v3/components/StatefulPressable.js.map +1 -0
- package/lib/module/v3/components/pointerStyle.js +6 -0
- package/lib/module/v3/components/pointerStyle.js.map +1 -0
- package/lib/module/v3/components/pointerStyle.web.js +9 -0
- package/lib/module/v3/components/pointerStyle.web.js.map +1 -0
- package/lib/module/v3/hooks/utils/configUtils.js +3 -1
- package/lib/module/v3/hooks/utils/configUtils.js.map +1 -1
- package/lib/module/v3/hooks/utils/reanimatedUtils.js +14 -3
- package/lib/module/v3/hooks/utils/reanimatedUtils.js.map +1 -1
- package/lib/module/web/handlers/GestureHandler.js +21 -44
- package/lib/module/web/handlers/GestureHandler.js.map +1 -1
- package/lib/module/web/interfaces.js.map +1 -1
- package/lib/typescript/components/GestureComponents.web.d.ts +12 -4
- package/lib/typescript/components/GestureComponents.web.d.ts.map +1 -1
- package/lib/typescript/components/GestureHandlerButton.web.d.ts.map +1 -1
- package/lib/typescript/components/ReanimatedDrawerLayout.d.ts.map +1 -1
- package/lib/typescript/components/touchables/TouchableHighlight.d.ts +1 -1
- package/lib/typescript/components/touchables/TouchableHighlight.d.ts.map +1 -1
- package/lib/typescript/components/touchables/TouchableOpacity.d.ts.map +1 -1
- package/lib/typescript/handlers/hitSlop.d.ts +51 -0
- package/lib/typescript/handlers/hitSlop.d.ts.map +1 -0
- package/lib/typescript/handlers/utils.d.ts.map +1 -1
- package/lib/typescript/mocks/hostDetector.d.ts +5 -2
- package/lib/typescript/mocks/hostDetector.d.ts.map +1 -1
- package/lib/typescript/specs/RNGestureHandlerDetectorNativeComponent.d.ts +1 -1
- package/lib/typescript/specs/RNGestureHandlerDetectorNativeComponent.d.ts.map +1 -1
- package/lib/typescript/specs/RNGestureHandlerRootViewNativeComponent.d.ts +1 -1
- package/lib/typescript/specs/RNGestureHandlerRootViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/v3/components/GestureComponents.web.d.ts +16 -8
- package/lib/typescript/v3/components/GestureComponents.web.d.ts.map +1 -1
- package/lib/typescript/v3/components/Pressable.d.ts +13 -0
- package/lib/typescript/v3/components/Pressable.d.ts.map +1 -1
- package/lib/typescript/v3/components/PressableWithTouchable.d.ts +5 -0
- package/lib/typescript/v3/components/PressableWithTouchable.d.ts.map +1 -0
- package/lib/typescript/v3/components/StatefulPressable.d.ts +5 -0
- package/lib/typescript/v3/components/StatefulPressable.d.ts.map +1 -0
- package/lib/typescript/v3/components/pointerStyle.d.ts +3 -0
- package/lib/typescript/v3/components/pointerStyle.d.ts.map +1 -0
- package/lib/typescript/v3/components/pointerStyle.web.d.ts +3 -0
- package/lib/typescript/v3/components/pointerStyle.web.d.ts.map +1 -0
- package/lib/typescript/v3/detectors/ReanimatedNativeDetector.d.ts +3 -1
- package/lib/typescript/v3/detectors/ReanimatedNativeDetector.d.ts.map +1 -1
- package/lib/typescript/v3/detectors/ReanimatedNativeDetector.web.d.ts +3 -1
- package/lib/typescript/v3/detectors/ReanimatedNativeDetector.web.d.ts.map +1 -1
- package/lib/typescript/v3/detectors/common.d.ts +2 -2
- package/lib/typescript/v3/detectors/common.d.ts.map +1 -1
- package/lib/typescript/v3/hooks/utils/configUtils.d.ts.map +1 -1
- package/lib/typescript/v3/hooks/utils/reanimatedUtils.d.ts.map +1 -1
- package/lib/typescript/v3/types/ConfigTypes.d.ts +2 -1
- package/lib/typescript/v3/types/ConfigTypes.d.ts.map +1 -1
- package/lib/typescript/v3/types/EventTypes.d.ts +3 -2
- package/lib/typescript/v3/types/EventTypes.d.ts.map +1 -1
- package/lib/typescript/web/handlers/GestureHandler.d.ts +0 -1
- package/lib/typescript/web/handlers/GestureHandler.d.ts.map +1 -1
- package/lib/typescript/web/interfaces.d.ts +3 -12
- package/lib/typescript/web/interfaces.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/components/GestureHandlerButton.web.tsx +3 -2
- package/src/components/ReanimatedDrawerLayout.tsx +3 -0
- package/src/components/touchables/TouchableOpacity.tsx +1 -0
- package/src/handlers/hitSlop.ts +146 -0
- package/src/handlers/utils.ts +4 -2
- package/src/specs/RNGestureHandlerDetectorNativeComponent.ts +1 -1
- package/src/specs/RNGestureHandlerRootViewNativeComponent.ts +1 -1
- package/src/v3/components/Pressable.tsx +28 -448
- package/src/v3/components/PressableWithTouchable.tsx +326 -0
- package/src/v3/components/StatefulPressable.tsx +476 -0
- package/src/v3/components/pointerStyle.ts +5 -0
- package/src/v3/components/pointerStyle.web.ts +6 -0
- package/src/v3/hooks/utils/configUtils.ts +7 -1
- package/src/v3/hooks/utils/reanimatedUtils.ts +17 -8
- package/src/v3/types/ConfigTypes.ts +2 -1
- package/src/v3/types/EventTypes.ts +7 -3
- package/src/web/handlers/GestureHandler.ts +26 -77
- package/src/web/interfaces.ts +3 -13
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
use,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type {
|
|
10
|
+
Insets,
|
|
11
|
+
LayoutChangeEvent,
|
|
12
|
+
StyleProp,
|
|
13
|
+
ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import { Platform } from 'react-native';
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
PressableDimensions,
|
|
19
|
+
PressableEvent,
|
|
20
|
+
PressableProps,
|
|
21
|
+
} from '../../components/Pressable/PressableProps';
|
|
22
|
+
import {
|
|
23
|
+
getStatesConfig,
|
|
24
|
+
StateMachineEvent,
|
|
25
|
+
} from '../../components/Pressable/stateDefinitions';
|
|
26
|
+
import { PressableStateMachine } from '../../components/Pressable/StateMachine';
|
|
27
|
+
import {
|
|
28
|
+
addInsets,
|
|
29
|
+
gestureToPressableEvent,
|
|
30
|
+
gestureTouchToPressableEvent,
|
|
31
|
+
isTouchWithinInset,
|
|
32
|
+
numberAsInset,
|
|
33
|
+
viewCenterToPressableEvent,
|
|
34
|
+
} from '../../components/Pressable/utils';
|
|
35
|
+
import { getTVProps } from '../../components/utils';
|
|
36
|
+
import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
|
|
37
|
+
import { useIsScreenReaderEnabled } from '../../useIsScreenReaderEnabled';
|
|
38
|
+
import { INT32_MAX, isTestEnv } from '../../utils';
|
|
39
|
+
import { GestureDetector } from '../detectors';
|
|
40
|
+
import {
|
|
41
|
+
useHoverGesture,
|
|
42
|
+
useLongPressGesture,
|
|
43
|
+
useNativeGesture,
|
|
44
|
+
useSimultaneousGestures,
|
|
45
|
+
} from '../hooks';
|
|
46
|
+
import {
|
|
47
|
+
isKeyboardDismissingTap,
|
|
48
|
+
JSResponderContext,
|
|
49
|
+
} from '../scrollViewInterop';
|
|
50
|
+
import { PureNativeButton } from './GestureButtons';
|
|
51
|
+
|
|
52
|
+
const DEFAULT_LONG_PRESS_DURATION = 500;
|
|
53
|
+
const IS_TEST_ENV = isTestEnv();
|
|
54
|
+
|
|
55
|
+
const StatefulPressable = (props: PressableProps) => {
|
|
56
|
+
const {
|
|
57
|
+
testOnly_pressed,
|
|
58
|
+
hitSlop,
|
|
59
|
+
pressRetentionOffset,
|
|
60
|
+
delayHoverIn,
|
|
61
|
+
delayHoverOut,
|
|
62
|
+
delayLongPress,
|
|
63
|
+
unstable_pressDelay,
|
|
64
|
+
onHoverIn,
|
|
65
|
+
onHoverOut,
|
|
66
|
+
onPress,
|
|
67
|
+
onPressIn,
|
|
68
|
+
onPressOut,
|
|
69
|
+
onLongPress,
|
|
70
|
+
onLayout,
|
|
71
|
+
style,
|
|
72
|
+
children,
|
|
73
|
+
android_disableSound,
|
|
74
|
+
android_ripple,
|
|
75
|
+
disabled,
|
|
76
|
+
accessible,
|
|
77
|
+
simultaneousWith,
|
|
78
|
+
requireToFail,
|
|
79
|
+
block,
|
|
80
|
+
ref,
|
|
81
|
+
...remainingProps
|
|
82
|
+
} = props;
|
|
83
|
+
|
|
84
|
+
const [pressedState, setPressedState] = useState(false);
|
|
85
|
+
|
|
86
|
+
const longPressTimeoutRef = useRef<number | null>(null);
|
|
87
|
+
const pressDelayTimeoutRef = useRef<number | null>(null);
|
|
88
|
+
const isOnPressAllowed = useRef<boolean>(true);
|
|
89
|
+
const jsResponderContext = use(JSResponderContext);
|
|
90
|
+
const isCurrentlyPressed = useRef<boolean>(false);
|
|
91
|
+
const dimensions = useRef<PressableDimensions>({
|
|
92
|
+
width: 0,
|
|
93
|
+
height: 0,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// When the touch that begins a press is the one dismissing the keyboard
|
|
97
|
+
// (keyboardShouldPersistTaps="never"), the press is swallowed to match RN's
|
|
98
|
+
// touchables.
|
|
99
|
+
const dropKeyboardTapRef = useRef<boolean | null>(null);
|
|
100
|
+
|
|
101
|
+
const normalizedHitSlop: Insets = useMemo(
|
|
102
|
+
() =>
|
|
103
|
+
typeof hitSlop === 'number'
|
|
104
|
+
? numberAsInset(hitSlop)
|
|
105
|
+
: (hitSlop ?? numberAsInset(0)),
|
|
106
|
+
[hitSlop]
|
|
107
|
+
);
|
|
108
|
+
const normalizedPressRetentionOffset: Insets = useMemo(
|
|
109
|
+
() =>
|
|
110
|
+
typeof pressRetentionOffset === 'number'
|
|
111
|
+
? numberAsInset(pressRetentionOffset)
|
|
112
|
+
: (pressRetentionOffset ?? {}),
|
|
113
|
+
[pressRetentionOffset]
|
|
114
|
+
);
|
|
115
|
+
const appliedHitSlop = addInsets(
|
|
116
|
+
normalizedHitSlop,
|
|
117
|
+
normalizedPressRetentionOffset
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const cancelLongPress = useCallback(() => {
|
|
121
|
+
if (longPressTimeoutRef.current) {
|
|
122
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
123
|
+
longPressTimeoutRef.current = null;
|
|
124
|
+
isOnPressAllowed.current = true;
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const cancelDelayedPress = useCallback(() => {
|
|
129
|
+
if (pressDelayTimeoutRef.current) {
|
|
130
|
+
clearTimeout(pressDelayTimeoutRef.current);
|
|
131
|
+
pressDelayTimeoutRef.current = null;
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const startLongPress = useCallback(
|
|
136
|
+
(event: PressableEvent) => {
|
|
137
|
+
if (onLongPress) {
|
|
138
|
+
cancelLongPress();
|
|
139
|
+
longPressTimeoutRef.current = setTimeout(() => {
|
|
140
|
+
isOnPressAllowed.current = false;
|
|
141
|
+
onLongPress(event);
|
|
142
|
+
}, delayLongPress ?? DEFAULT_LONG_PRESS_DURATION);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[onLongPress, cancelLongPress, delayLongPress]
|
|
146
|
+
);
|
|
147
|
+
const innerHandlePressIn = useCallback(
|
|
148
|
+
(event: PressableEvent) => {
|
|
149
|
+
onPressIn?.(event);
|
|
150
|
+
startLongPress(event);
|
|
151
|
+
setPressedState(true);
|
|
152
|
+
if (pressDelayTimeoutRef.current) {
|
|
153
|
+
clearTimeout(pressDelayTimeoutRef.current);
|
|
154
|
+
pressDelayTimeoutRef.current = null;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[onPressIn, startLongPress]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handleFinalize = useCallback(() => {
|
|
161
|
+
isCurrentlyPressed.current = false;
|
|
162
|
+
dropKeyboardTapRef.current = null;
|
|
163
|
+
cancelLongPress();
|
|
164
|
+
cancelDelayedPress();
|
|
165
|
+
setPressedState(false);
|
|
166
|
+
}, [cancelDelayedPress, cancelLongPress]);
|
|
167
|
+
|
|
168
|
+
const captureKeyboardDismiss = useCallback(() => {
|
|
169
|
+
dropKeyboardTapRef.current ??= isKeyboardDismissingTap(jsResponderContext);
|
|
170
|
+
}, [jsResponderContext]);
|
|
171
|
+
|
|
172
|
+
const handlePressIn = useCallback(
|
|
173
|
+
(event: PressableEvent, skipBoundsCheck = false) => {
|
|
174
|
+
if (
|
|
175
|
+
!skipBoundsCheck &&
|
|
176
|
+
!isTouchWithinInset(
|
|
177
|
+
dimensions.current,
|
|
178
|
+
normalizedHitSlop,
|
|
179
|
+
event.nativeEvent.changedTouches.at(-1)
|
|
180
|
+
)
|
|
181
|
+
) {
|
|
182
|
+
// Ignoring pressIn within pressRetentionOffset
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
isCurrentlyPressed.current = true;
|
|
187
|
+
if (unstable_pressDelay) {
|
|
188
|
+
pressDelayTimeoutRef.current = setTimeout(() => {
|
|
189
|
+
innerHandlePressIn(event);
|
|
190
|
+
}, unstable_pressDelay);
|
|
191
|
+
} else {
|
|
192
|
+
innerHandlePressIn(event);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
[innerHandlePressIn, normalizedHitSlop, unstable_pressDelay]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const handlePressOut = useCallback(
|
|
199
|
+
(event: PressableEvent, success: boolean = true) => {
|
|
200
|
+
if (!isCurrentlyPressed.current) {
|
|
201
|
+
// Some prop configurations may lead to handlePressOut being called multiple times.
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isCurrentlyPressed.current = false;
|
|
206
|
+
|
|
207
|
+
if (pressDelayTimeoutRef.current) {
|
|
208
|
+
innerHandlePressIn(event);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
onPressOut?.(event);
|
|
212
|
+
|
|
213
|
+
if (isOnPressAllowed.current && success) {
|
|
214
|
+
onPress?.(event);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
handleFinalize();
|
|
218
|
+
},
|
|
219
|
+
[handleFinalize, innerHandlePressIn, onPress, onPressOut]
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const stateMachine = useMemo(() => new PressableStateMachine(), []);
|
|
223
|
+
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const configuration = getStatesConfig(
|
|
227
|
+
handlePressIn,
|
|
228
|
+
handlePressOut,
|
|
229
|
+
isScreenReaderEnabled
|
|
230
|
+
);
|
|
231
|
+
stateMachine.setStates(configuration);
|
|
232
|
+
}, [handlePressIn, handlePressOut, stateMachine, isScreenReaderEnabled]);
|
|
233
|
+
|
|
234
|
+
const hoverInTimeout = useRef<number | null>(null);
|
|
235
|
+
const hoverOutTimeout = useRef<number | null>(null);
|
|
236
|
+
|
|
237
|
+
useEffect(
|
|
238
|
+
() => () => {
|
|
239
|
+
if (longPressTimeoutRef.current) {
|
|
240
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
241
|
+
}
|
|
242
|
+
if (pressDelayTimeoutRef.current) {
|
|
243
|
+
clearTimeout(pressDelayTimeoutRef.current);
|
|
244
|
+
}
|
|
245
|
+
if (hoverInTimeout.current) {
|
|
246
|
+
clearTimeout(hoverInTimeout.current);
|
|
247
|
+
}
|
|
248
|
+
if (hoverOutTimeout.current) {
|
|
249
|
+
clearTimeout(hoverOutTimeout.current);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const hoverGesture = useHoverGesture({
|
|
256
|
+
manualActivation: true, // Prevents Hover blocking Native gesture on web
|
|
257
|
+
cancelsTouchesInView: false,
|
|
258
|
+
onBegin: (event) => {
|
|
259
|
+
if (hoverOutTimeout.current) {
|
|
260
|
+
clearTimeout(hoverOutTimeout.current);
|
|
261
|
+
}
|
|
262
|
+
if (delayHoverIn) {
|
|
263
|
+
hoverInTimeout.current = setTimeout(
|
|
264
|
+
() => onHoverIn?.(gestureToPressableEvent(event)),
|
|
265
|
+
delayHoverIn
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
onHoverIn?.(gestureToPressableEvent(event));
|
|
270
|
+
},
|
|
271
|
+
onFinalize: (event) => {
|
|
272
|
+
if (hoverInTimeout.current) {
|
|
273
|
+
clearTimeout(hoverInTimeout.current);
|
|
274
|
+
}
|
|
275
|
+
if (delayHoverOut) {
|
|
276
|
+
hoverOutTimeout.current = setTimeout(
|
|
277
|
+
() => onHoverOut?.(gestureToPressableEvent(event)),
|
|
278
|
+
delayHoverOut
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
onHoverOut?.(gestureToPressableEvent(event));
|
|
283
|
+
},
|
|
284
|
+
enabled: disabled !== true,
|
|
285
|
+
disableReanimated: true,
|
|
286
|
+
simultaneousWith,
|
|
287
|
+
block,
|
|
288
|
+
requireToFail,
|
|
289
|
+
hitSlop: appliedHitSlop,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const pressAndTouchGesture = useLongPressGesture({
|
|
293
|
+
minDuration: Platform.OS === 'web' ? 0 : INT32_MAX, // Long press handles finalize on web, thus it must activate right away
|
|
294
|
+
maxDistance: INT32_MAX, // Stops long press from cancelling on touch move
|
|
295
|
+
cancelsTouchesInView: false,
|
|
296
|
+
onTouchesDown: (event) => {
|
|
297
|
+
captureKeyboardDismiss();
|
|
298
|
+
|
|
299
|
+
if (dropKeyboardTapRef.current) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const pressableEvent = gestureTouchToPressableEvent(event);
|
|
304
|
+
stateMachine.handleEvent(
|
|
305
|
+
StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
|
|
306
|
+
pressableEvent
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
onTouchesUp: () => {
|
|
310
|
+
if (Platform.OS === 'android' && !isScreenReaderEnabled) {
|
|
311
|
+
// Prevents potential soft-locks
|
|
312
|
+
stateMachine.reset();
|
|
313
|
+
handleFinalize();
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
onTouchesCancel: (event) => {
|
|
317
|
+
const pressableEvent = gestureTouchToPressableEvent(event);
|
|
318
|
+
stateMachine.reset();
|
|
319
|
+
handlePressOut(pressableEvent, false);
|
|
320
|
+
},
|
|
321
|
+
onFinalize: (event) => {
|
|
322
|
+
if (Platform.OS !== 'web') {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
stateMachine.handleEvent(
|
|
327
|
+
event.canceled ? StateMachineEvent.CANCEL : StateMachineEvent.FINALIZE
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
handleFinalize();
|
|
331
|
+
},
|
|
332
|
+
enabled: disabled !== true,
|
|
333
|
+
disableReanimated: true,
|
|
334
|
+
simultaneousWith: simultaneousWith,
|
|
335
|
+
block: block,
|
|
336
|
+
requireToFail: requireToFail,
|
|
337
|
+
hitSlop: appliedHitSlop,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events
|
|
341
|
+
const buttonGesture = useNativeGesture({
|
|
342
|
+
onTouchesCancel: (event) => {
|
|
343
|
+
if (Platform.OS !== 'macos' && Platform.OS !== 'web') {
|
|
344
|
+
// On MacOS cancel occurs in middle of gesture
|
|
345
|
+
// On Web cancel occurs on mouse move, which is unwanted
|
|
346
|
+
const pressableEvent = gestureTouchToPressableEvent(event);
|
|
347
|
+
stateMachine.reset();
|
|
348
|
+
handlePressOut(pressableEvent, false);
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
onBegin: () => {
|
|
352
|
+
captureKeyboardDismiss();
|
|
353
|
+
|
|
354
|
+
if (dropKeyboardTapRef.current) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (Platform.isTV) {
|
|
359
|
+
// tvOS drives this native gesture from the focus-engine Select press.
|
|
360
|
+
// The press state machine is touch-based and never
|
|
361
|
+
// receives LONG_PRESS_TOUCHES_DOWN here, so bypass it and drive the press handlers directly.
|
|
362
|
+
// A focus-driven press has no coordinates, so skip the hit-slop bounds check entirely.
|
|
363
|
+
handlePressIn(viewCenterToPressableEvent(dimensions.current), true);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (Platform.OS === 'android' && isScreenReaderEnabled) {
|
|
367
|
+
stateMachine.handleEvent(
|
|
368
|
+
StateMachineEvent.NATIVE_BEGIN,
|
|
369
|
+
viewCenterToPressableEvent(dimensions.current)
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
|
|
374
|
+
},
|
|
375
|
+
onActivate: () => {
|
|
376
|
+
if (!Platform.isTV && Platform.OS !== 'android') {
|
|
377
|
+
stateMachine.handleEvent(StateMachineEvent.NATIVE_START);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
onFinalize: (event) => {
|
|
381
|
+
// On Web we use LongPress.onFinalize instead of Native.onFinalize,
|
|
382
|
+
// as Native cancels on mouse move, and LongPress does not.
|
|
383
|
+
if (Platform.OS === 'web') {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (Platform.isTV) {
|
|
388
|
+
handlePressOut(
|
|
389
|
+
viewCenterToPressableEvent(dimensions.current),
|
|
390
|
+
!event.canceled
|
|
391
|
+
);
|
|
392
|
+
handleFinalize();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
stateMachine.handleEvent(
|
|
397
|
+
event.canceled ? StateMachineEvent.CANCEL : StateMachineEvent.FINALIZE
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
handleFinalize();
|
|
401
|
+
},
|
|
402
|
+
enabled: disabled !== true,
|
|
403
|
+
disableReanimated: true,
|
|
404
|
+
simultaneousWith,
|
|
405
|
+
block,
|
|
406
|
+
requireToFail,
|
|
407
|
+
hitSlop: appliedHitSlop,
|
|
408
|
+
shouldActivateOnStart: Platform.OS === 'web',
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const gesture = useSimultaneousGestures(
|
|
412
|
+
buttonGesture,
|
|
413
|
+
pressAndTouchGesture,
|
|
414
|
+
hoverGesture
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// `cursor: 'pointer'` on `RNButton` crashes iOS
|
|
418
|
+
const pointerStyle: StyleProp<ViewStyle> =
|
|
419
|
+
Platform.OS === 'web' ? { cursor: 'pointer' } : {};
|
|
420
|
+
|
|
421
|
+
// `testOnly_pressed` forces the pressed state for snapshots/tests. Derive the
|
|
422
|
+
// displayed value from it each render, keeping the interactive `pressedState`
|
|
423
|
+
// independent (seeded to false) so clearing the prop doesn't leave it stuck.
|
|
424
|
+
const displayPressed = testOnly_pressed ?? pressedState;
|
|
425
|
+
|
|
426
|
+
const styleProp =
|
|
427
|
+
typeof style === 'function' ? style({ pressed: displayPressed }) : style;
|
|
428
|
+
|
|
429
|
+
const childrenProp =
|
|
430
|
+
typeof children === 'function'
|
|
431
|
+
? children({ pressed: displayPressed })
|
|
432
|
+
: children;
|
|
433
|
+
|
|
434
|
+
const rippleColor = useMemo(() => {
|
|
435
|
+
const defaultRippleColor = android_ripple ? undefined : 'transparent';
|
|
436
|
+
return android_ripple?.color ?? defaultRippleColor;
|
|
437
|
+
}, [android_ripple]);
|
|
438
|
+
|
|
439
|
+
const setDimensions = useCallback(
|
|
440
|
+
(event: LayoutChangeEvent) => {
|
|
441
|
+
onLayout?.(event);
|
|
442
|
+
dimensions.current = event.nativeEvent.layout;
|
|
443
|
+
},
|
|
444
|
+
[onLayout]
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const tvProps = getTVProps(remainingProps);
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<GestureDetector gesture={gesture}>
|
|
451
|
+
<PureNativeButton
|
|
452
|
+
{...remainingProps}
|
|
453
|
+
ref={ref as React.Ref<React.ComponentRef<typeof PureNativeButton>>}
|
|
454
|
+
{...tvProps}
|
|
455
|
+
onLayout={setDimensions}
|
|
456
|
+
accessible={accessible !== false}
|
|
457
|
+
hitSlop={appliedHitSlop}
|
|
458
|
+
enabled={disabled !== true}
|
|
459
|
+
touchSoundDisabled={android_disableSound ?? undefined}
|
|
460
|
+
rippleColor={rippleColor}
|
|
461
|
+
rippleRadius={android_ripple?.radius ?? undefined}
|
|
462
|
+
style={[pointerStyle, styleProp]}
|
|
463
|
+
testOnly_onPress={IS_TEST_ENV ? onPress : undefined}
|
|
464
|
+
testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined}
|
|
465
|
+
testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined}
|
|
466
|
+
testOnly_onLongPress={IS_TEST_ENV ? onLongPress : undefined}>
|
|
467
|
+
{childrenProp}
|
|
468
|
+
{__DEV__ ? (
|
|
469
|
+
<PressabilityDebugView color="red" hitSlop={normalizedHitSlop} />
|
|
470
|
+
) : null}
|
|
471
|
+
</PureNativeButton>
|
|
472
|
+
</GestureDetector>
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export default StatefulPressable;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
// react-native-web sets `cursor: 'pointer'` on its interactive components
|
|
4
|
+
// (Pressable, TouchableOpacity, …) but not on `View`, which the native button
|
|
5
|
+
// renders — so the Touchable-based Pressable adds it here to match RN Pressable.
|
|
6
|
+
export const pointerStyle: ViewStyle = { cursor: 'pointer' };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
|
|
3
|
+
import type { HitSlop } from '../../../handlers/gestureHandlerCommon';
|
|
3
4
|
import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
|
|
5
|
+
import { normalizeHitSlop } from '../../../handlers/hitSlop';
|
|
4
6
|
import { isTestEnv, tagMessage } from '../../../utils';
|
|
5
7
|
import type {
|
|
6
8
|
BaseGestureConfig,
|
|
@@ -108,8 +110,12 @@ export function prepareConfigForNativeSide<
|
|
|
108
110
|
for (const [key, value] of Object.entries(config)) {
|
|
109
111
|
// @ts-ignore That's the point, we want to see if key exists in the whitelists
|
|
110
112
|
if (allowedNativeProps.has(key) || handlerPropsWhiteList.has(key)) {
|
|
113
|
+
const unpackedValue = maybeUnpackValue(value);
|
|
114
|
+
|
|
111
115
|
(filteredConfig as Record<string, unknown>)[key] =
|
|
112
|
-
|
|
116
|
+
key === 'hitSlop'
|
|
117
|
+
? normalizeHitSlop(unpackedValue as HitSlop)
|
|
118
|
+
: unpackedValue;
|
|
113
119
|
} else if (PropsToFilter.has(key)) {
|
|
114
120
|
continue;
|
|
115
121
|
} else {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { HitSlop } from '../../../handlers/gestureHandlerCommon';
|
|
1
2
|
import {
|
|
2
3
|
Reanimated,
|
|
3
4
|
Worklets,
|
|
4
5
|
} from '../../../handlers/gestures/reanimatedWrapper';
|
|
6
|
+
import { normalizeHitSlop } from '../../../handlers/hitSlop';
|
|
5
7
|
import { NativeProxy } from '../../NativeProxy';
|
|
6
8
|
import type {
|
|
7
9
|
BaseGestureConfig,
|
|
@@ -53,14 +55,21 @@ export function bindSharedValues<
|
|
|
53
55
|
const listenerId = baseListenerId + keyHash;
|
|
54
56
|
|
|
55
57
|
sharedValue.addListener(listenerId, (value) => {
|
|
56
|
-
|
|
57
|
-
handlerTag,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
58
|
+
if (configKey === 'runOnJS') {
|
|
59
|
+
updateGestureHandlerConfig(handlerTag, {
|
|
60
|
+
dispatchesReanimatedEvents: shouldUseReanimatedDetector && !value,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (configKey === 'hitSlop') {
|
|
66
|
+
updateGestureHandlerConfig(handlerTag, {
|
|
67
|
+
hitSlop: normalizeHitSlop(value as HitSlop),
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
updateGestureHandlerConfig(handlerTag, { [configKey]: value });
|
|
64
73
|
});
|
|
65
74
|
};
|
|
66
75
|
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TouchAction,
|
|
7
7
|
UserSelect,
|
|
8
8
|
} from '../../handlers/gestureHandlerCommon';
|
|
9
|
+
import type { NormalizedHitSlop } from '../../handlers/hitSlop';
|
|
9
10
|
import type {
|
|
10
11
|
AnimatedEvent,
|
|
11
12
|
ChangeCalculatorType,
|
|
@@ -73,7 +74,7 @@ export type CommonGestureConfig = {
|
|
|
73
74
|
runOnJS?: boolean | undefined;
|
|
74
75
|
enabled?: boolean | undefined;
|
|
75
76
|
shouldCancelWhenOutside?: boolean | undefined;
|
|
76
|
-
hitSlop?: HitSlop | undefined;
|
|
77
|
+
hitSlop?: HitSlop | NormalizedHitSlop | undefined;
|
|
77
78
|
activeCursor?: ActiveCursor | undefined;
|
|
78
79
|
mouseButton?: MouseButton | undefined;
|
|
79
80
|
cancelsTouchesInView?: boolean | undefined;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type { BaseSyntheticEvent } from 'react';
|
|
2
|
-
|
|
3
1
|
import type { GestureTouchEvent } from '../../handlers/gestureHandlerCommon';
|
|
4
2
|
import type { PointerType } from '../../PointerType';
|
|
5
3
|
import type { State } from '../../State';
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
// Structural stand-in for a synthetic event carrying `nativeEvent`. Kept
|
|
6
|
+
// minimal on purpose: RN 0.87's Strict TS API makes the other synthetic-event
|
|
7
|
+
// fields optional, so any richer shape stops being a superset of the real
|
|
8
|
+
// `NativeSyntheticEvent` across RN versions. We only ever read `nativeEvent`.
|
|
9
|
+
export type NativeEventWrapper<T> = {
|
|
10
|
+
nativeEvent: T;
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
// Payload of the managed button's press events. The codegen spec
|
|
10
14
|
// (`specs/RNGestureHandlerButtonNativeComponent`) declares its own
|