diff --git a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index fd8fd9919d6d..eb542d05423a 100644 --- a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -437,9 +437,22 @@ export type AndroidTextInputNativeProps = Readonly<{ * This will be called with `{ nativeEvent: { key: keyValue } }` * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and * the typed-in character otherwise including `' '` for space. + * On Android, keys pressed on a hardware keyboard are reported as well, each + * by its `KeyboardEvent.key` value (such as `'ArrowLeft'`, `'Shift'` or + * `'F1'`), with `altKey`, `ctrlKey`, `metaKey` and `shiftKey` set for the + * modifier keys held. * Fires before `onChange` callbacks. */ - onKeyPress?: ?BubblingEventHandler>, + onKeyPress?: ?BubblingEventHandler< + Readonly<{ + target: Int32, + key: string, + altKey: boolean, + ctrlKey: boolean, + metaKey: boolean, + shiftKey: boolean, + }>, + >, /** * Invoked on content scroll with `{ nativeEvent: { contentOffset: { x, y } } }`. diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 91bc7901343d..fb3cbee53923 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -101,6 +101,10 @@ type TextInputKeyPressEventData = Readonly<{ key: string, target?: ?number, eventCount: number, + altKey?: boolean, + ctrlKey?: boolean, + metaKey?: boolean, + shiftKey?: boolean, ... }>; @@ -850,6 +854,10 @@ type TextInputBaseProps = Readonly<{ * This will be called with `{ nativeEvent: { key: keyValue } }` * where `keyValue` is `'Enter'` or `'Backspace'` for respective keys and * the typed-in character otherwise including `' '` for space. + * On Android, keys pressed on a hardware keyboard are reported as well, each + * by its `KeyboardEvent.key` value (such as `'ArrowLeft'`, `'Shift'` or + * `'F1'`), with `altKey`, `ctrlKey`, `metaKey` and `shiftKey` set for the + * modifier keys held. * Fires before `onChange` callbacks. */ onKeyPress?: ?(e: TextInputKeyPressEvent) => unknown, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyEvent.kt index 802221e40939..ac25c8870314 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyEvent.kt @@ -19,7 +19,7 @@ internal abstract class KeyEvent( // Extract all needed data from keyEvent immediately to avoid storing the AndroidKeyEvent itself private val keyCode: Int = keyEvent.keyCode - private val unicodeChar: Int = keyEvent.unicodeChar + private val key: String = KeyValue.of(keyEvent) private val isAltPressed: Boolean = keyEvent.isAltPressed private val isCtrlPressed: Boolean = keyEvent.isCtrlPressed private val isMetaPressed: Boolean = keyEvent.isMetaPressed @@ -35,7 +35,7 @@ internal abstract class KeyEvent( eventData.putInt("target", viewTag) // W3C KeyboardEvent properties - eventData.putString("key", getKeyString()) + eventData.putString("key", key) eventData.putString("code", getCodeString()) // Modifier keys @@ -50,13 +50,6 @@ internal abstract class KeyEvent( return eventData } - private fun getKeyString(): String { - return when { - unicodeChar != 0 && !Character.isISOControl(unicodeChar) -> unicodeChar.toChar().toString() - else -> KEY_NAME_MAP[keyCode] ?: UNIDENTIFIED - } - } - private fun getCodeString(): String { return CODE_MAP[keyCode] ?: UNIDENTIFIED } @@ -128,29 +121,5 @@ internal abstract class KeyEvent( AndroidKeyEvent.KEYCODE_DPAD_CENTER to "Enter", ) } - - private val KEY_NAME_MAP: Map by - lazy(LazyThreadSafetyMode.PUBLICATION) { - mapOf( - AndroidKeyEvent.KEYCODE_ENTER to "Enter", - AndroidKeyEvent.KEYCODE_DPAD_CENTER to "Enter", - AndroidKeyEvent.KEYCODE_SPACE to " ", - AndroidKeyEvent.KEYCODE_TAB to "Tab", - AndroidKeyEvent.KEYCODE_DEL to "Backspace", - AndroidKeyEvent.KEYCODE_ESCAPE to "Escape", - AndroidKeyEvent.KEYCODE_SHIFT_LEFT to "Shift", - AndroidKeyEvent.KEYCODE_SHIFT_RIGHT to "Shift", - AndroidKeyEvent.KEYCODE_CTRL_LEFT to "Control", - AndroidKeyEvent.KEYCODE_CTRL_RIGHT to "Control", - AndroidKeyEvent.KEYCODE_ALT_LEFT to "Alt", - AndroidKeyEvent.KEYCODE_ALT_RIGHT to "Alt", - AndroidKeyEvent.KEYCODE_META_LEFT to "Meta", - AndroidKeyEvent.KEYCODE_META_RIGHT to "Meta", - AndroidKeyEvent.KEYCODE_DPAD_UP to "ArrowUp", - AndroidKeyEvent.KEYCODE_DPAD_DOWN to "ArrowDown", - AndroidKeyEvent.KEYCODE_DPAD_LEFT to "ArrowLeft", - AndroidKeyEvent.KEYCODE_DPAD_RIGHT to "ArrowRight", - ) - } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyValue.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyValue.kt new file mode 100644 index 000000000000..636c39011d28 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyValue.kt @@ -0,0 +1,215 @@ +/* + * 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.uimanager.events + +import android.view.KeyCharacterMap +import android.view.KeyEvent as AndroidKeyEvent + +/** + * The [UI Events key value](https://www.w3.org/TR/uievents-key/) of an Android key event, with key + * codes paired to key values as Firefox for Android pairs them + * ([NativeKeyToDOMKeyName.h](https://searchfox.org/mozilla-central/source/widget/NativeKeyToDOMKeyName.h)). + */ +internal object KeyValue { + + fun of(event: AndroidKeyEvent): String = of(event.keyCode, typedCharacter(event)) + + /** The key value of the key [keyCode] when it types [character], as [typedCharacter] reads it. */ + fun of(keyCode: Int, character: Int): String = + when { + isDeadKey(character) -> DEAD + character != 0 && !Character.isISOControl(character) -> String(Character.toChars(character)) + else -> NAMED_KEYS[keyCode] ?: UNIDENTIFIED + } + + /** + * The character [event] types with Control and Meta released, and with Alt released too when Alt + * types nothing on the layout, because the key value of a shortcut such as Ctrl+Z or Alt+E is the + * character of its key. + */ + fun typedCharacter(event: AndroidKeyEvent): Int { + val character = event.getUnicodeChar(event.metaState and SHORTCUT_MODIFIERS.inv()) + return if (character != 0 || !event.isAltPressed) { + character + } else { + event.getUnicodeChar(event.metaState and (SHORTCUT_MODIFIERS or ALT_MODIFIERS).inv()) + } + } + + fun isDeadKey(character: Int): Boolean = (character and KeyCharacterMap.COMBINING_ACCENT) != 0 + + private const val SHORTCUT_MODIFIERS: Int = + AndroidKeyEvent.META_CTRL_MASK or AndroidKeyEvent.META_META_MASK + private const val ALT_MODIFIERS: Int = AndroidKeyEvent.META_ALT_MASK + private const val DEAD: String = "Dead" + private const val UNIDENTIFIED: String = "Unidentified" + + private val NAMED_KEYS: Map by + lazy(LazyThreadSafetyMode.PUBLICATION) { + mapOf( + AndroidKeyEvent.KEYCODE_ALT_LEFT to "Alt", + AndroidKeyEvent.KEYCODE_ALT_RIGHT to "Alt", + AndroidKeyEvent.KEYCODE_CAPS_LOCK to "CapsLock", + AndroidKeyEvent.KEYCODE_CTRL_LEFT to "Control", + AndroidKeyEvent.KEYCODE_CTRL_RIGHT to "Control", + AndroidKeyEvent.KEYCODE_FUNCTION to "Fn", + AndroidKeyEvent.KEYCODE_META_LEFT to "Meta", + AndroidKeyEvent.KEYCODE_META_RIGHT to "Meta", + AndroidKeyEvent.KEYCODE_NUM_LOCK to "NumLock", + AndroidKeyEvent.KEYCODE_SCROLL_LOCK to "ScrollLock", + AndroidKeyEvent.KEYCODE_SHIFT_LEFT to "Shift", + AndroidKeyEvent.KEYCODE_SHIFT_RIGHT to "Shift", + AndroidKeyEvent.KEYCODE_SYM to "Symbol", + AndroidKeyEvent.KEYCODE_DPAD_CENTER to "Enter", + AndroidKeyEvent.KEYCODE_ENTER to "Enter", + AndroidKeyEvent.KEYCODE_NUMPAD_ENTER to "Enter", + AndroidKeyEvent.KEYCODE_TAB to "Tab", + AndroidKeyEvent.KEYCODE_DPAD_DOWN to "ArrowDown", + AndroidKeyEvent.KEYCODE_DPAD_LEFT to "ArrowLeft", + AndroidKeyEvent.KEYCODE_DPAD_RIGHT to "ArrowRight", + AndroidKeyEvent.KEYCODE_DPAD_UP to "ArrowUp", + AndroidKeyEvent.KEYCODE_MOVE_END to "End", + AndroidKeyEvent.KEYCODE_MOVE_HOME to "Home", + AndroidKeyEvent.KEYCODE_PAGE_DOWN to "PageDown", + AndroidKeyEvent.KEYCODE_PAGE_UP to "PageUp", + AndroidKeyEvent.KEYCODE_DEL to "Backspace", + AndroidKeyEvent.KEYCODE_CLEAR to "Clear", + AndroidKeyEvent.KEYCODE_COPY to "Copy", + AndroidKeyEvent.KEYCODE_CUT to "Cut", + AndroidKeyEvent.KEYCODE_FORWARD_DEL to "Delete", + AndroidKeyEvent.KEYCODE_INSERT to "Insert", + AndroidKeyEvent.KEYCODE_PASTE to "Paste", + AndroidKeyEvent.KEYCODE_MENU to "ContextMenu", + AndroidKeyEvent.KEYCODE_ESCAPE to "Escape", + AndroidKeyEvent.KEYCODE_HELP to "Help", + AndroidKeyEvent.KEYCODE_BREAK to "Pause", + AndroidKeyEvent.KEYCODE_ZOOM_IN to "ZoomIn", + AndroidKeyEvent.KEYCODE_ZOOM_OUT to "ZoomOut", + AndroidKeyEvent.KEYCODE_BRIGHTNESS_DOWN to "BrightnessDown", + AndroidKeyEvent.KEYCODE_BRIGHTNESS_UP to "BrightnessUp", + AndroidKeyEvent.KEYCODE_MEDIA_EJECT to "Eject", + AndroidKeyEvent.KEYCODE_POWER to "Power", + AndroidKeyEvent.KEYCODE_SYSRQ to "PrintScreen", + AndroidKeyEvent.KEYCODE_SLEEP to "Standby", + AndroidKeyEvent.KEYCODE_WAKEUP to "WakeUp", + AndroidKeyEvent.KEYCODE_HENKAN to "Convert", + AndroidKeyEvent.KEYCODE_LANGUAGE_SWITCH to "GroupNext", + AndroidKeyEvent.KEYCODE_SWITCH_CHARSET to "ModeChange", + AndroidKeyEvent.KEYCODE_MUHENKAN to "NonConvert", + AndroidKeyEvent.KEYCODE_EISU to "Eisu", + AndroidKeyEvent.KEYCODE_KATAKANA_HIRAGANA to "HiraganaKatakana", + AndroidKeyEvent.KEYCODE_KANA to "KanjiMode", + AndroidKeyEvent.KEYCODE_ZENKAKU_HANKAKU to "ZenkakuHankaku", + AndroidKeyEvent.KEYCODE_F1 to "F1", + AndroidKeyEvent.KEYCODE_F2 to "F2", + AndroidKeyEvent.KEYCODE_F3 to "F3", + AndroidKeyEvent.KEYCODE_F4 to "F4", + AndroidKeyEvent.KEYCODE_F5 to "F5", + AndroidKeyEvent.KEYCODE_F6 to "F6", + AndroidKeyEvent.KEYCODE_F7 to "F7", + AndroidKeyEvent.KEYCODE_F8 to "F8", + AndroidKeyEvent.KEYCODE_F9 to "F9", + AndroidKeyEvent.KEYCODE_F10 to "F10", + AndroidKeyEvent.KEYCODE_F11 to "F11", + AndroidKeyEvent.KEYCODE_F12 to "F12", + AndroidKeyEvent.KEYCODE_CHANNEL_DOWN to "ChannelDown", + AndroidKeyEvent.KEYCODE_CHANNEL_UP to "ChannelUp", + AndroidKeyEvent.KEYCODE_MEDIA_FAST_FORWARD to "MediaFastForward", + AndroidKeyEvent.KEYCODE_MEDIA_PAUSE to "MediaPause", + AndroidKeyEvent.KEYCODE_MEDIA_PLAY to "MediaPlay", + AndroidKeyEvent.KEYCODE_MEDIA_PLAY_PAUSE to "MediaPlayPause", + AndroidKeyEvent.KEYCODE_MEDIA_RECORD to "MediaRecord", + AndroidKeyEvent.KEYCODE_MEDIA_REWIND to "MediaRewind", + AndroidKeyEvent.KEYCODE_MEDIA_STOP to "MediaStop", + AndroidKeyEvent.KEYCODE_MEDIA_NEXT to "MediaTrackNext", + AndroidKeyEvent.KEYCODE_MEDIA_PREVIOUS to "MediaTrackPrevious", + AndroidKeyEvent.KEYCODE_VOLUME_DOWN to "AudioVolumeDown", + AndroidKeyEvent.KEYCODE_VOLUME_UP to "AudioVolumeUp", + AndroidKeyEvent.KEYCODE_VOLUME_MUTE to "AudioVolumeMute", + AndroidKeyEvent.KEYCODE_MUTE to "MicrophoneVolumeMute", + AndroidKeyEvent.KEYCODE_CALCULATOR to "LaunchCalculator", + AndroidKeyEvent.KEYCODE_CALENDAR to "LaunchCalendar", + AndroidKeyEvent.KEYCODE_CONTACTS to "LaunchContacts", + AndroidKeyEvent.KEYCODE_ENVELOPE to "LaunchMail", + AndroidKeyEvent.KEYCODE_MUSIC to "LaunchMusicPlayer", + AndroidKeyEvent.KEYCODE_EXPLORER to "LaunchWebBrowser", + AndroidKeyEvent.KEYCODE_BOOKMARK to "BrowserFavorites", + AndroidKeyEvent.KEYCODE_FORWARD to "BrowserForward", + AndroidKeyEvent.KEYCODE_SEARCH to "BrowserSearch", + AndroidKeyEvent.KEYCODE_APP_SWITCH to "AppSwitch", + AndroidKeyEvent.KEYCODE_CALL to "Call", + AndroidKeyEvent.KEYCODE_CAMERA to "Camera", + AndroidKeyEvent.KEYCODE_FOCUS to "CameraFocus", + AndroidKeyEvent.KEYCODE_ENDCALL to "EndCall", + AndroidKeyEvent.KEYCODE_BACK to "GoBack", + AndroidKeyEvent.KEYCODE_HOME to "GoHome", + AndroidKeyEvent.KEYCODE_HEADSETHOOK to "HeadsetHook", + AndroidKeyEvent.KEYCODE_NOTIFICATION to "Notification", + AndroidKeyEvent.KEYCODE_MANNER_MODE to "MannerMode", + AndroidKeyEvent.KEYCODE_TV to "TV", + AndroidKeyEvent.KEYCODE_3D_MODE to "TV3DMode", + AndroidKeyEvent.KEYCODE_TV_ANTENNA_CABLE to "TVAntennaCable", + AndroidKeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION to "TVAudioDescription", + AndroidKeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN to "TVAudioDescriptionMixDown", + AndroidKeyEvent.KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP to "TVAudioDescriptionMixUp", + AndroidKeyEvent.KEYCODE_TV_CONTENTS_MENU to "TVContentsMenu", + AndroidKeyEvent.KEYCODE_TV_DATA_SERVICE to "TVDataService", + AndroidKeyEvent.KEYCODE_TV_INPUT to "TVInput", + AndroidKeyEvent.KEYCODE_TV_INPUT_COMPONENT_1 to "TVInputComponent1", + AndroidKeyEvent.KEYCODE_TV_INPUT_COMPONENT_2 to "TVInputComponent2", + AndroidKeyEvent.KEYCODE_TV_INPUT_COMPOSITE_1 to "TVInputComposite1", + AndroidKeyEvent.KEYCODE_TV_INPUT_COMPOSITE_2 to "TVInputComposite2", + AndroidKeyEvent.KEYCODE_TV_INPUT_HDMI_1 to "TVInputHDMI1", + AndroidKeyEvent.KEYCODE_TV_INPUT_HDMI_2 to "TVInputHDMI2", + AndroidKeyEvent.KEYCODE_TV_INPUT_HDMI_3 to "TVInputHDMI3", + AndroidKeyEvent.KEYCODE_TV_INPUT_HDMI_4 to "TVInputHDMI4", + AndroidKeyEvent.KEYCODE_TV_INPUT_VGA_1 to "TVInputVGA1", + AndroidKeyEvent.KEYCODE_TV_NETWORK to "TVNetwork", + AndroidKeyEvent.KEYCODE_TV_NUMBER_ENTRY to "TVNumberEntry", + AndroidKeyEvent.KEYCODE_TV_POWER to "TVPower", + AndroidKeyEvent.KEYCODE_TV_RADIO_SERVICE to "TVRadioService", + AndroidKeyEvent.KEYCODE_TV_SATELLITE to "TVSatellite", + AndroidKeyEvent.KEYCODE_TV_SATELLITE_BS to "TVSatelliteBS", + AndroidKeyEvent.KEYCODE_TV_SATELLITE_CS to "TVSatelliteCS", + AndroidKeyEvent.KEYCODE_TV_SATELLITE_SERVICE to "TVSatelliteToggle", + AndroidKeyEvent.KEYCODE_TV_TERRESTRIAL_ANALOG to "TVTerrestrialAnalog", + AndroidKeyEvent.KEYCODE_TV_TERRESTRIAL_DIGITAL to "TVTerrestrialDigital", + AndroidKeyEvent.KEYCODE_TV_TIMER_PROGRAMMING to "TVTimer", + AndroidKeyEvent.KEYCODE_AVR_INPUT to "AVRInput", + AndroidKeyEvent.KEYCODE_AVR_POWER to "AVRPower", + AndroidKeyEvent.KEYCODE_PROG_RED to "ColorF0Red", + AndroidKeyEvent.KEYCODE_PROG_GREEN to "ColorF1Green", + AndroidKeyEvent.KEYCODE_PROG_YELLOW to "ColorF2Yellow", + AndroidKeyEvent.KEYCODE_PROG_BLUE to "ColorF3Blue", + AndroidKeyEvent.KEYCODE_CAPTIONS to "ClosedCaptionToggle", + AndroidKeyEvent.KEYCODE_DVR to "DVR", + AndroidKeyEvent.KEYCODE_GUIDE to "Guide", + AndroidKeyEvent.KEYCODE_INFO to "Info", + AndroidKeyEvent.KEYCODE_MEDIA_AUDIO_TRACK to "MediaAudioTrack", + AndroidKeyEvent.KEYCODE_LAST_CHANNEL to "MediaLast", + AndroidKeyEvent.KEYCODE_MEDIA_TOP_MENU to "MediaTopMenu", + AndroidKeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD to "MediaSkipBackward", + AndroidKeyEvent.KEYCODE_MEDIA_SKIP_FORWARD to "MediaSkipForward", + AndroidKeyEvent.KEYCODE_MEDIA_STEP_BACKWARD to "MediaStepBackward", + AndroidKeyEvent.KEYCODE_MEDIA_STEP_FORWARD to "MediaStepForward", + AndroidKeyEvent.KEYCODE_NAVIGATE_IN to "NavigateIn", + AndroidKeyEvent.KEYCODE_NAVIGATE_NEXT to "NavigateNext", + AndroidKeyEvent.KEYCODE_NAVIGATE_OUT to "NavigateOut", + AndroidKeyEvent.KEYCODE_NAVIGATE_PREVIOUS to "NavigatePrevious", + AndroidKeyEvent.KEYCODE_PAIRING to "Pairing", + AndroidKeyEvent.KEYCODE_WINDOW to "PinPToggle", + AndroidKeyEvent.KEYCODE_SETTINGS to "Settings", + AndroidKeyEvent.KEYCODE_STB_INPUT to "STBInput", + AndroidKeyEvent.KEYCODE_STB_POWER to "STBPower", + AndroidKeyEvent.KEYCODE_TV_TELETEXT to "Teletext", + AndroidKeyEvent.KEYCODE_TV_ZOOM_MODE to "ZoomToggle", + AndroidKeyEvent.KEYCODE_SOFT_LEFT to "SoftLeft", + AndroidKeyEvent.KEYCODE_SOFT_RIGHT to "SoftRight", + ) + } +} 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..b65ba9d027bc 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 @@ -67,6 +67,7 @@ import com.facebook.react.uimanager.ReactAccessibilityDelegate import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.KeyValue import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.LogicalEdge @@ -133,6 +134,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var keyListener: InternalKeyListener? = null private var detectScrollMovement = false private var onKeyPress = false + private var isDeadKeyPending = false private val textAttributes: TextAttributes private var typefaceDirty = false private var fontFamily: String? = null @@ -339,6 +341,34 @@ public open class ReactEditText public constructor(context: Context) : AppCompat return super.onTouchEvent(ev) } + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + val character = KeyValue.typedCharacter(event) + if (onKeyPress && !event.isSystem) { + eventDispatcher?.dispatchEvent( + ReactTextInputKeyPressEvent( + UIManagerHelper.getSurfaceId(this), + id, + KeyValue.of(keyCode, composeWithPendingDeadKey(character)), + event.metaState, + ) + ) + } + val handled = super.onKeyDown(keyCode, event) + isDeadKeyPending = + KeyValue.isDeadKey(character) || (isDeadKeyPending && KeyEvent.isModifierKey(keyCode)) + return handled + } + + // QwertyKeyListener leaves a dead key's accent selected and composes it with the next character. + private fun composeWithPendingDeadKey(character: Int): Int { + val text = text + if (!isDeadKeyPending || character == 0 || text == null || selectionEnd - selectionStart != 1) { + return character + } + val composed = KeyEvent.getDeadChar(text[selectionStart].code, character) + return if (composed != 0) composed else character + } + // 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 { @@ -503,6 +533,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat FLog.e(TAG, "onSelectionChanged[$id]: $selStart $selEnd") } + isDeadKeyPending = false super.onSelectionChanged(selStart, selEnd) if (selectionWatcher != null && hasFocus()) { selectionWatcher?.onSelectionChanged(selStart, selEnd) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextInputConnectionWrapper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextInputConnectionWrapper.kt index c80c9d58d95b..8959af39cb03 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextInputConnectionWrapper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditTextInputConnectionWrapper.kt @@ -7,7 +7,6 @@ package com.facebook.react.views.textinput -import android.view.KeyEvent import android.view.inputmethod.InputConnection import android.view.inputmethod.InputConnectionWrapper import com.facebook.react.bridge.ReactContext @@ -38,12 +37,6 @@ import com.facebook.react.uimanager.events.EventDispatcher * composing text with [InputConnection.setComposingText] and commits a space character with a * separate call to [InputConnection.setComposingText]. Here we chose to emit the last input of a * batch edit as that tends to be the user input, but it's completely arbitrary. - * - *

Another function of this class is to detect backspaces when the cursor is at the beginning of - * the [android.widget.EditText], i.e no text is deleted. - * - *

N.B. this class is only applicable for soft keyboards behavior. For hardware keyboards - * [android.view.View.onKeyDown] can be overridden to obtain the keycode of the key pressed. */ internal class ReactEditTextInputConnectionWrapper( target: InputConnection, @@ -111,25 +104,6 @@ internal class ReactEditTextInputConnectionWrapper( return super.deleteSurroundingText(beforeLength, afterLength) } - // Called by SwiftKey when cursor at beginning of input when there is a delete - // or when enter is pressed anywhere in the text. Whereas stock Android Keyboard calls - // [InputConnection.deleteSurroundingText] & [InputConnection.commitText] - // in each case, respectively. - override fun sendKeyEvent(event: KeyEvent): Boolean { - if (event.action == KeyEvent.ACTION_DOWN) { - val isNumberKey = event.unicodeChar in 48..57 - when (event.keyCode) { - KeyEvent.KEYCODE_DEL -> dispatchKeyEvent(BACKSPACE_KEY_VALUE) - KeyEvent.KEYCODE_ENTER -> dispatchKeyEvent(ENTER_KEY_VALUE) - else -> - if (isNumberKey) { - dispatchKeyEvent(event.number.toString()) - } - } - } - return super.sendKeyEvent(event) - } - private fun dispatchKeyEventOrEnqueue(inputKey: String) { if (isBatchEdit) { key = inputKey diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputKeyPressEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputKeyPressEvent.kt index 356e2de89306..30a9bad29a12 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputKeyPressEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputKeyPressEvent.kt @@ -7,17 +7,28 @@ package com.facebook.react.views.textinput +import android.view.KeyEvent import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event /** Event emitted by EditText native view when key pressed. */ -internal class ReactTextInputKeyPressEvent(surfaceId: Int, viewId: Int, private val key: String) : - Event(surfaceId, viewId) { +internal class ReactTextInputKeyPressEvent( + surfaceId: Int, + viewId: Int, + private val key: String, + private val metaState: Int = 0, +) : Event(surfaceId, viewId) { override fun getEventName(): String = EVENT_NAME override fun getEventData(): WritableMap { - return Arguments.createMap().apply { putString("key", key) } + return Arguments.createMap().apply { + putString("key", key) + putBoolean("altKey", (metaState and KeyEvent.META_ALT_ON) != 0) + putBoolean("ctrlKey", (metaState and KeyEvent.META_CTRL_ON) != 0) + putBoolean("metaKey", (metaState and KeyEvent.META_META_ON) != 0) + putBoolean("shiftKey", (metaState and KeyEvent.META_SHIFT_ON) != 0) + } } // We don't want to miss any textinput event, as event data is incremental. diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyDownEventTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyDownEventTest.kt new file mode 100644 index 000000000000..41ee36994feb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyDownEventTest.kt @@ -0,0 +1,50 @@ +/* + * 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.uimanager.events + +import android.view.KeyEvent as AndroidKeyEvent +import com.facebook.react.bridge.WritableMap +import com.facebook.testutils.shadows.ShadowArguments +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowArguments::class]) +class KeyDownEventTest { + + @Suppress("DEPRECATION") + private fun keyOf(keyCode: Int): String? { + val event = KeyDownEvent(1, 2, AndroidKeyEvent(0, 0, AndroidKeyEvent.ACTION_DOWN, keyCode, 0)) + val emitter = mock() + val payload = argumentCaptor() + event.dispatchModern(emitter) + verify(emitter).receiveEvent(any(), any(), any(), any(), any(), payload.capture(), any()) + return payload.firstValue.getString("key") + } + + @Test + fun keyThatTypesNoCharacterIsNamedByItsKeyValue() { + assertThat( + listOf( + AndroidKeyEvent.KEYCODE_MOVE_HOME, + AndroidKeyEvent.KEYCODE_PAGE_DOWN, + AndroidKeyEvent.KEYCODE_FORWARD_DEL, + AndroidKeyEvent.KEYCODE_F1, + ) + .map(::keyOf) + ) + .containsExactly("Home", "PageDown", "Delete", "F1") + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyValueTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyValueTest.kt new file mode 100644 index 000000000000..da39660a641d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/events/KeyValueTest.kt @@ -0,0 +1,49 @@ +/* + * 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.uimanager.events + +import android.view.KeyCharacterMap +import android.view.KeyEvent as AndroidKeyEvent +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class KeyValueTest { + + @Test + fun keyThatTypesACharacterIsThatCharacter() { + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_E, 'é'.code)).isEqualTo("é") + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_SPACE, ' '.code)).isEqualTo(" ") + } + + @Test + fun keyThatTypesAControlCharacterIsNamed() { + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_ENTER, '\n'.code)).isEqualTo("Enter") + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_TAB, '\t'.code)).isEqualTo("Tab") + } + + @Test + fun keyThatTypesNoCharacterIsNamed() { + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_MOVE_END, 0)).isEqualTo("End") + } + + @Test + fun deadKeyIsDead() { + assertThat( + KeyValue.of(AndroidKeyEvent.KEYCODE_GRAVE, KeyCharacterMap.COMBINING_ACCENT or '`'.code) + ) + .isEqualTo("Dead") + } + + @Test + fun keyWithNoNameThatTypesNoCharacterIsUnidentified() { + assertThat(KeyValue.of(AndroidKeyEvent.KEYCODE_BUTTON_A, 0)).isEqualTo("Unidentified") + } +} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyPressTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyPressTest.kt new file mode 100644 index 000000000000..d321da671352 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactEditTextKeyPressTest.kt @@ -0,0 +1,350 @@ +/* + * 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. + */ + +// TODO T207169925: Migrate CatalystInstance to Reacthost and remove the Suppress("DEPRECATION") +// annotation +@file:Suppress("DEPRECATION") + +package com.facebook.react.views.textinput + +import android.app.Activity +import android.os.Looper +import android.util.DisplayMetrics +import android.view.InputDevice +import android.view.KeyCharacterMap +import android.view.KeyEvent +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.core.content.res.ResourcesCompat.ID_NULL +import com.facebook.react.bridge.BridgeReactContext +import com.facebook.react.bridge.ReactTestHelper.createMockCatalystInstance +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.DisplayMetricsHolder +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.RCTModernEventEmitter +import com.facebook.testutils.shadows.ShadowArguments +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowViewRootImpl + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowArguments::class]) +class ReactEditTextKeyPressTest { + + private data class KeyPress( + val key: String?, + val heldModifiers: Set, + val textWhenReported: String, + ) + + private lateinit var activity: Activity + private lateinit var view: ReactEditText + private val keyPresses = mutableListOf() + private var nextDownTime = 1_000L + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics()) + activity = Robolectric.buildActivity(Activity::class.java).setup().get() + val reactContext = BridgeReactContext(activity) + reactContext.initializeWithInstance(createMockCatalystInstance()) + val eventDispatcher: EventDispatcher = mock { + on { dispatchEvent(any()) } doAnswer + { invocation -> + val event = invocation.getArgument>(0) + if (event is ReactTextInputKeyPressEvent) { + keyPresses.add(read(event)) + } + } + } + view = ReactEditText(ThemedReactContext(reactContext, activity, null, ID_NULL)) + view.setEventDispatcher(eventDispatcher) + view.setOnKeyPress(true) + activity.setContentView(view) + Shadow.extract(activity.window.decorView.parent) + .callWindowFocusChanged(true) + view.requestFocusFromJS() + shadowOf(Looper.getMainLooper()).idle() + assertThat(view.hasWindowFocus()).isTrue() + assertThat(view.isFocused).isTrue() + } + + private fun read(event: ReactTextInputKeyPressEvent): KeyPress { + val emitter = mock() + val payload = argumentCaptor() + event.dispatchModern(emitter) + verify(emitter).receiveEvent(any(), any(), any(), any(), any(), payload.capture(), any()) + val data: ReadableMap = payload.firstValue + return KeyPress( + key = data.getString("key"), + heldModifiers = MODIFIER_FIELDS.filter { data.hasKey(it) && data.getBoolean(it) }.toSet(), + textWhenReported = view.text.toString(), + ) + } + + private fun keys(): List = keyPresses.map { it.key } + + private fun keyEvent( + action: Int, + keyCode: Int, + downTime: Long, + metaState: Int = 0, + repeatCount: Int = 0, + flags: Int = 0, + ): KeyEvent = + KeyEvent( + downTime, + downTime + repeatCount, + action, + keyCode, + repeatCount, + metaState, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0, + flags, + InputDevice.SOURCE_KEYBOARD, + ) + + private fun pressHardwareKey(keyCode: Int, metaState: Int = 0, repeats: Int = 0) { + val downTime = nextDownTime++ + val window = activity.window.decorView + for (repeatCount in 0..repeats) { + window.dispatchKeyEvent( + keyEvent(KeyEvent.ACTION_DOWN, keyCode, downTime, metaState, repeatCount) + ) + } + window.dispatchKeyEvent(keyEvent(KeyEvent.ACTION_UP, keyCode, downTime, metaState)) + } + + // Robolectric's key character map has no dead keys and ignores Alt, so this event carries its + // own. + private class LayoutKeyEvent( + action: Int, + keyCode: Int, + downTime: Long, + metaState: Int, + private val layout: (metaState: Int) -> Int, + ) : + KeyEvent( + downTime, + downTime, + action, + keyCode, + 0, + metaState, + KeyCharacterMap.VIRTUAL_KEYBOARD, + 0, + 0, + InputDevice.SOURCE_KEYBOARD, + ) { + override fun getUnicodeChar(metaState: Int): Int = layout(metaState) + } + + private fun pressHardwareKeyOnLayout(keyCode: Int, metaState: Int, layout: (Int) -> Int) { + val downTime = nextDownTime++ + val window = activity.window.decorView + window.dispatchKeyEvent( + LayoutKeyEvent(KeyEvent.ACTION_DOWN, keyCode, downTime, metaState, layout) + ) + window.dispatchKeyEvent( + LayoutKeyEvent(KeyEvent.ACTION_UP, keyCode, downTime, metaState, layout) + ) + } + + private fun openInputConnection(): InputConnection = + checkNotNull(view.onCreateInputConnection(EditorInfo())) + + private fun sendFromInputMethod(inputConnection: InputConnection, keyCode: Int, flags: Int) { + val downTime = nextDownTime++ + inputConnection.sendKeyEvent(keyEvent(KeyEvent.ACTION_DOWN, keyCode, downTime, flags = flags)) + inputConnection.sendKeyEvent(keyEvent(KeyEvent.ACTION_UP, keyCode, downTime, flags = flags)) + } + + private fun deliverPendingInput() { + shadowOf(Looper.getMainLooper()).idle() + } + + @Test + fun hardwareKeysAreReportedByTheirKeyValue() { + pressHardwareKey(KeyEvent.KEYCODE_A) + pressHardwareKey(KeyEvent.KEYCODE_7) + pressHardwareKey(KeyEvent.KEYCODE_SPACE) + pressHardwareKey(KeyEvent.KEYCODE_DPAD_LEFT) + pressHardwareKey(KeyEvent.KEYCODE_MOVE_HOME) + pressHardwareKey(KeyEvent.KEYCODE_F1) + pressHardwareKey(KeyEvent.KEYCODE_SHIFT_LEFT) + pressHardwareKey(KeyEvent.KEYCODE_BUTTON_A) + + assertThat(keys()) + .containsExactly("a", "7", " ", "ArrowLeft", "Home", "F1", "Shift", "Unidentified") + } + + @Test + fun hardwareKeyIsReportedWithTheModifiersHeldWithIt() { + pressHardwareKey(KeyEvent.KEYCODE_A, KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON) + pressHardwareKey(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON) + + assertThat(keyPresses.map { it.key to it.heldModifiers }) + .containsExactly("A" to setOf("shiftKey"), "z" to setOf("ctrlKey")) + } + + @Test + fun characterTypedAfterADeadKeyIsReportedComposed() { + val acuteOnAltE = { metaState: Int -> + if ((metaState and KeyEvent.META_ALT_ON) != 0) KeyCharacterMap.COMBINING_ACCENT or '´'.code + else 'e'.code + } + + pressHardwareKeyOnLayout(KeyEvent.KEYCODE_E, ALT_HELD, acuteOnAltE) + pressHardwareKeyOnLayout(KeyEvent.KEYCODE_E, 0, acuteOnAltE) + + assertThat(keyPresses.map { it.key to it.heldModifiers }) + .containsExactly("Dead" to setOf("altKey"), "é" to emptySet()) + assertThat(view.text.toString()).isEqualTo("é") + } + + @Test + fun altShortcutOnALayoutWhereAltTypesNothingIsReportedByItsKeysCharacter() { + val nothingOnAlt = { metaState: Int -> + if ((metaState and KeyEvent.META_ALT_ON) != 0) 0 else 'e'.code + } + + pressHardwareKeyOnLayout(KeyEvent.KEYCODE_E, ALT_HELD, nothingOnAlt) + + assertThat(keyPresses.map { it.key to it.heldModifiers }) + .containsExactly("e" to setOf("altKey")) + assertThat(view.text.toString()).isEmpty() + } + + @Test + fun hardwareEnterAndBackspaceUseTheNamesTheInputMethodReports() { + view.setText("ab") + view.setSelection(2) + + pressHardwareKey(KeyEvent.KEYCODE_DEL) + pressHardwareKey(KeyEvent.KEYCODE_ENTER) + + assertThat(keys()).containsExactly("Backspace", "Enter") + } + + @Test + fun systemKeysAreNotReported() { + pressHardwareKey(KeyEvent.KEYCODE_VOLUME_UP) + pressHardwareKey(KeyEvent.KEYCODE_VOLUME_DOWN) + pressHardwareKey(KeyEvent.KEYCODE_BACK) + + assertThat(keyPresses).isEmpty() + } + + @Test + fun keyIsReportedBeforeTheTextItTypesChanges() { + pressHardwareKey(KeyEvent.KEYCODE_A) + + assertThat(keyPresses.map { it.textWhenReported }).containsExactly("") + assertThat(view.text.toString()).isEqualTo("a") + } + + @Test + fun heldKeyIsReportedAtEachRepeat() { + view.setText("abc") + view.setSelection(3) + + pressHardwareKey(KeyEvent.KEYCODE_DEL, repeats = 2) + + assertThat(keys()).containsExactly("Backspace", "Backspace", "Backspace") + assertThat(view.text.toString()).isEmpty() + } + + @Test + fun characterTheInputMethodSendsAsAKeyIsReportedOnce() { + sendFromInputMethod(openInputConnection(), KeyEvent.KEYCODE_A, SOFT_KEYBOARD_FLAGS) + deliverPendingInput() + + assertThat(keys()).containsExactly("a") + assertThat(view.text.toString()).isEqualTo("a") + } + + @Test + fun digitTheNumericKeypadSendsIsReportedOnce() { + sendFromInputMethod(openInputConnection(), KeyEvent.KEYCODE_7, SOFT_KEYBOARD_FLAGS) + deliverPendingInput() + + assertThat(keys()).containsExactly("7") + } + + @Test + fun backspaceTheInputMethodSendsOnAnEmptyFieldIsReportedOnce() { + sendFromInputMethod(openInputConnection(), KeyEvent.KEYCODE_DEL, SOFT_KEYBOARD_FLAGS) + deliverPendingInput() + + assertThat(keys()).containsExactly("Backspace") + } + + @Test + fun hardwareKeyTheInputMethodForwardsIsReportedOnce() { + sendFromInputMethod( + openInputConnection(), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.FLAG_FROM_SYSTEM, + ) + deliverPendingInput() + + assertThat(keys()).containsExactly("ArrowLeft") + } + + @Test + fun keysTheInputMethodSendsTogetherAreReportedInOrder() { + val inputConnection = openInputConnection() + sendFromInputMethod(inputConnection, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.FLAG_FROM_SYSTEM) + sendFromInputMethod(inputConnection, KeyEvent.KEYCODE_DEL, KeyEvent.FLAG_FROM_SYSTEM) + deliverPendingInput() + + assertThat(keys()).containsExactly("ArrowLeft", "Backspace") + } + + @Test + fun textTheInputMethodCommitsIsReportedOnce() { + openInputConnection().commitText("a", 1) + deliverPendingInput() + + assertThat(keys()).containsExactly("a") + } + + @Test + fun noKeyIsReportedWithoutAnOnKeyPressListener() { + view.setOnKeyPress(false) + + pressHardwareKey(KeyEvent.KEYCODE_DPAD_UP) + sendFromInputMethod(openInputConnection(), KeyEvent.KEYCODE_A, SOFT_KEYBOARD_FLAGS) + deliverPendingInput() + + assertThat(keyPresses).isEmpty() + } + + private companion object { + val MODIFIER_FIELDS = listOf("altKey", "ctrlKey", "metaKey", "shiftKey") + const val SOFT_KEYBOARD_FLAGS = KeyEvent.FLAG_SOFT_KEYBOARD or KeyEvent.FLAG_KEEP_TOUCH_MODE + const val ALT_HELD = KeyEvent.META_ALT_ON or KeyEvent.META_ALT_LEFT_ON + } +} diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 74c407136ff1..d1d9e2ae5ae2 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<5f564dbad822f48e5e5968aeea2bc82b>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -5132,8 +5132,12 @@ declare type TextInputKeyPressEvent = NativeSyntheticEvent declare type TextInputKeyPressEventData = Readonly< Omit & { + altKey?: boolean + ctrlKey?: boolean eventCount: number key: string + metaKey?: boolean + shiftKey?: boolean target?: number } > @@ -6022,7 +6026,7 @@ export { TaskProvider, // 266dedf2 Text, // 6f8269c1 TextContentType, // 239b3ecc - TextInput, // cf6467dc + TextInput, // a2db6742 TextInputAndroidProps, // 9ebbc103 TextInputBlurEvent, // b4c7a58c TextInputChangeEvent, // 92363e10 @@ -6031,8 +6035,8 @@ export { TextInputFocusEvent, // ef604040 TextInputIOSProps, // fb3c9327 TextInputInstance, // 53fe7b73 - TextInputKeyPressEvent, // cbbc322a - TextInputProps, // bb087c92 + TextInputKeyPressEvent, // 849d7872 + TextInputProps, // d3f74596 TextInputSelectionChangeEvent, // 4a8f10c2 TextInputSubmitEditingEvent, // e811fdbd TextInstance, // 7c7e2069 diff --git a/packages/react-native/types_DEPRECATED/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/types_DEPRECATED/Libraries/Components/TextInput/TextInput.d.ts index b3ed14124246..3dbce76668c7 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/Components/TextInput/TextInput.d.ts @@ -471,6 +471,10 @@ export type TextInputSelectionChangeEvent = export interface TextInputKeyPressEventData { key: string; eventCount: number; + altKey?: boolean | undefined; + ctrlKey?: boolean | undefined; + metaKey?: boolean | undefined; + shiftKey?: boolean | undefined; } /** @@ -874,7 +878,7 @@ export interface TextInputProps * where keyValue is 'Enter' or 'Backspace' for respective keys and the typed-in character otherwise including ' ' for space. * * Fires before onChange callbacks. - * Note: on Android only the inputs from soft keyboard are handled, not the hardware keyboard inputs. + * On Android, keys pressed on a hardware keyboard are reported as well, each by its KeyboardEvent.key value (such as 'ArrowLeft', 'Shift' or 'F1'), with altKey, ctrlKey, metaKey and shiftKey set for the modifier keys held. */ onKeyPress?: ((e: TextInputKeyPressEvent) => void) | undefined;