SIGN IN SIGN UP

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