Skip to content
Closed
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
10 changes: 10 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
BlurEvent,
FocusEvent,
GestureResponderEvent,
KeyDownEvent,
KeyUpEvent,
LayoutChangeEvent,
LayoutRectangle,
MouseEvent,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -505,6 +514,7 @@ export type ViewProps = $ReadOnly<{
...MouseEventProps,
...PointerEventProps,
...FocusEventProps,
...KeyEventProps,
...TouchEventProps,
...ViewPropsAndroid,
...ViewPropsIOS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ const bubblingEventTypes = {
bubbled: 'onFocus',
},
},
topKeyDown: {
phasedRegistrationNames: {
captured: 'onKeyDownCapture',
bubbled: 'onKeyDown',
},
},
topKeyUp: {
phasedRegistrationNames: {
captured: 'onKeyUpCapture',
bubbled: 'onKeyUp',
},
},
};

const directEventTypes = {
Expand Down
31 changes: 31 additions & 0 deletions packages/react-native/Libraries/Types/CoreEventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyEvent>;

export type KeyDownEvent = NativeSyntheticEvent<KeyEvent>;
2 changes: 2 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ public class com/facebook/react/ReactRootView : android/widget/FrameLayout, com/
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;)V
public fun <init> (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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -666,6 +715,10 @@ public void onAttachedToReactInstance() {
mJSPointerDispatcher = new JSPointerDispatcher(this);
}

if (ReactNativeFeatureFlags.enableKeyEvents()) {
mJSKeyDispatcher = new JSKeyDispatcher();
}

if (mRootViewEventListener != null) {
mRootViewEventListener.onAttachedToReactInstance(this);
}
Expand Down Expand Up @@ -744,6 +797,9 @@ public void runApplication() {
if (ReactFeatureFlags.dispatchPointerEvents) {
mJSPointerDispatcher = new JSPointerDispatcher(this);
}
if (ReactNativeFeatureFlags.enableKeyEvents()) {
mJSKeyDispatcher = new JSKeyDispatcher();
}
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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

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