From 835ce9ccc17fedf8667386f022465e1a8c5dcb44 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Thu, 22 Oct 2026 08:41:01 -0700 Subject: [PATCH] Consume Enter key-ups that would trigger native focus advance in TextInput (#59247) Summary: Pull Request resolved: https://github.com/react/react-native/pull/59247 - Problem: An Android TextInput crashes with `IllegalStateException: focus search returned a view that wasn't able to take focus!` from `ReactEditText.onKeyUp` when a numpad Enter, an editor-action Enter, or an Enter on a multiline email input is released. - Fix: Guard `ReactEditText.onKeyUp` so every Enter key-up that `TextView` would turn into native focus advance is consumed, so that focus moves only from JS and the input no longer crashes. Changelog: [Android][Fixed] - Fix TextInput crash when an Enter key-up triggers native focus search Differential Revision: D126482002 --- .../react/views/textinput/ReactEditText.kt | 16 +++- .../textinput/ReactEditTextKeyEventTest.kt | 92 +++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyEventTest.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 754f10026624..24ce08a81284 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -168,6 +168,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat internal val isMultiline: Boolean get() = (inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0 + // Mirrors TextView.shouldAdvanceFocusOnEnter, which advances focus on Enter for both email + // address and subject even when multiline (newlines don't make sense there). + private val isEmailInput: Boolean + get() = + (inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT && + (inputType and InputType.TYPE_MASK_VARIATION).let { + it == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || + it == InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT + } + private val isSecureText: Boolean get() = ((inputType and @@ -342,10 +352,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't // since we only allow JS to change focus, which in turn causes TextView to crash. override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (keyCode == KeyEvent.KEYCODE_ENTER && !isMultiline) { + val isEnterKey = keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER + if (isEnterKey && !isMultiline) { hideSoftKeyboard() return true } + if (isEnterKey && ((event.flags and KeyEvent.FLAG_EDITOR_ACTION) != 0 || isEmailInput)) { + return true + } return super.onKeyUp(keyCode, event) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyEventTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyEventTest.kt new file mode 100644 index 000000000000..5f7458d1f73a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyEventTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.textinput + +import android.graphics.Rect +import android.os.SystemClock +import android.util.DisplayMetrics +import android.view.KeyEvent +import android.view.View +import android.widget.FrameLayout +import androidx.core.content.res.ResourcesCompat.ID_NULL +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.ReactStylesDiffMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.testutils.shadows.ShadowSoLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowSoLoader::class]) +class ReactEditTextKeyEventTest { + + private lateinit var manager: ReactTextInputManager + private lateinit var view: ReactEditText + + @Before + fun setup() { + ReactNativeFeatureFlagsForTests.setUp() + val context = createTestReactApplicationContext(RuntimeEnvironment.getApplication()) + val themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL) + manager = ReactTextInputManager() + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics()) + view = manager.createViewInstance(themedContext) + + val unfocusableView = + object : View(themedContext) { + override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean = false + } + val parent = + object : FrameLayout(themedContext) { + override fun focusSearch(focused: View?, direction: Int): View = unfocusableView + } + parent.addView(view) + } + + @Test + fun testSingleLineNumpadEnterKeyUpIsConsumed() { + manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", false))) + + assertThat(view.onKeyUp(KeyEvent.KEYCODE_NUMPAD_ENTER, keyUp(KeyEvent.KEYCODE_NUMPAD_ENTER))) + .isTrue + } + + @Test + fun testMultilineEditorActionEnterKeyUpIsConsumed() { + manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", true))) + + for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) { + assertThat(view.onKeyUp(keyCode, keyUp(keyCode, KeyEvent.FLAG_EDITOR_ACTION))).isTrue + } + } + + @Test + fun testMultilineEmailEnterKeyUpIsConsumed() { + manager.updateProperties( + view, + ReactStylesDiffMap(JavaOnlyMap.of("multiline", true, "keyboardType", "email-address")), + ) + + for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) { + assertThat(view.onKeyUp(keyCode, keyUp(keyCode))).isTrue + } + } + + private fun keyUp(keyCode: Int, flags: Int = 0): KeyEvent { + val now = SystemClock.uptimeMillis() + return KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0, flags) + } +}