Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d81c798
[fabric] Add valid key down/up props to View
Jan 30, 2024
5670456
[fabric] Add key down/up event emitters to View
Jan 30, 2024
8b79636
[fabric] Add key down/up handling to View component
Jan 30, 2024
39e0ca3
[fabric] Fix crash on key up in non focusable view
Feb 20, 2024
71a95e5
Update KeyEvent and event emitter
Saadnajmi Sep 22, 2025
c0a348d
Remove diffs in BaseTouch.cpp
Saadnajmi Sep 22, 2025
d461ca0
MacOSViewEvents --> HostPlatformViewEvents
Saadnajmi Sep 22, 2025
58a6b35
validKeysDown --> keyDownEvents (cpp)
Saadnajmi Sep 22, 2025
55ae35f
remove special case for space and enter in paper
Saadnajmi Sep 22, 2025
0e30271
[WIP] keyDownEvents implementation (obj-c++)
Saadnajmi Sep 22, 2025
69f3d60
WIP emit event only once
Saadnajmi Sep 23, 2025
cdd013c
mark keyboard events as bubbling
Saadnajmi Sep 23, 2025
f3d81d3
fix: ensure keyDownEvents is passed to View
Saadnajmi Sep 23, 2025
bea6a92
Copy new implementation to paper
Saadnajmi Sep 24, 2025
61b672a
clea nup objc_getAssociatedObject code (paper and fabric)
Saadnajmi Sep 24, 2025
a15fe8c
Mikes feedback
Saadnajmi Sep 24, 2025
b0c3d1f
dont nil check event emitter and dispatcher
Saadnajmi Sep 24, 2025
3763e4e
add focus on mount to keyboard event example
Saadnajmi Sep 24, 2025
50db3a4
f
Saadnajmi Sep 24, 2025
deefba4
Update RNTester example
Saadnajmi Sep 25, 2025
d052ab7
undo BaseTouch changes
Saadnajmi Sep 25, 2025
97625fb
fix flow errors
Saadnajmi Sep 25, 2025
de7d391
fix format
Saadnajmi Sep 27, 2025
dba556f
Update HostPlatformViewProps.cpp
Saadnajmi Sep 29, 2025
955c04e
Update KeyEvent.h
Saadnajmi Sep 29, 2025
4ff60b6
Update RCTViewComponentView.mm
Saadnajmi Sep 29, 2025
d63af7f
Update packages/react-native/React/Fabric/Mounting/ComponentViews/Vie…
Saadnajmi Sep 29, 2025
559accc
Update packages/react-native/React/Views/RCTView.m
Saadnajmi Sep 29, 2025
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
4 changes: 0 additions & 4 deletions packages/react-native/Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ const View: component(
importantForAccessibility,
nativeID,
tabIndex,
// [macOS
keyDownEvents,
keyUpEvents,
// macOS]
...otherProps
}: ViewProps,
forwardedRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ import {ConditionallyIgnoredEventHandlers} from './ViewConfigIgnore';

const bubblingEventTypes = {
...PlatformBaseViewConfigIos.bubblingEventTypes,
topKeyDown: {
phasedRegistrationNames: {
captured: 'onKeyDownCapture',
bubbled: 'onKeyDown',
},
},
topKeyUp: {
phasedRegistrationNames: {
captured: 'onKeyUpCapture',
bubbled: 'onKeyUp',
},
},
};

