diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index ac7e88db84ff6c..353fb91f1766c3 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -16,6 +16,8 @@ import type { BlurEvent, FocusEvent, GestureResponderEvent, + KeyDownEvent, + KeyUpEvent, LayoutChangeEvent, LayoutRectangle, MouseEvent, @@ -115,6 +117,13 @@ type FocusEventProps = $ReadOnly<{ onFocusCapture?: ?(event: FocusEvent) => void, }>; +type KeyEventProps = $ReadOnly<{ + onKeyDown?: ?(event: KeyDownEvent) => void, + onKeyDownCapture?: ?(event: KeyDownEvent) => void, + onKeyUp?: ?(event: KeyUpEvent) => void, + onKeyUpCapture?: ?(event: KeyUpEvent) => void, +}>; + type TouchEventProps = $ReadOnly<{ onTouchCancel?: ?(e: GestureResponderEvent) => void, onTouchCancelCapture?: ?(e: GestureResponderEvent) => void, @@ -505,6 +514,7 @@ export type ViewProps = $ReadOnly<{ ...MouseEventProps, ...PointerEventProps, ...FocusEventProps, + ...KeyEventProps, ...TouchEventProps, ...ViewPropsAndroid, ...ViewPropsIOS, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js index e999e185f4fdb3..728515be9bc41b 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.android.js @@ -122,6 +122,18 @@ const bubblingEventTypes = { bubbled: 'onFocus', }, }, + topKeyDown: { + phasedRegistrationNames: { + captured: 'onKeyDownCapture', + bubbled: 'onKeyDown', + }, + }, + topKeyUp: { + phasedRegistrationNames: { + captured: 'onKeyUpCapture', + bubbled: 'onKeyUp', + }, + }, }; const directEventTypes = { diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index da8e9aa08fa0a6..886d404494b01e 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -322,3 +322,34 @@ export type MouseEvent = NativeSyntheticEvent< timestamp: number, }>, >; + +export type KeyEvent = $ReadOnly<{ + /** + * The actual key that was pressed. For example, F would be "f" or "F" depending on the shift key. + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + */ + key: string, + /** + * The key code of the key that was pressed. For example, F would be "KeyF" + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code + */ + code: string, + altKey: boolean, + ctrlKey: boolean, + metaKey: boolean, + shiftKey: boolean, + /** + * A boolean value that is true if the given key is being held down such that it is automatically repeating. + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat + */ + repeat?: boolean, + /** + * Returns a boolean value indicating if the event is fired within a composition session + * @see https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/isComposing + */ + isComposing?: boolean, +}>; + +export type KeyUpEvent = NativeSyntheticEvent; + +export type KeyDownEvent = NativeSyntheticEvent; diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 5dc25763469471..c39a442b5f7d19 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -380,6 +380,7 @@ public class com/facebook/react/ReactRootView : android/widget/FrameLayout, com/ public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V protected fun dispatchDraw (Landroid/graphics/Canvas;)V + protected fun dispatchJSKeyEvent (Landroid/view/KeyEvent;)V protected fun dispatchJSPointerEvent (Landroid/view/MotionEvent;Z)V protected fun dispatchJSTouchEvent (Landroid/view/MotionEvent;)V public fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z @@ -3121,6 +3122,7 @@ public final class com/facebook/react/runtime/ReactSurfaceView : com/facebook/re public fun isViewAttachedToReactInstance ()Z public fun onChildEndedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V public fun onChildStartedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V + public fun requestChildFocus (Landroid/view/View;Landroid/view/View;)V public fun requestDisallowInterceptTouchEvent (Z)V public fun setIsFabric (Z)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index cf143a7c6a67b6..58d0b8c8d04746 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -57,6 +57,7 @@ import com.facebook.react.modules.deviceinfo.DeviceInfoModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.IllegalViewOperationException; +import com.facebook.react.uimanager.JSKeyDispatcher; import com.facebook.react.uimanager.JSPointerDispatcher; import com.facebook.react.uimanager.JSTouchDispatcher; import com.facebook.react.uimanager.PixelUtil; @@ -105,6 +106,7 @@ public interface ReactRootViewEventListener { private boolean mShouldLogContentAppeared; private @Nullable JSTouchDispatcher mJSTouchDispatcher; private @Nullable JSPointerDispatcher mJSPointerDispatcher; + private @Nullable JSKeyDispatcher mJSKeyDispatcher; private final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper = new ReactAndroidHWInputDeviceHelper(); private boolean mWasMeasured = false; @@ -333,10 +335,17 @@ public boolean dispatchKeyEvent(KeyEvent ev) { FLog.w(TAG, "Unable to handle key event as the catalyst instance has not been attached"); return super.dispatchKeyEvent(ev); } + ReactContext context = getCurrentReactContext(); - if (context != null) { - mAndroidHWInputDeviceHelper.handleKeyEvent(ev, context); + if (context == null) { + return super.dispatchKeyEvent(ev); } + + mAndroidHWInputDeviceHelper.handleKeyEvent(ev, context); + + // Dispatch during the capture phase before children handle the event as the focus could shift + dispatchJSKeyEvent(ev); + return super.dispatchKeyEvent(ev); } @@ -352,6 +361,17 @@ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyF ReactContext context = getCurrentReactContext(); if (context != null) { mAndroidHWInputDeviceHelper.clearFocus(context); + + if (mJSKeyDispatcher != null && ReactNativeFeatureFlags.enableKeyEvents()) { + if (gainFocus) { + @Nullable View focusedChild = getFocusedChild(); + if (focusedChild != null) { + mJSKeyDispatcher.setFocusedView(focusedChild.getId()); + } + } else { + mJSKeyDispatcher.clearFocus(); + } + } } super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @@ -369,6 +389,10 @@ public void requestChildFocus(View child, View focused) { ReactContext context = getCurrentReactContext(); if (context != null) { mAndroidHWInputDeviceHelper.onFocusChanged(focused, context); + + if (mJSKeyDispatcher != null && ReactNativeFeatureFlags.enableKeyEvents()) { + mJSKeyDispatcher.setFocusedView(focused.getId()); + } } super.requestChildFocus(child, focused); } @@ -410,6 +434,31 @@ protected void dispatchJSTouchEvent(MotionEvent event) { } } + protected void dispatchJSKeyEvent(KeyEvent ev) { + if (!ReactNativeFeatureFlags.enableKeyEvents()) { + // Silently return early if key events are disabled + return; + } + if (!hasActiveReactContext() || !isViewAttachedToReactInstance()) { + FLog.w( + TAG, "Unable to dispatch key event to JS as the catalyst instance has not been attached"); + return; + } + if (mJSKeyDispatcher == null) { + FLog.w(TAG, "Unable to dispatch key event to JS before the dispatcher is available"); + return; + } + ReactContext context = getCurrentReactContext(); + if (context != null) { + EventDispatcher eventDispatcher = + UIManagerHelper.getEventDispatcher(context, getUIManagerType()); + int surfaceId = UIManagerHelper.getSurfaceId(context); + if (eventDispatcher != null) { + mJSKeyDispatcher.handleKeyEvent(ev, eventDispatcher, surfaceId); + } + } + } + @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { // Override in order to still receive events to onInterceptTouchEvent even when some other @@ -666,6 +715,10 @@ public void onAttachedToReactInstance() { mJSPointerDispatcher = new JSPointerDispatcher(this); } + if (ReactNativeFeatureFlags.enableKeyEvents()) { + mJSKeyDispatcher = new JSKeyDispatcher(); + } + if (mRootViewEventListener != null) { mRootViewEventListener.onAttachedToReactInstance(this); } @@ -744,6 +797,9 @@ public void runApplication() { if (ReactFeatureFlags.dispatchPointerEvents) { mJSPointerDispatcher = new JSPointerDispatcher(this); } + if (ReactNativeFeatureFlags.enableKeyEvents()) { + mJSKeyDispatcher = new JSKeyDispatcher(); + } } @VisibleForTesting diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt index c68ee2832912cf..91e83603de2648 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt @@ -12,6 +12,7 @@ package com.facebook.react.runtime import android.content.Context import android.graphics.Point import android.graphics.Rect +import android.view.KeyEvent import android.view.MotionEvent import android.view.View import com.facebook.common.logging.FLog @@ -20,7 +21,9 @@ import com.facebook.react.bridge.ReactContext import com.facebook.react.common.annotations.FrameworkAPI import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.config.ReactFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.IllegalViewOperationException +import com.facebook.react.uimanager.JSKeyDispatcher import com.facebook.react.uimanager.JSPointerDispatcher import com.facebook.react.uimanager.JSTouchDispatcher import com.facebook.react.uimanager.common.UIManagerType @@ -37,6 +40,7 @@ public class ReactSurfaceView(context: Context?, private val surface: ReactSurfa ReactRootView(context) { private val jsTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this) private var jsPointerDispatcher: JSPointerDispatcher? = null + private var jsKeyDispatcher: JSKeyDispatcher? = null private var wasMeasured = false private var widthMeasureSpec = 0 private var heightMeasureSpec = 0 @@ -45,6 +49,9 @@ public class ReactSurfaceView(context: Context?, private val surface: ReactSurfa if (ReactFeatureFlags.dispatchPointerEvents) { jsPointerDispatcher = JSPointerDispatcher(this) } + if (ReactNativeFeatureFlags.enableKeyEvents()) { + jsKeyDispatcher = JSKeyDispatcher() + } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { @@ -188,6 +195,51 @@ public class ReactSurfaceView(context: Context?, private val surface: ReactSurfa } } + override fun dispatchJSKeyEvent(event: KeyEvent) { + if (jsKeyDispatcher == null) { + if (!ReactNativeFeatureFlags.enableKeyEvents()) { + return + } + FLog.w(TAG, "Unable to dispatch key events to JS before the dispatcher is available") + return + } + val eventDispatcher = surface.eventDispatcher + if (eventDispatcher != null) { + jsKeyDispatcher?.handleKeyEvent(event, eventDispatcher, surface.surfaceID) + } else { + FLog.w( + TAG, + "Unable to dispatch key events to JS as the React instance has not been attached", + ) + } + } + + override fun requestChildFocus(child: View?, focused: View?) { + super.requestChildFocus(child, focused) + + if (ReactNativeFeatureFlags.enableKeyEvents()) { + val focusedViewTag = focused?.id + if (focusedViewTag != null) { + jsKeyDispatcher?.setFocusedView(focusedViewTag) + } + } + } + + override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + + if (ReactNativeFeatureFlags.enableKeyEvents()) { + if (gainFocus) { + val focusedViewTag = focusedChild?.id + if (focusedViewTag != null) { + jsKeyDispatcher?.setFocusedView(focusedViewTag) + } + } else { + jsKeyDispatcher?.clearFocus() + } + } + } + override fun hasActiveReactContext(): Boolean = surface.isAttached && surface.reactHost?.currentReactContext != null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index f665d4df6222cf..01b5d01143ebe7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -770,6 +770,16 @@ protected void onAfterUpdateTransaction(@NonNull T view) { MapBuilder.of( "phasedRegistrationNames", MapBuilder.of("bubbled", "onFocus", "captured", "onFocusCapture"))) + .put( + "topKeyDown", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onKeyDown", "captured", "onKeyDownCapture"))) + .put( + "topKeyUp", + MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onKeyUp", "captured", "onKeyUpCapture"))) .build()); return eventTypeConstants; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSKeyDispatcher.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSKeyDispatcher.kt new file mode 100644 index 00000000000000..a0aef41c46331d --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSKeyDispatcher.kt @@ -0,0 +1,65 @@ +/* + * 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 + +import android.view.KeyEvent as AndroidKeyEvent +import android.view.View +import com.facebook.react.uimanager.events.EventDispatcher +import com.facebook.react.uimanager.events.KeyDownEvent +import com.facebook.react.uimanager.events.KeyUpEvent + +/** + * JSKeyDispatcher handles dispatching keyboard events to JS from RootViews. It sends keydown and + * keyup events according to the W3C KeyboardEvent specification, supporting both capture and bubble + * phases. + * + * The keydown and keyup events provide a code indicating which key is pressed. The event target is + * derived from the currently focused Android view. + */ +internal class JSKeyDispatcher { + private var focusedViewTag: Int = View.NO_ID + + fun handleKeyEvent( + keyEvent: AndroidKeyEvent, + eventDispatcher: EventDispatcher, + surfaceId: Int, + ) { + if (focusedViewTag == View.NO_ID) { + return + } + + when (keyEvent.action) { + AndroidKeyEvent.ACTION_DOWN -> { + eventDispatcher.dispatchEvent( + KeyDownEvent( + surfaceId, + focusedViewTag, + keyEvent, + ) + ) + } + AndroidKeyEvent.ACTION_UP -> { + eventDispatcher.dispatchEvent( + KeyUpEvent( + surfaceId, + focusedViewTag, + keyEvent, + ) + ) + } + } + } + + fun setFocusedView(viewTag: Int) { + focusedViewTag = viewTag + } + + fun clearFocus() { + focusedViewTag = View.NO_ID + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyDownEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyDownEvent.kt new file mode 100644 index 00000000000000..fbda388762abca --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyDownEvent.kt @@ -0,0 +1,23 @@ +/* + * 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 + +internal class KeyDownEvent( + surfaceId: Int, + viewTag: Int, + keyEvent: AndroidKeyEvent, +) : KeyEvent(surfaceId, viewTag, keyEvent) { + + override fun getEventName(): String = EVENT_NAME + + companion object { + private const val EVENT_NAME: String = "topKeyDown" + } +} 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 new file mode 100644 index 00000000000000..802221e40939b1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyEvent.kt @@ -0,0 +1,156 @@ +/* + * 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.Arguments +import com.facebook.react.bridge.WritableMap + +internal abstract class KeyEvent( + surfaceId: Int, + viewTag: Int, + keyEvent: AndroidKeyEvent, +) : Event(surfaceId, viewTag) { + + // 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 isAltPressed: Boolean = keyEvent.isAltPressed + private val isCtrlPressed: Boolean = keyEvent.isCtrlPressed + private val isMetaPressed: Boolean = keyEvent.isMetaPressed + private val isShiftPressed: Boolean = keyEvent.isShiftPressed + + override fun canCoalesce(): Boolean = false + + override fun getEventCategory(): Int = EventCategoryDef.DISCRETE + + override fun getEventData(): WritableMap { + val eventData = Arguments.createMap() + + eventData.putInt("target", viewTag) + + // W3C KeyboardEvent properties + eventData.putString("key", getKeyString()) + eventData.putString("code", getCodeString()) + + // Modifier keys + eventData.putBoolean("altKey", isAltPressed) + eventData.putBoolean("ctrlKey", isCtrlPressed) + eventData.putBoolean("metaKey", isMetaPressed) + eventData.putBoolean("shiftKey", isShiftPressed) + + // Additional properties + eventData.putDouble("timestamp", timestampMs.toDouble()) + + 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 + } + + internal companion object { + private const val UNIDENTIFIED = "Unidentified" + + private val CODE_MAP: Map by + lazy(LazyThreadSafetyMode.PUBLICATION) { + mapOf( + // Letter keys + AndroidKeyEvent.KEYCODE_A to "KeyA", + AndroidKeyEvent.KEYCODE_B to "KeyB", + AndroidKeyEvent.KEYCODE_C to "KeyC", + AndroidKeyEvent.KEYCODE_D to "KeyD", + AndroidKeyEvent.KEYCODE_E to "KeyE", + AndroidKeyEvent.KEYCODE_F to "KeyF", + AndroidKeyEvent.KEYCODE_G to "KeyG", + AndroidKeyEvent.KEYCODE_H to "KeyH", + AndroidKeyEvent.KEYCODE_I to "KeyI", + AndroidKeyEvent.KEYCODE_J to "KeyJ", + AndroidKeyEvent.KEYCODE_K to "KeyK", + AndroidKeyEvent.KEYCODE_L to "KeyL", + AndroidKeyEvent.KEYCODE_M to "KeyM", + AndroidKeyEvent.KEYCODE_N to "KeyN", + AndroidKeyEvent.KEYCODE_O to "KeyO", + AndroidKeyEvent.KEYCODE_P to "KeyP", + AndroidKeyEvent.KEYCODE_Q to "KeyQ", + AndroidKeyEvent.KEYCODE_R to "KeyR", + AndroidKeyEvent.KEYCODE_S to "KeyS", + AndroidKeyEvent.KEYCODE_T to "KeyT", + AndroidKeyEvent.KEYCODE_U to "KeyU", + AndroidKeyEvent.KEYCODE_V to "KeyV", + AndroidKeyEvent.KEYCODE_W to "KeyW", + AndroidKeyEvent.KEYCODE_X to "KeyX", + AndroidKeyEvent.KEYCODE_Y to "KeyY", + AndroidKeyEvent.KEYCODE_Z to "KeyZ", + // Digit keys + AndroidKeyEvent.KEYCODE_0 to "Digit0", + AndroidKeyEvent.KEYCODE_1 to "Digit1", + AndroidKeyEvent.KEYCODE_2 to "Digit2", + AndroidKeyEvent.KEYCODE_3 to "Digit3", + AndroidKeyEvent.KEYCODE_4 to "Digit4", + AndroidKeyEvent.KEYCODE_5 to "Digit5", + AndroidKeyEvent.KEYCODE_6 to "Digit6", + AndroidKeyEvent.KEYCODE_7 to "Digit7", + AndroidKeyEvent.KEYCODE_8 to "Digit8", + AndroidKeyEvent.KEYCODE_9 to "Digit9", + // Special keys + AndroidKeyEvent.KEYCODE_ENTER to "Enter", + AndroidKeyEvent.KEYCODE_SPACE to "Space", + AndroidKeyEvent.KEYCODE_TAB to "Tab", + AndroidKeyEvent.KEYCODE_DEL to "Backspace", + AndroidKeyEvent.KEYCODE_ESCAPE to "Escape", + // Modifier keys + AndroidKeyEvent.KEYCODE_SHIFT_LEFT to "ShiftLeft", + AndroidKeyEvent.KEYCODE_SHIFT_RIGHT to "ShiftRight", + AndroidKeyEvent.KEYCODE_CTRL_LEFT to "ControlLeft", + AndroidKeyEvent.KEYCODE_CTRL_RIGHT to "ControlRight", + AndroidKeyEvent.KEYCODE_ALT_LEFT to "AltLeft", + AndroidKeyEvent.KEYCODE_ALT_RIGHT to "AltRight", + AndroidKeyEvent.KEYCODE_META_LEFT to "MetaLeft", + AndroidKeyEvent.KEYCODE_META_RIGHT to "MetaRight", + // Arrow keys + AndroidKeyEvent.KEYCODE_DPAD_UP to "ArrowUp", + AndroidKeyEvent.KEYCODE_DPAD_DOWN to "ArrowDown", + AndroidKeyEvent.KEYCODE_DPAD_LEFT to "ArrowLeft", + AndroidKeyEvent.KEYCODE_DPAD_RIGHT to "ArrowRight", + 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/KeyUpEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyUpEvent.kt new file mode 100644 index 00000000000000..debb5a15c0a7a4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/KeyUpEvent.kt @@ -0,0 +1,24 @@ +/* + * 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 + +/** An event representing a key release (keyup). Corresponds to W3C KeyboardEvent specification. */ +internal class KeyUpEvent( + surfaceId: Int, + viewTag: Int, + keyEvent: AndroidKeyEvent, +) : KeyEvent(surfaceId, viewTag, keyEvent) { + + override fun getEventName(): String = EVENT_NAME + + companion object { + private const val EVENT_NAME: String = "topKeyUp" + } +} diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 3dff962fc17956..1d23ff591d57d0 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<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -2938,8 +2938,26 @@ declare type KeyboardTypeOptions = | KeyboardType | KeyboardTypeAndroid | KeyboardTypeIOS +declare type KeyDownEvent = NativeSyntheticEvent +declare type KeyEvent = { + readonly altKey: boolean + readonly code: string + readonly ctrlKey: boolean + readonly isComposing?: boolean + readonly key: string + readonly metaKey: boolean + readonly repeat?: boolean + readonly shiftKey: boolean +} +declare type KeyEventProps = { + readonly onKeyDown?: (event: KeyDownEvent) => void + readonly onKeyDownCapture?: (event: KeyDownEvent) => void + readonly onKeyUp?: (event: KeyUpEvent) => void + readonly onKeyUpCapture?: (event: KeyUpEvent) => void +} declare function keyExtractor(item: any, index: number): string declare type KeysOfUnion = T extends any ? keyof T : never +declare type KeyUpEvent = NativeSyntheticEvent declare type LayoutAnimation = typeof LayoutAnimation declare type LayoutAnimationAnim = { readonly delay?: number @@ -5741,6 +5759,7 @@ declare type ViewProps = Readonly< MouseEventProps & PointerEventProps & FocusEventProps & + KeyEventProps & TouchEventProps & ViewPropsAndroid & ViewPropsIOS & @@ -5939,14 +5958,14 @@ export { ActionSheetIOS, // 88e6bfb0 ActionSheetIOSOptions, // 1756eb5a ActivityIndicator, // 8d041a45 - ActivityIndicatorProps, // 0fa4e79d + ActivityIndicatorProps, // 64ab03e1 Alert, // 5bf12165 AlertButton, // bf1a3b60 AlertButtonStyle, // ec9fb242 AlertOptions, // a0cdac0f AlertType, // 5ab91217 AndroidKeyboardEvent, // e03becc8 - Animated, // e48a2408 + Animated, // 9c6edf39 AppConfig, // ebddad4b AppRegistry, // 6cdee1d6 AppState, // f7097b1b @@ -5978,7 +5997,7 @@ export { DisplayMetrics, // 1dc35cef DisplayMetricsAndroid, // 872e62eb DrawerLayoutAndroid, // 14121b61 - DrawerLayoutAndroidProps, // 123d3a9d + DrawerLayoutAndroidProps, // 66371296 DrawerSlideEvent, // cc43db83 DropShadowValue, // e9df2606 DynamicColorIOS, // 1f9b3410 @@ -5992,8 +6011,8 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // ee0215d3 - FlatListProps, // 53c344dc + FlatList, // 940a1186 + FlatListProps, // 6fc8f490 FocusEvent, // 529b43eb FontVariant, // 7c7558bb GestureResponderEvent, // b466f6d6 @@ -6005,14 +6024,14 @@ export { IOSKeyboardEvent, // e67bfe3a IgnorePattern, // ec6f6ece Image, // 04474205 - ImageBackground, // 489b1c17 - ImageBackgroundProps, // 1b209e36 + ImageBackground, // 06d5af5c + ImageBackgroundProps, // 473a9eb4 ImageErrorEvent, // b7b2ae63 ImageLoadEvent, // 5baae813 ImageProgressEventIOS, // adb35052 - ImageProps, // 40c727e1 + ImageProps, // 089f290a ImagePropsAndroid, // 9fd9bcbb - ImagePropsBase, // 715b84bf + ImagePropsBase, // cab66428 ImagePropsIOS, // 318adce2 ImageRequireSource, // 681d683b ImageResolvedAssetSource, // f3060931 @@ -6026,9 +6045,12 @@ export { InputModeOptions, // 4e8581b9 Insets, // e7fe432a InteractionManager, // 301bfa63 + KeyDownEvent, // 5309360e + KeyEvent, // 20fa4267 + KeyUpEvent, // 7c3054e1 Keyboard, // 87311c77 - KeyboardAvoidingView, // d88d0d4c - KeyboardAvoidingViewProps, // bc844418 + KeyboardAvoidingView, // 4a933094 + KeyboardAvoidingViewProps, // 81cba23f KeyboardEvent, // c3f895d4 KeyboardEventEasing, // af4091c8 KeyboardEventName, // 59299ad6 @@ -6055,7 +6077,7 @@ export { MeasureOnSuccessCallback, // 82824e59 Modal, // 78e8a79d ModalBaseProps, // 0c81c9b1 - ModalProps, // 270223fa + ModalProps, // a69426b0 ModalPropsAndroid, // 515fb173 ModalPropsIOS, // 4fbcedf6 ModeChangeEvent, // af2c4926 @@ -6093,11 +6115,11 @@ export { PointerEvent, // ff3129ff Pressable, // 3c6e4eb9 PressableAndroidRippleConfig, // 42bc9727 - PressableProps, // 96c8132d + PressableProps, // 42ae2cc7 PressableStateCallbackType, // 9af36561 ProcessedColorValue, // 33f74304 ProgressBarAndroid, // 03e66cf5 - ProgressBarAndroidProps, // 29338dc2 + ProgressBarAndroidProps, // 021ef6d2 PromiseTask, // 5102c862 PublicRootInstance, // 8040afd7 PublicTextInstance, // 7d73f802 @@ -6106,8 +6128,8 @@ export { PushNotificationPermissions, // c2e7ae4f Rationale, // 5df1b1c1 ReactNativeVersion, // abd76827 - RefreshControl, // 036f45cf - RefreshControlProps, // b7de1e77 + RefreshControl, // 2adf2586 + RefreshControlProps, // f0d99302 RefreshControlPropsAndroid, // 99f64c97 RefreshControlPropsIOS, // 72a36381 Registry, // e1ed403e @@ -6126,14 +6148,14 @@ export { ScrollToLocationParamsType, // d7ecdad1 ScrollView, // 7fb7c469 ScrollViewImperativeMethods, // eb20aa46 - ScrollViewProps, // bb4d0dd0 + ScrollViewProps, // d25b90bd ScrollViewPropsAndroid, // 84e2134b ScrollViewPropsIOS, // d83c9733 ScrollViewScrollToOptions, // 3313411e SectionBase, // b376bddc - SectionList, // 84642036 + SectionList, // 19c9aaa2 SectionListData, // 119baf83 - SectionListProps, // 4fe82009 + SectionListProps, // 52787c42 SectionListRenderItem, // 1fad0435 SectionListRenderItemInfo, // 745e1992 Separators, // 6a45f7e3 @@ -6154,7 +6176,7 @@ export { SubmitBehavior, // c4ddf490 Switch, // aebc9941 SwitchChangeEvent, // 2e5bd2de - SwitchProps, // cb21930d + SwitchProps, // 5b2f78be Systrace, // b5aa21fc TVViewPropsIOS, // 330ce7b5 TargetedEvent, // 16e98910 @@ -6169,7 +6191,7 @@ export { TextInputFocusEvent, // c36e977c TextInputIOSProps, // 0d05a855 TextInputKeyPressEvent, // 967178c2 - TextInputProps, // 8f3237f1 + TextInputProps, // a817a7f7 TextInputSelectionChangeEvent, // a1a7622f TextInputSubmitEditingEvent, // 48d903af TextLayoutEvent, // 45b0a8d7 @@ -6192,15 +6214,15 @@ export { UTFSequence, // baacd11b Vibration, // 315e131d View, // 39dd4de4 - ViewProps, // f8aca212 + ViewProps, // 1945fbb5 ViewPropsAndroid, // 21385d96 ViewPropsIOS, // 58ee19bf ViewStyle, // c2db0e6e VirtualViewMode, // 85a69ef6 VirtualizedList, // 4d513939 - VirtualizedListProps, // 06d07d2b + VirtualizedListProps, // d3376e70 VirtualizedSectionList, // 446ba0df - VirtualizedSectionListProps, // 05937609 + VirtualizedSectionListProps, // 4f35f007 WrapperComponentProvider, // 9cf3844c codegenNativeCommands, // e16d62f7 codegenNativeComponent, // ed4c8103 diff --git a/packages/react-native/index.js.flow b/packages/react-native/index.js.flow index aebeabfdf01012..aff11e60c23367 100644 --- a/packages/react-native/index.js.flow +++ b/packages/react-native/index.js.flow @@ -420,6 +420,9 @@ export type { BlurEvent, FocusEvent, GestureResponderEvent, + KeyDownEvent, + KeyEvent, + KeyUpEvent, LayoutChangeEvent, LayoutRectangle, MouseEvent,