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) + } +}