diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index 2830fc92a9f0..fa12ac50def3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -931,7 +931,11 @@ public open class ReactTextInputManager public constructor() : } } - editText.setOnEditorActionListener { _: TextView?, actionId: Int, _: KeyEvent? -> + editText.setOnEditorActionListener { _: TextView?, actionId: Int, keyEvent: KeyEvent? -> + // TextView calls this again on key up for a hardware Enter it already handled on key down + if (keyEvent?.action == KeyEvent.ACTION_UP) { + return@setOnEditorActionListener true + } if ((actionId and EditorInfo.IME_MASK_ACTION) != 0 || actionId == EditorInfo.IME_NULL) { val isMultiline = editText.isMultiline diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingTest.kt new file mode 100644 index 000000000000..823826135c56 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputSubmitEditingTest.kt @@ -0,0 +1,84 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package com.facebook.react.views.textinput + +import android.util.DisplayMetrics +import android.view.KeyEvent +import com.facebook.react.bridge.BridgeReactContext +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance +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.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.EventDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class ReactTextInputSubmitEditingTest { + + private lateinit var themedContext: ThemedReactContext + private lateinit var eventDispatcher: EventDispatcher + private lateinit var uiManagerHelperMock: MockedStatic + + @Before + fun setup() { + ReactNativeFeatureFlagsForTests.setUp() + val context = BridgeReactContext(RuntimeEnvironment.getApplication()) + context.initializeWithInstance(createMockCatalystInstance()) + themedContext = ThemedReactContext(context, context, null, -1) + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics()) + eventDispatcher = mock() + uiManagerHelperMock = mockStatic(UIManagerHelper::class.java) + uiManagerHelperMock + .`when` { UIManagerHelper.getEventDispatcher(any()) } + .thenReturn(eventDispatcher) + uiManagerHelperMock + .`when` { UIManagerHelper.getReactContext(any()) } + .thenReturn(context) + } + + @After + fun teardown() { + uiManagerHelperMock.close() + DisplayMetricsHolder.setScreenDisplayMetrics(null) + } + + @Test + fun testHardwareEnterSubmitsMultilineOnce() { + val view = + ReactTextInputManager() + .createView( + 1, + themedContext, + ReactStylesDiffMap(JavaOnlyMap.of("multiline", true, "submitBehavior", "submit")), + null, + mock(), + ) + + view.onKeyDown(KeyEvent.KEYCODE_ENTER, KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)) + view.onKeyUp(KeyEvent.KEYCODE_ENTER, KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)) + + verify(eventDispatcher, times(1)).dispatchEvent(any()) + } +}