const directEventTypes = {
Expand All @@ -31,12 +43,6 @@ const directEventTypes = {
topDrop: {
registrationName: 'onDrop',
},
topKeyUp: {
registrationName: 'onKeyUp',
},
topKeyDown: {
registrationName: 'onKeyDown',
},
topMouseEnter: {
registrationName: 'onMouseEnter',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
#import <React/RCTBorderDrawing.h>
#import <React/RCTBoxShadow.h>
#import <React/RCTConversions.h>
#import <React/RCTCursor.h> // [macOS]
#import <React/RCTLinearGradient.h>
#import <React/RCTLocalizedString.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
Expand All @@ -30,6 +29,11 @@
#import <React/RCTComponentViewFactory.h>
#endif

#if TARGET_OS_OSX // [macOS
#import <React/RCTCursor.h>
#import <React/RCTViewKeyboardEvent.h>
#endif // macOS]

using namespace facebook::react;

const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
Expand Down Expand Up @@ -1597,7 +1601,61 @@ - (BOOL)resignFirstResponder

return YES;
}


#pragma mark - Keyboard Events

- (BOOL)handleKeyboardEvent:(NSEvent *)event {
RCTAssert(
event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp,
@"Keyboard event must be keyDown, keyUp. Got type: %ld", (long)event.type);

// Convert the event to a KeyEvent
NSEventModifierFlags modifierFlags = event.modifierFlags;
facebook::react::KeyEvent keyEvent = {
.key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String],
.altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
.capsLockKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCapsLock),
.numericPadKey = static_cast<bool>(modifierFlags & NSEventModifierFlagNumericPad),
.helpKey = static_cast<bool>(modifierFlags & NSEventModifierFlagHelp),
.functionKey = static_cast<bool>(modifierFlags & NSEventModifierFlagFunction),
};

// Emit the event to JS only once. By default, events, will bubble up the respnder chain
// when we call super, so let's emit the event only at the first responder. It would be
// simpler to check `if (self == self.window.firstResponder), however, that does not account
// for cases like TextInputComponentView, where the first responder may be a subview.
static const char kRCTViewKeyboardEventEmittedKey = 0;
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
BOOL alreadyEmitted = [emitted boolValue];
if (!alreadyEmitted) {
if (event.type == NSEventTypeKeyDown) {
_eventEmitter->onKeyDown(keyEvent);
} else {
_eventEmitter->onKeyUp(keyEvent);
}
objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
auto const& handledKeyEvents = event.type == NSEventTypeKeyDown ? _props->keyDownEvents : _props->keyUpEvents;
return std::find(handledKeyEvents.cbegin(), handledKeyEvents.cend(), keyEvent) != handledKeyEvents.cend();
}

- (void)keyDown:(NSEvent *)event {
if (![self handleKeyboardEvent:event]) {
[super keyDown:event];
}
}

- (void)keyUp:(NSEvent *)event {
if (![self handleKeyboardEvent:event]) {
[super keyUp:event];
}
}

#endif // macOS]

- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
// NOTE does not properly work with single line text inputs (most key downs). This is because those are
// presumably handled by the window's field editor. To make it work, we'd need to look into providing
// a custom field editor for NSTextField controls.
@property (nonatomic, copy) RCTDirectEventBlock onKeyDown;
@property (nonatomic, copy) RCTDirectEventBlock onKeyUp;
@property (nonatomic, copy) RCTBubblingEventBlock onKeyDown;
@property (nonatomic, copy) RCTBubblingEventBlock onKeyUp;
@property (nonatomic, copy) NSArray<RCTHandledKey*> *keyDownEvents;
@property (nonatomic, copy) NSArray<RCTHandledKey*> *keyUpEvents;

Expand Down
70 changes: 19 additions & 51 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -1578,63 +1578,31 @@ - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender

#pragma mark - Keyboard Events

// This dictionary is attached to the NSEvent being handled so we can ensure we only dispatch it
// once per RCTView\nativeTag. The reason we need to track this state is that certain React native
// views such as RCTUITextView inherit from views (such as NSTextView) which may or may not
// decide to bubble the event to the next responder, and we don't want to dispatch the same
// event more than once (e.g. first from RCTUITextView, and then from it's parent RCTView).
NSMutableDictionary<NSNumber *, NSNumber *> *GetEventDispatchStateDictionary(NSEvent *event) {
static const char *key = "RCTEventDispatchStateDictionary";
NSMutableDictionary<NSNumber *, NSNumber *> *dict = objc_getAssociatedObject(event, key);
if (dict == nil) {
dict = [NSMutableDictionary new];
objc_setAssociatedObject(event, key, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return dict;
}
- (BOOL)handleKeyboardEvent:(NSEvent *)event {
RCTAssert(
event.type == NSEventTypeKeyDown ||
event.type == NSEventTypeKeyUp,
@"Keyboard event must be keyDown, keyUp. Got type: %ld", (long)event.type);

- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event shouldBlock:(BOOL *)shouldBlock {
BOOL keyDown = event.type == NSEventTypeKeyDown;
NSArray<RCTHandledKey *> *keyEvents = keyDown ? self.keyDownEvents : self.keyUpEvents;
RCTViewKeyboardEvent *keyboardEvent = [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag];

// If the view is focusable and the component didn't explicity set the keyDownEvents or keyUpEvents,
// allow enter/return and spacebar key events to mimic the behavior of native controls.
if (self.focusable && keyEvents == nil) {
keyEvents = @[
[[RCTHandledKey alloc] initWithKey:@"Enter"],
[[RCTHandledKey alloc] initWithKey:@" "]
];
// Emit the event to JS only once. By default, events, will bubble up the respnder chain
// when we call super, so let's emit the event only at the first responder. It would be
// simpler to check `if (self == self.window.firstResponder), however, that does not account
// for cases like TextInputComponentView, where the first responder may be a subview.
static const char kRCTViewKeyboardEventEmittedKey = 0;
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
BOOL alreadyEmitted = [emitted boolValue];
if (!alreadyEmitted) {
[_eventDispatcher sendEvent:keyboardEvent];
objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// If a view specifies a key, it will always be removed from the responder chain (i.e. "handled")
*shouldBlock = [RCTHandledKey event:event matchesFilter:keyEvents];

// If an event isn't being removed from the queue, we want to be sure we dispatch it
// only once for that view. See note for GetEventDispatchStateDictionary.
if (!*shouldBlock) {
NSNumber *tag = [self reactTag];
NSMutableDictionary<NSNumber *, NSNumber *> *dict = GetEventDispatchStateDictionary(event);
NSArray<RCTHandledKey *> *keyEvents = event.type == NSEventTypeKeyDown ? self.keyDownEvents : self.keyUpEvents;

if ([dict[tag] boolValue]) {
return nil;
}

dict[tag] = @YES;
}
BOOL shouldBlockNativeHandling = [RCTHandledKey event:event matchesFilter:keyEvents];

return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag];
}

- (BOOL)handleKeyboardEvent:(NSEvent *)event {
if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) {
BOOL shouldBlock = YES;
RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock];
if (keyboardEvent) {
[_eventDispatcher sendEvent:keyboardEvent];
return shouldBlock;
}
}
return NO;
return shouldBlockNativeHandling;
}

- (void)keyDown:(NSEvent *)event {
Expand Down
4 changes: 2 additions & 2 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,8 @@ - (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defa
RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragLeave, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(keyDownEvents, NSArray<RCTHandledKey *>)
RCT_EXPORT_VIEW_PROPERTY(keyUpEvents, NSArray<RCTHandledKey *>)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// [macOS]

#include <react/renderer/components/view/HostPlatformViewEventEmitter.h>
#include <react/renderer/components/view/KeyEvent.h>

namespace facebook::react {

Expand All @@ -21,4 +22,32 @@ void HostPlatformViewEventEmitter::onBlur() const {
dispatchEvent("blur");
}

#pragma mark - Keyboard Events

static jsi::Value keyEventPayload(jsi::Runtime& runtime, const KeyEvent& event) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key));
payload.setProperty(runtime, "ctrlKey", event.ctrlKey);
payload.setProperty(runtime, "shiftKey", event.shiftKey);
payload.setProperty(runtime, "altKey", event.altKey);
payload.setProperty(runtime, "metaKey", event.metaKey);
payload.setProperty(runtime, "capsLockKey", event.capsLockKey);
payload.setProperty(runtime, "numericPadKey", event.numericPadKey);
payload.setProperty(runtime, "helpKey", event.helpKey);
payload.setProperty(runtime, "functionKey", event.functionKey);
return payload;
};

void HostPlatformViewEventEmitter::onKeyDown(const KeyEvent& keyEvent) const {
dispatchEvent("keyDown", [keyEvent](jsi::Runtime& runtime) {
return keyEventPayload(runtime, keyEvent);
});
}

void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const {
dispatchEvent("keyUp", [keyEvent](jsi::Runtime& runtime) {
return keyEventPayload(runtime, keyEvent);
});
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#pragma once

#include <react/renderer/components/view/BaseViewEventEmitter.h>
#include <react/renderer/components/view/KeyEvent.h>

namespace facebook::react {

Expand All @@ -21,6 +22,11 @@ class HostPlatformViewEventEmitter : public BaseViewEventEmitter {

void onFocus() const;
void onBlur() const;

#pragma mark - Keyboard Events

void onKeyDown(KeyEvent const &keyEvent) const;
void onKeyUp(KeyEvent const &keyEvent) const;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@

namespace facebook::react {

struct MacOSViewEvents {
struct HostPlatformViewEvents {
std::bitset<64> bits{};

enum class Offset : std::size_t {
// Focus Events
Focus = 0,
Blur = 1,

// Keyboard Events
KeyDown = 2,
KeyUp = 3,
};

constexpr bool operator[](const Offset offset) const {
Expand All @@ -32,27 +36,31 @@ struct MacOSViewEvents {
}
};

inline static bool operator==(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) {
inline static bool operator==(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) {
return lhs.bits == rhs.bits;
}

inline static bool operator!=(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) {
inline static bool operator!=(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) {
return lhs.bits != rhs.bits;
}

static inline MacOSViewEvents convertRawProp(
static inline HostPlatformViewEvents convertRawProp(
const PropsParserContext &context,
const RawProps &rawProps,
const MacOSViewEvents &sourceValue,
const MacOSViewEvents &defaultValue) {
MacOSViewEvents result{};
using Offset = MacOSViewEvents::Offset;
const HostPlatformViewEvents &sourceValue,
const HostPlatformViewEvents &defaultValue) {
HostPlatformViewEvents result{};
using Offset = HostPlatformViewEvents::Offset;

// Focus Events
result[Offset::Focus] =
convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]);
result[Offset::Blur] =
convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]);
result[Offset::KeyDown] =
convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]);
result[Offset::KeyUp] =
convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]);

return result;
}
Expand Down
Loading
Loading