Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading