Skip to content

Commit 35d0f8f

Browse files
christophpurrermeta-codesync[bot]
authored andcommitted
Consume Enter key-ups that would trigger native focus advance in TextInput (#59247)
Summary: Pull Request resolved: #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
1 parent 7b6516e commit 35d0f8f

2 files changed

Lines changed: 105 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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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+
package com.facebook.react.views.textinput
9+
10+
import android.graphics.Rect
11+
import android.os.SystemClock
12+
import android.util.DisplayMetrics
13+
import android.view.KeyEvent
14+
import android.view.View
15+
import android.widget.FrameLayout
16+
import androidx.core.content.res.ResourcesCompat.ID_NULL
17+
import com.facebook.react.bridge.JavaOnlyMap
18+
import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext
19+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
20+
import com.facebook.react.uimanager.DisplayMetricsHolder
21+
import com.facebook.react.uimanager.ReactStylesDiffMap
22+
import com.facebook.react.uimanager.ThemedReactContext
23+
import com.facebook.testutils.shadows.ShadowSoLoader
24+
import org.assertj.core.api.Assertions.assertThat
25+
import org.junit.Before
26+
import org.junit.Test
27+
import org.junit.runner.RunWith
28+
import org.robolectric.RobolectricTestRunner
29+
import org.robolectric.RuntimeEnvironment
30+
import org.robolectric.annotation.Config
31+
32+
@RunWith(RobolectricTestRunner::class)
33+
@Config(shadows = [ShadowSoLoader::class])
34+
class ReactEditTextKeyEventTest {
35+
36+
private lateinit var manager: ReactTextInputManager
37+
private lateinit var view: ReactEditText
38+
39+
@Before
40+
fun setup() {
41+
ReactNativeFeatureFlagsForTests.setUp()
42+
val context = createTestReactApplicationContext(RuntimeEnvironment.getApplication())
43+
val themedContext = ThemedReactContext(context, context.baseContext, null, ID_NULL)
44+
manager = ReactTextInputManager()
45+
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics())
46+
view = manager.createViewInstance(themedContext)
47+
48+
val unfocusableView =
49+
object : View(themedContext) {
50+
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean = false
51+
}
52+
val parent =
53+
object : FrameLayout(themedContext) {
54+
override fun focusSearch(focused: View?, direction: Int): View = unfocusableView
55+
}
56+
parent.addView(view)
57+
}
58+
59+
@Test
60+
fun testSingleLineNumpadEnterKeyUpIsConsumed() {
61+
manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", false)))
62+
63+
assertThat(view.onKeyUp(KeyEvent.KEYCODE_NUMPAD_ENTER, keyUp(KeyEvent.KEYCODE_NUMPAD_ENTER)))
64+
.isTrue
65+
}
66+
67+
@Test
68+
fun testMultilineEditorActionEnterKeyUpIsConsumed() {
69+
manager.updateProperties(view, ReactStylesDiffMap(JavaOnlyMap.of("multiline", true)))
70+
71+
for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) {
72+
assertThat(view.onKeyUp(keyCode, keyUp(keyCode, KeyEvent.FLAG_EDITOR_ACTION))).isTrue
73+
}
74+
}
75+
76+
@Test
77+
fun testMultilineEmailEnterKeyUpIsConsumed() {
78+
manager.updateProperties(
79+
view,
80+
ReactStylesDiffMap(JavaOnlyMap.of("multiline", true, "keyboardType", "email-address")),
81+
)
82+
83+
for (keyCode in listOf(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER)) {
84+
assertThat(view.onKeyUp(keyCode, keyUp(keyCode))).isTrue
85+
}
86+
}
87+
88+
private fun keyUp(keyCode: Int, flags: Int = 0): KeyEvent {
89+
val now = SystemClock.uptimeMillis()
90+
return KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0, 0, 0, flags)
91+
}
92+
}

0 commit comments

Comments
 (0)