Skip to content

Commit c9b9a18

Browse files
committed
Handle press events
1 parent 0196da5 commit c9b9a18

4 files changed

Lines changed: 84 additions & 15 deletions

File tree

packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
ColorValue,
44
HostComponent,
55
LayoutChangeEvent,
6+
NativeSyntheticEvent,
67
StyleProp,
78
ViewProps,
89
ViewStyle,
@@ -28,6 +29,36 @@ export interface ButtonProps extends ViewProps, AccessibilityProps {
2829
*/
2930
exclusive?: boolean | undefined;
3031

32+
/**
33+
* Called when the button gets pressed.
34+
*/
35+
onPress?: ((event: NativeSyntheticEvent<ButtonEvent>) => void) | undefined;
36+
37+
/**
38+
* Called when the pointer touches the button.
39+
*/
40+
onPressIn?: ((event: NativeSyntheticEvent<ButtonEvent>) => void) | undefined;
41+
42+
/**
43+
* Called when the pointer is released or leaves the button.
44+
*/
45+
onPressOut?: ((event: NativeSyntheticEvent<ButtonEvent>) => void) | undefined;
46+
47+
/**
48+
* Called when the button gets pressed and held past `longPressDuration`.
49+
*/
50+
onLongPress?:
51+
| ((event: NativeSyntheticEvent<ButtonEvent>) => void)
52+
| undefined;
53+
54+
/**
55+
* Called when the interaction with the button ends, after any terminal
56+
* `onPressOut`/`onPress` events, regardless of how it ended.
57+
*/
58+
onInteractionFinished?:
59+
| ((event: NativeSyntheticEvent<ButtonEvent>) => void)
60+
| undefined;
61+
3162
/**
3263
* Android only.
3364
*

packages/react-native-gesture-handler/src/v3/components/GestureButtonsProps.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import type { NativeWrapperProperties } from '../types/NativeWrapperType';
1010
export interface RawButtonProps
1111
extends Omit<
1212
ButtonProps,
13+
// The native press events are omitted — the deprecated buttons drive
14+
// their press callbacks from the gesture in JS and redeclare them with
15+
// their own signatures.
16+
| 'onPress'
17+
| 'onPressIn'
18+
| 'onPressOut'
19+
| 'onLongPress'
20+
| 'onInteractionFinished'
1321
| 'defaultOpacity'
1422
| 'defaultScale'
1523
| 'defaultUnderlayOpacity'

packages/react-native-gesture-handler/src/v3/components/Touchable/Touchable.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,13 @@ import {
1212
isKeyboardDismissingTap,
1313
JSResponderContext,
1414
} from '../ScrollViewResponderInterceptor';
15-
import type {
16-
AnimationDuration,
17-
CallbackEventType,
18-
EndCallbackEventType,
19-
TouchableProps,
20-
} from './TouchableProps';
15+
import type { AnimationDuration, TouchableProps } from './TouchableProps';
2116

2217
const isAndroid = Platform.OS === 'android';
2318
const TRANSPARENT_RIPPLE = { rippleColor: 'transparent' as const };
2419
const DEFAULT_IN_DURATION_MS = 50;
2520
const DEFAULT_OUT_DURATION_MS = 100;
2621

27-
enum PointerState {
28-
UNKNOWN,
29-
INSIDE,
30-
OUTSIDE,
31-
}
32-
3322
// Clamp user-supplied durations to finite, non-negative milliseconds.
3423
// Negative, NaN, or Infinity values would produce invalid CSS transitions
3524
// on web and negative setTimeout delays in branch 3 of the press-out path.
@@ -102,34 +91,65 @@ export const Touchable = (props: TouchableProps) => {
10291

10392
const shouldUseNativeRipple = isAndroid && androidRipple !== undefined;
10493

94+
const jsResponderContext = use(JSResponderContext);
95+
const dropKeyboardTapRef = useRef<boolean | null>(null);
96+
10597
const internalOnPress = useCallback(
10698
(e: NativeSyntheticEvent<ButtonEvent>) => {
99+
if (dropKeyboardTapRef.current) {
100+
return;
101+
}
102+
107103
onPress?.(e.nativeEvent);
108104
},
109105
[onPress]
110106
);
111107

112108
const internalOnPressIn = useCallback(
113109
(e: NativeSyntheticEvent<ButtonEvent>) => {
110+
// PressIn opens every press sequence; capture the verdict once per
111+
// sequence so a re-entry PressIn (with cancelOnLeave={false}) doesn't
112+
// overwrite it after the keyboard is already dismissed.
113+
dropKeyboardTapRef.current ??=
114+
isKeyboardDismissingTap(jsResponderContext);
115+
116+
if (dropKeyboardTapRef.current) {
117+
return;
118+
}
119+
114120
onPressIn?.(e.nativeEvent);
115121
},
116-
[onPressIn]
122+
[jsResponderContext, onPressIn]
117123
);
118124

119125
const internalOnPressOut = useCallback(
120126
(e: NativeSyntheticEvent<ButtonEvent>) => {
127+
if (dropKeyboardTapRef.current) {
128+
return;
129+
}
130+
121131
onPressOut?.(e.nativeEvent);
122132
},
123133
[onPressOut]
124134
);
125135

126136
const internalOnLongPress = useCallback(
127137
(e: NativeSyntheticEvent<ButtonEvent>) => {
138+
if (dropKeyboardTapRef.current) {
139+
return;
140+
}
141+
128142
onLongPress?.(e.nativeEvent);
129143
},
130144
[onLongPress]
131145
);
132146

147+
// InteractionFinished is dispatched after the terminal PressOut/Press
148+
// events, so resetting synchronously here is safe.
149+
const internalOnInteractionFinished = useCallback(() => {
150+
dropKeyboardTapRef.current = null;
151+
}, []);
152+
133153
const nativeGesture = useNativeGesture({
134154
hitSlop: props.hitSlop,
135155
testID: props.testID,
@@ -170,7 +190,8 @@ export const Touchable = (props: TouchableProps) => {
170190
onPress={internalOnPress}
171191
onPressIn={internalOnPressIn}
172192
onPressOut={internalOnPressOut}
173-
onLongPress={internalOnLongPress}>
193+
onLongPress={internalOnLongPress}
194+
onInteractionFinished={internalOnInteractionFinished}>
174195
{children}
175196
</GestureHandlerButton>
176197
</NativeDetector>

packages/react-native-gesture-handler/src/v3/components/Touchable/TouchableProps.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ type PressableAndroidRippleConfig = {
2020

2121
type RippleProps = 'rippleColor' | 'rippleRadius' | 'borderless' | 'foreground';
2222

23+
// The press events are redeclared below with the unwrapped `ButtonEvent`
24+
// signature; `onInteractionFinished` is consumed internally by `Touchable`.
25+
type PressProps =
26+
| 'onPress'
27+
| 'onPressIn'
28+
| 'onPressOut'
29+
| 'onLongPress'
30+
| 'onInteractionFinished';
31+
2332
type DurationProps =
2433
| 'tapAnimationInDuration'
2534
| 'tapAnimationOutDuration'
@@ -60,7 +69,7 @@ export type AnimationDuration =
6069

6170
export type TouchableProps = Omit<
6271
ButtonProps,
63-
RippleProps | 'enabled' | DurationProps
72+
RippleProps | PressProps | 'enabled' | DurationProps
6473
> &
6574
Omit<
6675
BaseButtonProps,

0 commit comments

Comments
 (0)