Skip to content

Commit 591d660

Browse files
Consume Enter key-ups that would trigger native focus advance in TextInput
Summary: - 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
1 parent 7b6516e commit 591d660

2 files changed

Lines changed: 108 additions & 1 deletion

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
168168
internal val isMultiline: Boolean
169169
get() = (inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0
170170

171+
private val isEmailInput: Boolean
172+
get() =
173+
(inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT &&
174+
(inputType and InputType.TYPE_MASK_VARIATION).let {
175+
it == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
176+
it == InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT
177+
}
178+
171179
private val isSecureText: Boolean
172180
get() =
173181
((inputType and
@@ -342,10 +350,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
342350
// Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't
343351
// since we only allow JS to change focus, which in turn causes TextView to crash.
344352
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
345-
if (keyCode == KeyEvent.KEYCODE_ENTER && !isMultiline) {
353+
val isEnterKey = keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER
354+
if (isEnterKey && !isMultiline) {
346355
hideSoftKeyboard()
347356
return true
348357
}
358+
if (isEnterKey && ((event.flags and KeyEvent.FLAG_EDITOR_ACTION) != 0 || isEmailInput)) {
359+
return true
360+
}
349361
return super.onKeyUp(keyCode, event)
350362
}
351363

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
// TODO T207169925: Migrate CatalystInstance to Reacthost and remove the Suppress("DEPRECATION")
9+
// annotation
10+
@file:Suppress("DEPRECATION")
11+
12+
package com.facebook.react.views.textinput
13+
14+
import android.graphics.Rect
15+
import android.os.SystemClock
16+
import android.util.DisplayMetrics
17+
import android.view.KeyEvent
18+
import android.view.View
19+
import android.widget.FrameLayout
20+
import androidx.core.content.res.ResourcesCompat.ID_NULL
21+
import com.facebook.react.bridge.BridgeReactContext
22+
import com.facebook.react.bridge.JavaOnlyMap
23+
import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance
24+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
25+
import com.facebook.react.uimanager.DisplayMetricsHolder
26+
import com.facebook.react.uimanager.ReactStylesDiffMap
27+
import com.facebook.react.uimanager.ThemedReactContext
28+
import org.assertj.core.api.Assertions.assertThat
29+
import org.junit.Before
30+
import org.junit.Test
31+
import org.junit.runner.RunWith
32+
import org.robolectric.RobolectricTestRunner
33+
import org.robolectric.RuntimeEnvironment
34+
35+
@RunWith(RobolectricTestRunner::class)
36+
class ReactEditTextKeyEventTest {
37+
38+
private lateinit var manager: ReactTextInputManager
39+
private lateinit var view: ReactEditText
40+
41+
@Before
42+
fun setup() {
43+
ReactNativeFeatureFlagsForTests.setUp()
44+
val context = BridgeReactContext(RuntimeEnvironment.getApplication())
45+
context.initializeWithInstance(createMockCatalystInstance())
46+
val themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL)
47+
manager = ReactTextInputManager()
48+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
49+
view = manager.createViewInstance(themedContext)
50+
51+
val unfocusableView =
52+
object : View(themedContext) {
53+
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean = false
54+
}
55+
val parent =
56+
object : FrameLayout(themedContext) {
57+
override fun focusSearch(focused: View?, direction: Int): View = unfocusableView
58+
}
59+
parent.addView(view)
60+
}
61+
62+
@Test
63+
fun testSingleLineNumpadEnterKeyUpIsConsumed() {
64+
manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", false)))
65+
66+
assertThat(view.onKeyUp(KeyEvent.KEYCODE_NUMPAD_ENTER, keyUp(KeyEvent.KEYCODE_NUMPAD_ENTER)))
67+
.isTrue
68+
}
69+
70+
@Test
71+
fun testMultilineEditorActionEnterKeyUpIsConsumed() {
72+
manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", true)))
73+
74+
for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) {
75+
assertThat(view.onKeyUp(keyCode, keyUp(keyCode, KeyEvent.FLAG_EDITOR_ACTION))).isTrue
76+
}
77+
}
78+
79+
@Test
80+
fun testMultilineEmailEnterKeyUpIsConsumed() {
81+
manager.updateProperties(
82+
view,
83+
ReactStylesDiffMap(JavaOnlyMap.of("multiline", true, "keyboardType", "email-address")),
84+
)
85+
86+
for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) {
87+
assertThat(view.onKeyUp(keyCode, keyUp(keyCode))).isTrue
88+
}
89+
}
90+
91+
private fun keyUp(keyCode: Int, flags: Int = 0): KeyEvent {
92+
val now = SystemClock.uptimeMillis()
93+
return KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0, flags)
94+
}
95+
}

0 commit comments

Comments
 (0)