From f3571209191872262dc38d0d2dcff1e94c5edd99 Mon Sep 17 00:00:00 2001 From: Andrew Sidorchuk Date: Thu, 16 Jul 2026 12:35:05 -0400 Subject: [PATCH 1/3] Blur focused TextInput before unregistering it on unmount Since #54085, ReactNativeElement.blur() routes through TextInputState.isTextInput(), which checks the registered-inputs set. TextInput's unmount cleanup unregistered the input before calling blur(), so the blur silently became a no-op: the native blur command was never dispatched, hideSoftKeyboard() never ran, and on Android the soft keyboard stayed open after unmounting a focused input (e.g. when popping a native-stack screen), with focus escaping to any remaining focusable view such as a WebView. Reordering the cleanup so blur() runs while the input is still registered restores the pre-0.83 behavior. This mirrors the fix that #54085 itself applied on the focus() side, where registerInput() was moved earlier so focus() could be called from ref callbacks. Changelog: [GENERAL] [FIXED] - TextInput: blur focused input before unregistering it on unmount, fixing the Android soft keyboard staying open after navigating away from a focused input --- .../Components/TextInput/TextInput.js | 9 +++++-- .../TextInput/__tests__/TextInput-itest.js | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 5203cf36e87..46fee995b09 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -302,11 +302,16 @@ function InternalTextInput(props: TextInputProps): React.Node { TextInputState.registerInput(inputRefValue); return () => { - TextInputState.unregisterInput(inputRefValue); - + // Blur the input before unregistering it. `blur()` routes through + // `TextInputState.isTextInput()`, which checks the registered-inputs + // set — if the input is unregistered first, the blur is silently + // skipped, the soft keyboard is never hidden, and (on Android) focus + // escapes to whichever focusable view remains in the window. if (TextInputState.currentlyFocusedInput() === inputRefValue) { nullthrows(inputRefValue).blur(); } + + TextInputState.unregisterInput(inputRefValue); }; } }, []); diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js index d7f1636c2cd..1fd3bfe4280 100644 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/TextInput-itest.js @@ -597,6 +597,33 @@ describe('', () => { 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "blur"}', ]); }); + + it('dispatches the blur command when a focused input is unmounted', () => { + const root = Fantom.createRoot(); + const ref = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + const instance = nullthrows(ref.current); + + Fantom.runTask(() => { + instance.focus(); + }); + + root.takeMountingManagerLogs(); + + Fantom.runTask(() => { + // unmount TextInput + root.render(<>); + }); + + expect(root.takeMountingManagerLogs()).toContain( + 'Command {type: "AndroidTextInput", nativeID: "text-input", name: "blur"}', + ); + expect(TextInput.State.currentlyFocusedInput()).toBe(null); + }); }); describe('clear()', () => { From f5e946e192fa4f3f0ae4dcdcdf0170351c27def5 Mon Sep 17 00:00:00 2001 From: Andrew Sidorchuk Date: Thu, 16 Jul 2026 12:41:26 -0400 Subject: [PATCH 2/3] Trim explanatory comment --- .../Libraries/Components/TextInput/TextInput.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 46fee995b09..0a4c4725ff8 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -302,11 +302,8 @@ function InternalTextInput(props: TextInputProps): React.Node { TextInputState.registerInput(inputRefValue); return () => { - // Blur the input before unregistering it. `blur()` routes through - // `TextInputState.isTextInput()`, which checks the registered-inputs - // set — if the input is unregistered first, the blur is silently - // skipped, the soft keyboard is never hidden, and (on Android) focus - // escapes to whichever focusable view remains in the window. + // Blur the input while it is still registered, otherwise `blur()` + // does not dispatch the blur command. if (TextInputState.currentlyFocusedInput() === inputRefValue) { nullthrows(inputRefValue).blur(); } From c59133dab0d88630d08148ec7e43bac78f0749ad Mon Sep 17 00:00:00 2001 From: Andrew Sidorchuk Date: Thu, 16 Jul 2026 12:45:08 -0400 Subject: [PATCH 3/3] Remove comment --- .../react-native/Libraries/Components/TextInput/TextInput.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 0a4c4725ff8..ee48b398c12 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -302,8 +302,6 @@ function InternalTextInput(props: TextInputProps): React.Node { TextInputState.registerInput(inputRefValue); return () => { - // Blur the input while it is still registered, otherwise `blur()` - // does not dispatch the blur command. if (TextInputState.currentlyFocusedInput() === inputRefValue) { nullthrows(inputRefValue).blur(); }