From 95f2f6168aa6007f9641abbf4bacce9759dc03e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 17 Aug 2026 11:22:01 +0200 Subject: [PATCH 1/2] Fix Touchable not responding to presses --- .../src/__tests__/api_v3.test.tsx | 42 ++++++++++++++++++- .../src/v3/scrollViewInterop.ts | 18 +++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx index ccfd163fdb..582d940fb3 100644 --- a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx +++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx @@ -5,7 +5,7 @@ import { screen, } from '@testing-library/react-native'; import { act } from 'react'; -import { Keyboard, View } from 'react-native'; +import { Keyboard, TextInput, View } from 'react-native'; import GestureHandlerRootView from '../components/GestureHandlerRootView'; import { fireGestureHandler, getByGestureTestId } from '../jestUtils'; @@ -454,8 +454,17 @@ describe('[API v3] Components', () => { keyboardShouldPersistTaps, }); + // The drop requires a focused RN TextInput to blur. + const focusInput = () => + jest + .spyOn(TextInput.State, 'currentlyFocusedInput') + .mockReturnValue( + {} as ReturnType + ); + test('isKeyboardDismissingTap is true only in never mode while the keyboard is visible', async () => { const addListenerSpy = jest.spyOn(Keyboard, 'addListener'); + const focusSpy = focusInput(); render( @@ -477,6 +486,31 @@ describe('[API v3] Components', () => { // Outside an RNGH ScrollView there is no context, so nothing is dropped. expect(isKeyboardDismissingTap(null)).toBe(false); + // The verdict must survive the dismissal blurring the input mid-tap. + focusSpy.mockReturnValue(undefined); + expect(isKeyboardDismissingTap(makeContext('never'))).toBe(true); + + addListenerSpy.mockRestore(); + focusSpy.mockRestore(); + }); + + test('isKeyboardDismissingTap is false when no RN TextInput is focused (native field keyboard)', async () => { + const addListenerSpy = jest.spyOn(Keyboard, 'addListener'); + + render( + + + + ); + await act(flushImmediate); + + // Keyboard up for a native field (e.g. a native-stack search bar) - + // no RN TextInput to blur, so the tap must not be dropped. + showKeyboard(addListenerSpy); + + expect(TextInput.State.currentlyFocusedInput()).toBeNull(); + expect(isKeyboardDismissingTap(makeContext('never'))).toBe(false); + addListenerSpy.mockRestore(); }); @@ -500,6 +534,7 @@ describe('[API v3] Components', () => { test('Touchable does NOT fire any press callback on the keyboard-dismissing tap (never)', async () => { const addListenerSpy = jest.spyOn(Keyboard, 'addListener'); + const focusSpy = focusInput(); const onPress = jest.fn(); const onPressIn = jest.fn(); const onPressOut = jest.fn(); @@ -519,6 +554,10 @@ describe('[API v3] Components', () => { await act(flushImmediate); showKeyboard(addListenerSpy); + // The 'never' responder blurs the input at touch-down, before the + // press events arrive - mirror that ordering. + focusSpy.mockReturnValue(undefined); + // Includes a re-entry PressIn (finger dragged out and back in) so the // capture-once verdict path is exercised too. const button = screen.getByTestId('touchable'); @@ -535,6 +574,7 @@ describe('[API v3] Components', () => { expect(onPressIn).not.toHaveBeenCalled(); expect(onPressOut).not.toHaveBeenCalled(); addListenerSpy.mockRestore(); + focusSpy.mockRestore(); }); test('Touchable fires onPress in never mode when the keyboard is not visible', async () => { diff --git a/packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts b/packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts index 42a1d7f7aa..ee469e5621 100644 --- a/packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts +++ b/packages/react-native-gesture-handler/src/v3/scrollViewInterop.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { TextInput } from 'react-native'; export type KeyboardShouldPersistTaps = | boolean @@ -27,9 +28,15 @@ export function updateResponderEventValue( } let isKeyboardVisible = false; +let keyboardOpenedForRNInput = false; export function setKeyboardVisibility(visible: boolean) { isKeyboardVisible = visible; + + // Snapshotted at show-time: the dismissal blurs the input at touch-down, + // before the press events get checked + keyboardOpenedForRNInput = + visible && TextInput.State.currentlyFocusedInput?.() != null; } export function isKeyboardDismissingTap( @@ -42,5 +49,14 @@ export function isKeyboardDismissingTap( const mode = jsResponderContext.keyboardShouldPersistTaps; const keyboardNeverPersistTaps = !mode || mode === 'never'; - return keyboardNeverPersistTaps && isKeyboardVisible; + // Drop only taps that can dismiss the keyboard, i.e. an RN TextInput is (or + // was at show-time) focused - mirrors RN ScrollView's `_keyboardIsDismissible`. + // A native field's keyboard (e.g. a native-stack search bar) can't be + // blurred, so dropping there would leave presses permanently dead + return ( + keyboardNeverPersistTaps && + isKeyboardVisible && + (keyboardOpenedForRNInput || + TextInput.State.currentlyFocusedInput?.() != null) + ); } From 3d9b2a7c24973b4909f1ea6423f32543bdce3816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82?= Date: Mon, 17 Aug 2026 15:06:16 +0200 Subject: [PATCH 2/2] test --- .../src/__tests__/api_v3.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx index 582d940fb3..31ce7493a1 100644 --- a/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx +++ b/packages/react-native-gesture-handler/src/__tests__/api_v3.test.tsx @@ -511,7 +511,13 @@ describe('[API v3] Components', () => { expect(TextInput.State.currentlyFocusedInput()).toBeNull(); expect(isKeyboardDismissingTap(makeContext('never'))).toBe(false); + // Focus moving to an RN input while the keyboard stays up makes the + // tap dismissible again. + const focusSpy = focusInput(); + expect(isKeyboardDismissingTap(makeContext('never'))).toBe(true); + addListenerSpy.mockRestore(); + focusSpy.mockRestore(); }); test('isKeyboardDismissingTap is false for a detached (height 0) keyboard', async () => {