Skip to content

Commit f4038e9

Browse files
authored
Stop the Pressable long press timer on touch up (#4644)
## Description When a `Pressable` has `requireToFail`, the native button finalizes only after the other gesture fails, so the long press timer started at press-in keeps running after the finger lifted. With the default tap `maxDelay` (200 ms) and `delayLongPress` (500 ms), a tap held for 300 to 500 ms logs `onLongPress` about 100 ms after the lift and never gets `onPress`. This was always the case on iOS; #4639 brought Android to the same state. The long press gesture now stops the pending timer on touch up. To keep a real long press suppressing `onPress`, the fired timer clears its own id, `cancelLongPress` no longer touches `isOnPressAllowed`, and every press-in resets the flag instead. No new hooks. Follow-up to #4633 and #4639, surfaced by the CodeRabbit review on #4639. ## Test plan Drop the snippet below into the empty example and hold each box. - 400 ms: onPressIn, onPressOut, onPress on both boxes, no onLongPress - 700 ms: onPressIn, onLongPress, onPressOut, no onPress on both boxes - 400 ms again after the long press: onPress is back Before this change the lower box logged onLongPress after the lift and no onPress for the 400 ms hold, on iOS and Android. Also rerun the #4630 regression matrix (disable under the finger, pressDelay, RN and RNGH ScrollView drag, parent pan, requireToFail double tap, modal, unmount, nested Pressable, hitSlop), each row followed by a plain tap: every row identical to #4639 on both platforms. <details> <summary>Repro</summary> ```tsx import React, { useRef, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureHandlerRootView, Pressable, useTapGesture, } from 'react-native-gesture-handler'; export default function Repro() { const [lines, setLines] = useState<string[]>([]); const pressInTime = useRef(0); const log = (line: string) => { const stamped = `${line} +${Date.now() - pressInTime.current}ms`; setLines((current) => [...current.slice(-7), stamped]); }; const logPressIn = (name: string) => () => { pressInTime.current = Date.now(); log(`${name}: onPressIn`); }; const doubleTap = useTapGesture({ numberOfTaps: 2, runOnJS: true }); return ( <GestureHandlerRootView style={styles.container}> <Pressable onPressIn={logPressIn('plain')} onLongPress={() => log('plain: onLongPress')} onPressOut={() => log('plain: onPressOut')} onPress={() => log('plain: onPress')}> <View style={styles.box}> <Text>Plain Pressable</Text> </View> </Pressable> <GestureDetector gesture={doubleTap}> <View> <Pressable requireToFail={doubleTap} onPressIn={logPressIn('requireToFail')} onLongPress={() => log('requireToFail: onLongPress')} onPressOut={() => log('requireToFail: onPressOut')} onPress={() => log('requireToFail: onPress')}> <View style={styles.box}> <Text>Pressable with requireToFail</Text> </View> </Pressable> </View> </GestureDetector> <Text style={styles.log}>{lines.join('\n')}</Text> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', paddingTop: 40, gap: 24 }, box: { width: 300, height: 80, backgroundColor: '#b58df1', alignItems: 'center', justifyContent: 'center', }, log: { width: 300, fontSize: 14 }, }); ```
1 parent 653d519 commit f4038e9

1 file changed

Lines changed: 7 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ const StatefulPressable = (props: PressableProps) => {
125125
if (longPressTimeoutRef.current) {
126126
clearTimeout(longPressTimeoutRef.current);
127127
longPressTimeoutRef.current = null;
128-
isOnPressAllowed.current = true;
129128
}
130129
}, []);
131130

@@ -141,6 +140,7 @@ const StatefulPressable = (props: PressableProps) => {
141140
if (onLongPress) {
142141
cancelLongPress();
143142
longPressTimeoutRef.current = setTimeout(() => {
143+
longPressTimeoutRef.current = null;
144144
isOnPressAllowed.current = false;
145145
onLongPress(event);
146146
}, delayLongPress ?? DEFAULT_LONG_PRESS_DURATION);
@@ -188,6 +188,7 @@ const StatefulPressable = (props: PressableProps) => {
188188
}
189189

190190
isCurrentlyPressed.current = true;
191+
isOnPressAllowed.current = true;
191192
pressInEventRef.current = event;
192193
if (unstable_pressDelay) {
193194
pressDelayTimeoutRef.current = setTimeout(() => {
@@ -311,6 +312,11 @@ const StatefulPressable = (props: PressableProps) => {
311312
pressableEvent
312313
);
313314
},
315+
onTouchesUp: () => {
316+
// The button may still be waiting for another gesture; a long press
317+
// must not fire after the finger lifted
318+
cancelLongPress();
319+
},
314320
onTouchesCancel: (event) => {
315321
const pressableEvent = gestureTouchToPressableEvent(event);
316322
stateMachine.reset();

0 commit comments

Comments
 (0)