Fix dead presses in `"never"` mode when the keyboard belongs to a native field (#4439)
## Description
With a Gesture Handler `ScrollView` in the default (`never`)
`keyboardShouldPersistTaps` mode, a keyboard opened by a native field
(e.g. a native-stack `headerSearchBarOptions` search bar) made every
RNGH `Pressable`/`Touchable` inside dead, with no way to dismiss the
keyboard by tapping. The keyboard-dismissing tap drop (#992) checks only
keyboard visibility, but the dismissal blurs
`TextInput.State.currentlyFocusedInput()`, which is `null` for native
fields - the tap was consumed while nothing could be dismissed.
Now the tap is dropped only when an RN `TextInput` is focused, mirroring
RN ScrollView's `_keyboardIsDismissible`. Focus is snapshotted when the
keyboard shows, since the dismissal blurs the input at touch-down,
before the press events are checked; a live check is OR-ed in for focus
moving to an RN input while the keyboard is already up. With a
native-field keyboard, presses now behave like RN's `Pressable`: they
fire and the keyboard stays.
## Test plan
- `yarn test` — added cases: no drop when the keyboard is up without a
focused RN input; the drop verdict survives the input being blurred
mid-tap; existing `never`-drop tests updated to mock a focused input.
- iOS simulator and Android emulator, repro below with RNGH and RN
`Pressable` side by side:
- search bar active: both fire, keyboard stays (previously the RNGH one
was dead)
- RN `TextInput` focused: both are dropped and the tap dismisses the
keyboard (#992 behavior unchanged)
<details>
<summary>Repro</summary>
```tsx
import React, { useState } from 'react';
import { Pressable as RNPressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { NavigationContainer, NavigationIndependentTree } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Pressable, ScrollView } from 'react-native-gesture-handler';
const ROUTES = ['12', '17', '25', '42', '58', '73', '81', '99'];
function RoutesScreen() {
const [pressCount, setPressCount] = useState(0);
const [lastPressed, setLastPressed] = useState<string | null>(null);
const press = (label: string) => () => {
setPressCount((c) => c + 1);
setLastPressed(label);
};
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<TextInput placeholder="RN TextInput (control)" style={styles.input} />
<Text style={styles.status}>
{`onPress count: ${pressCount}` + (lastPressed ? ` (last: ${lastPressed})` : '')}
</Text>
{ROUTES.map((route) => (
<View key={route} style={styles.routeRow}>
<Pressable style={styles.row} onPress={press(`GH ${route}`)}>
<Text>{`GH ${route}`}</Text>
</Pressable>
<RNPressable style={styles.row} onPress={press(`RN ${route}`)}>
<Text>{`RN ${route}`}</Text>
</RNPressable>
</View>
))}
</ScrollView>
);
}
const Stack = createNativeStackNavigator();
export default function Example() {
return (
<NavigationIndependentTree>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Routes"
component={RoutesScreen}
options={{
headerSearchBarOptions: { placeholder: 'Search routes', hideWhenScrolling: false },
}}
/>
</Stack.Navigator>
</NavigationContainer>
</NavigationIndependentTree>
);
}
const styles = StyleSheet.create({
status: { fontSize: 16, fontWeight: 'bold', padding: 16 },
input: { borderColor: '#ccd', borderRadius: 8, borderWidth: 1, margin: 16, padding: 12 },
routeRow: { flexDirection: 'row', gap: 8, marginHorizontal: 16, marginVertical: 4 },
row: { backgroundColor: '#eef', borderRadius: 8, flex: 1, padding: 16 },
});
```
</details> M
Michał Bert committed
33c4f28b77396fa24d5fed1ada31c299dc681597
Parent: d29545a
Committed by GitHub <noreply@github.com>
on 8/19/2026, 6:08:09 AM