Skip to content

fix: register EnrichedTextInput in TextInputState so taps outside blur it - #821

Open
henrikra wants to merge 1 commit into
software-mansion:mainfrom
henrikra:fix/register-input-in-text-input-state
Open

henrikra wants to merge 1 commit into
software-mansion:mainfrom
henrikra:fix/register-input-in-text-input-state

Conversation

@henrikra

Copy link
Copy Markdown
Contributor

Summary

Fixes #184.

When an EnrichedTextInput is focused inside a ScrollView/FlatList, tapping outside it leaves the input focused and the keyboard up, and Keyboard.dismiss() does nothing. The built-in TextInput blurs in both cases.

React Native's tap-to-dismiss is implemented in JS: ScrollView's responder handlers check TextInputState, a JS registry of text input host instances, to decide whether a tap should dismiss the keyboard. Keyboard.dismiss() also goes through it. An input that never registers is invisible to that logic, so ScrollView concludes no text input is focused and does nothing.

This PR mirrors what TextInput does, in the same way react-native-enriched-markdown #597 already does for EnrichedMarkdownTextInput:

  • register on mount, and unregister on unmount (blurring first if still focused)
  • focusInput / blurInput from the native focus and blur events, so every focus source is tracked (tap, ref.focus(), autoFocus)

The registry functions aren't part of the public TextInput.State, so this uses the same small utils/textInputState.ts wrapper as enriched-markdown: a raw require of the internal module that handles both of its export shapes. Other libraries with custom native inputs do the same (see Prior art below). Registering a non-TextInput host is safe because Fabric command dispatch is name-based, and EnrichedTextInput already implements focus and blur commands on iOS and Android.

It also removes the "No global blur support on mobile" section from known limitations (added in #785), since the limitation no longer applies, and notes the behavior under onBlur in the API reference.

Only the native component changes; web is untouched.

Context

This picks up @exploIF's draft #192, which paused this approach to avoid the deprecated deep import while asking RN to expose TextInputState. Since then, RN has declined to make it public (react/react-native#50321: "TextInputState should remain a private API"), and the internal module still resolves at runtime on RN main. Meanwhile, the libraries below have shipped the same registration. If you'd rather continue in #192, I'm happy to close this one.

Prior art

Libraries with custom native text inputs register them in TextInputState the same way:

  • Software Mansion react-native-enriched-markdown: the same fix for EnrichedMarkdownTextInput, PR #597 (issue #577). See the textInputState.ts wrapper, which this PR copies.
  • Expo expo-ui: SwiftUI/Compose text fields join keyboard coordination through TextInputState, expo/expo#48788 (source).
  • stripe-react-native: CardField / CardForm call registerInput, focusInput and blurInput (source).
  • Mattermost react-native-paste-input: registers and unregisters its native input (source).

Test Plan

  1. In the example app, put a focused EnrichedTextInput inside a ScrollView with keyboardShouldPersistTaps="handled", with the soft keyboard open (in the iOS Simulator, turn off Connect Hardware Keyboard).
  2. Tap a non-touchable area of the scroll view: the input blurs, onBlur fires and the keyboard is dismissed.
  3. Tap a button in the scroll view: it fires without dismissing ('handled' semantics).
  4. Tap inside the focused input: the caret moves and the keyboard stays up.
  5. Remove keyboardShouldPersistTaps (default 'never'): the first tap outside only dismisses, the next tap presses (stock TextInput behavior).
  6. Call Keyboard.dismiss() while the input is focused: it blurs.
  7. Unmount the screen while the input is focused: the keyboard goes away, and the next ScrollView tap isn't swallowed.

With a hardware keyboard there's no soft keyboard to dismiss, so outside taps leave the input focused, matching TextInput.

Verified the same change on iOS (Simulator, RN 0.86, library 1.1.1) in a production app: steps 2 and 4 behave as above. I haven't run it on Android or run the Maestro suite. yarn typecheck:lib and eslint pass.

Screenshots / Videos

N/A. The change is a behavior fix with no UI.

Compatibility

OS Implemented
iOS ✅
Android ✅
Web ❌

Web is not affected (the browser handles focus and blur natively).

Checklist

  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

🤖 Generated with Claude Code

…r it (software-mansion#184)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 13:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The private registry integration lacks dismissal regression coverage and Android runtime validation.

1 open issue
What changed in this PR

Integrates native EnrichedTextInput with React Native’s focus registry to address #184 without changing web behavior.

Changes:

  • Register inputs and synchronize focus, blur, and unmount cleanup.
  • Add a compatibility wrapper for the internal TextInputState module.
  • Document keyboard dismissal support and remove the previous limitation.
File Description
src/​utils/​textInputState.ts Wraps internal registry exports.
src/​native/​EnrichedTextInput.tsx Adds registry lifecycle and focus tracking.
docs/​docs/​misc/​known-limitations.md Removes the global blur limitation.
docs/​docs/​api-reference/​enriched-text-input.md Documents native dismissal behavior.

Review effort: Balanced

Comment on lines +160 to +164
useLayoutEffect(() => {
const instance = nativeRef.current;
if (instance == null) return;

TextInputState.registerInput(instance);

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] onBlur not called as expected

2 participants