Skip to content

Commit 69fe24f

Browse files
authored
feat(fabric)!: Implement keyboard event handling on View (#2699)
1 parent 8692129 commit 69fe24f

File tree

13 files changed

+601
-159
lines changed

13 files changed

+601
-159
lines changed

packages/react-native/Libraries/Components/View/View.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ const View: component(
5353
importantForAccessibility,
5454
nativeID,
5555
tabIndex,
56-
// [macOS
57-
keyDownEvents,
58-
keyUpEvents,
59-
// macOS]
6056
...otherProps
6157
}: ViewProps,
6258
forwardedRef,

packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ import {ConditionallyIgnoredEventHandlers} from './ViewConfigIgnore';
1818

1919
const bubblingEventTypes = {
2020
...PlatformBaseViewConfigIos.bubblingEventTypes,
21+
topKeyDown: {
22+
phasedRegistrationNames: {
23+
captured: 'onKeyDownCapture',
24+
bubbled: 'onKeyDown',
25+
},
26+
},
27+
topKeyUp: {
28+
phasedRegistrationNames: {
29+
captured: 'onKeyUpCapture',
30+
bubbled: 'onKeyUp',
31+
},
32+
},
2133
};
2234

2335
const directEventTypes = {
@@ -31,12 +43,6 @@ const directEventTypes = {
3143
topDrop: {
3244
registrationName: 'onDrop',
3345
},
34-
topKeyUp: {
35-
registrationName: 'onKeyUp',
36-
},
37-
topKeyDown: {
38-
registrationName: 'onKeyDown',
39-
},
4046
topMouseEnter: {
4147
registrationName: 'onMouseEnter',
4248
},

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
#import <React/RCTBorderDrawing.h>
1717
#import <React/RCTBoxShadow.h>
1818
#import <React/RCTConversions.h>
19-
#import <React/RCTCursor.h> // [macOS]
2019
#import <React/RCTLinearGradient.h>
2120
#import <React/RCTLocalizedString.h>
2221
#import <react/featureflags/ReactNativeFeatureFlags.h>
@@ -30,6 +29,11 @@
3029
#import <React/RCTComponentViewFactory.h>
3130
#endif
3231

32+
#if TARGET_OS_OSX // [macOS
33+
#import <React/RCTCursor.h>
34+
#import <React/RCTViewKeyboardEvent.h>
35+
#endif // macOS]
36+
3337
using namespace facebook::react;
3438

3539
const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
@@ -1597,7 +1601,61 @@ - (BOOL)resignFirstResponder
15971601

15981602
return YES;
15991603
}
1600-
1604+
1605+
#pragma mark - Keyboard Events
1606+
1607+
- (BOOL)handleKeyboardEvent:(NSEvent *)event {
1608+
RCTAssert(
1609+
event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp,
1610+
@"Keyboard event must be keyDown, keyUp. Got type: %ld", (long)event.type);
1611+
1612+
// Convert the event to a KeyEvent
1613+
NSEventModifierFlags modifierFlags = event.modifierFlags;
1614+
facebook::react::KeyEvent keyEvent = {
1615+
.key = [[RCTViewKeyboardEvent keyFromEvent:event] UTF8String],
1616+
.altKey = static_cast<bool>(modifierFlags & NSEventModifierFlagOption),
1617+
.ctrlKey = static_cast<bool>(modifierFlags & NSEventModifierFlagControl),
1618+
.shiftKey = static_cast<bool>(modifierFlags & NSEventModifierFlagShift),
1619+
.metaKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCommand),
1620+
.capsLockKey = static_cast<bool>(modifierFlags & NSEventModifierFlagCapsLock),
1621+
.numericPadKey = static_cast<bool>(modifierFlags & NSEventModifierFlagNumericPad),
1622+
.helpKey = static_cast<bool>(modifierFlags & NSEventModifierFlagHelp),
1623+
.functionKey = static_cast<bool>(modifierFlags & NSEventModifierFlagFunction),
1624+
};
1625+
1626+
// Emit the event to JS only once. By default, events, will bubble up the respnder chain
1627+
// when we call super, so let's emit the event only at the first responder. It would be
1628+
// simpler to check `if (self == self.window.firstResponder), however, that does not account
1629+
// for cases like TextInputComponentView, where the first responder may be a subview.
1630+
static const char kRCTViewKeyboardEventEmittedKey = 0;
1631+
NSNumber *emitted = objc_getAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey);
1632+
BOOL alreadyEmitted = [emitted boolValue];
1633+
if (!alreadyEmitted) {
1634+
if (event.type == NSEventTypeKeyDown) {
1635+
_eventEmitter->onKeyDown(keyEvent);
1636+
} else {
1637+
_eventEmitter->onKeyUp(keyEvent);
1638+
}
1639+
objc_setAssociatedObject(event, &kRCTViewKeyboardEventEmittedKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1640+
}
1641+
1642+
// If keyDownEvents or keyUpEvents specifies the event, block native handling of the event
1643+
auto const& handledKeyEvents = event.type == NSEventTypeKeyDown ? _props->keyDownEvents : _props->keyUpEvents;
1644+
return std::find(handledKeyEvents.cbegin(), handledKeyEvents.cend(), keyEvent) != handledKeyEvents.cend();
1645+
}
1646+
1647+
- (void)keyDown:(NSEvent *)event {
1648+
if (![self handleKeyboardEvent:event]) {
1649+
[super keyDown:event];
1650+
}
1651+
}
1652+
1653+
- (void)keyUp:(NSEvent *)event {
1654+
if (![self handleKeyboardEvent:event]) {
1655+
[super keyUp:event];
1656+
}
1657+
}
1658+
16011659
#endif // macOS]
16021660

16031661
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point

packages/react-native/React/Views/RCTView.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
173173
// NOTE does not properly work with single line text inputs (most key downs). This is because those are
174174
// presumably handled by the window's field editor. To make it work, we'd need to look into providing
175175
// a custom field editor for NSTextField controls.
176-
@property (nonatomic, copy) RCTDirectEventBlock onKeyDown;
177-
@property (nonatomic, copy) RCTDirectEventBlock onKeyUp;
176+
@property (nonatomic, copy) RCTBubblingEventBlock onKeyDown;
177+
@property (nonatomic, copy) RCTBubblingEventBlock onKeyUp;
178178
@property (nonatomic, copy) NSArray<RCTHandledKey*> *keyDownEvents;
179179
@property (nonatomic, copy) NSArray<RCTHandledKey*> *keyUpEvents;
180180

packages/react-native/React/Views/RCTView.m

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,63 +1578,31 @@ - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
15781578

15791579
#pragma mark - Keyboard Events
15801580

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

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

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

1609-
// If a view specifies a key, it will always be removed from the responder chain (i.e. "handled")
1610-
*shouldBlock = [RCTHandledKey event:event matchesFilter:keyEvents];
1611-
1612-
// If an event isn't being removed from the queue, we want to be sure we dispatch it
1613-
// only once for that view. See note for GetEventDispatchStateDictionary.
1614-
if (!*shouldBlock) {
1615-
NSNumber *tag = [self reactTag];
1616-
NSMutableDictionary<NSNumber *, NSNumber *> *dict = GetEventDispatchStateDictionary(event);
1601+
NSArray<RCTHandledKey *> *keyEvents = event.type == NSEventTypeKeyDown ? self.keyDownEvents : self.keyUpEvents;
16171602

1618-
if ([dict[tag] boolValue]) {
1619-
return nil;
1620-
}
1621-
1622-
dict[tag] = @YES;
1623-
}
1603+
BOOL shouldBlockNativeHandling = [RCTHandledKey event:event matchesFilter:keyEvents];
16241604

1625-
return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag];
1626-
}
1627-
1628-
- (BOOL)handleKeyboardEvent:(NSEvent *)event {
1629-
if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) {
1630-
BOOL shouldBlock = YES;
1631-
RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock];
1632-
if (keyboardEvent) {
1633-
[_eventDispatcher sendEvent:keyboardEvent];
1634-
return shouldBlock;
1635-
}
1636-
}
1637-
return NO;
1605+
return shouldBlockNativeHandling;
16381606
}
16391607

16401608
- (void)keyDown:(NSEvent *)event {

packages/react-native/React/Views/RCTViewManager.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -646,8 +646,8 @@ - (void) updateAccessibilityRole:(RCTView *)view withDefaultView:(RCTView *)defa
646646
RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock)
647647
RCT_EXPORT_VIEW_PROPERTY(onDragLeave, RCTDirectEventBlock)
648648
RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock)
649-
RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock)
650-
RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock)
649+
RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTBubblingEventBlock)
650+
RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTBubblingEventBlock)
651651
RCT_EXPORT_VIEW_PROPERTY(keyDownEvents, NSArray<RCTHandledKey *>)
652652
RCT_EXPORT_VIEW_PROPERTY(keyUpEvents, NSArray<RCTHandledKey *>)
653653

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// [macOS]
99

1010
#include <react/renderer/components/view/HostPlatformViewEventEmitter.h>
11+
#include <react/renderer/components/view/KeyEvent.h>
1112

1213
namespace facebook::react {
1314

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

25+
#pragma mark - Keyboard Events
26+
27+
static jsi::Value keyEventPayload(jsi::Runtime& runtime, const KeyEvent& event) {
28+
auto payload = jsi::Object(runtime);
29+
payload.setProperty(runtime, "key", jsi::String::createFromUtf8(runtime, event.key));
30+
payload.setProperty(runtime, "ctrlKey", event.ctrlKey);
31+
payload.setProperty(runtime, "shiftKey", event.shiftKey);
32+
payload.setProperty(runtime, "altKey", event.altKey);
33+
payload.setProperty(runtime, "metaKey", event.metaKey);
34+
payload.setProperty(runtime, "capsLockKey", event.capsLockKey);
35+
payload.setProperty(runtime, "numericPadKey", event.numericPadKey);
36+
payload.setProperty(runtime, "helpKey", event.helpKey);
37+
payload.setProperty(runtime, "functionKey", event.functionKey);
38+
return payload;
39+
};
40+
41+
void HostPlatformViewEventEmitter::onKeyDown(const KeyEvent& keyEvent) const {
42+
dispatchEvent("keyDown", [keyEvent](jsi::Runtime& runtime) {
43+
return keyEventPayload(runtime, keyEvent);
44+
});
45+
}
46+
47+
void HostPlatformViewEventEmitter::onKeyUp(const KeyEvent& keyEvent) const {
48+
dispatchEvent("keyUp", [keyEvent](jsi::Runtime& runtime) {
49+
return keyEventPayload(runtime, keyEvent);
50+
});
51+
}
52+
2453
} // namespace facebook::react

packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#pragma once
1111

1212
#include <react/renderer/components/view/BaseViewEventEmitter.h>
13+
#include <react/renderer/components/view/KeyEvent.h>
1314

1415
namespace facebook::react {
1516

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

2223
void onFocus() const;
2324
void onBlur() const;
25+
26+
#pragma mark - Keyboard Events
27+
28+
void onKeyDown(KeyEvent const &keyEvent) const;
29+
void onKeyUp(KeyEvent const &keyEvent) const;
2430
};
2531

2632
} // namespace facebook::react
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414

1515
namespace facebook::react {
1616

17-
struct MacOSViewEvents {
17+
struct HostPlatformViewEvents {
1818
std::bitset<64> bits{};
1919

2020
enum class Offset : std::size_t {
2121
// Focus Events
2222
Focus = 0,
2323
Blur = 1,
24+
25+
// Keyboard Events
26+
KeyDown = 2,
27+
KeyUp = 3,
2428
};
2529

2630
constexpr bool operator[](const Offset offset) const {
@@ -32,27 +36,31 @@ struct MacOSViewEvents {
3236
}
3337
};
3438

35-
inline static bool operator==(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) {
39+
inline static bool operator==(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) {
3640
return lhs.bits == rhs.bits;
3741
}
3842

39-
inline static bool operator!=(MacOSViewEvents const &lhs, MacOSViewEvents const &rhs) {
43+
inline static bool operator!=(HostPlatformViewEvents const &lhs, HostPlatformViewEvents const &rhs) {
4044
return lhs.bits != rhs.bits;
4145
}
4246

43-
static inline MacOSViewEvents convertRawProp(
47+
static inline HostPlatformViewEvents convertRawProp(
4448
const PropsParserContext &context,
4549
const RawProps &rawProps,
46-
const MacOSViewEvents &sourceValue,
47-
const MacOSViewEvents &defaultValue) {
48-
MacOSViewEvents result{};
49-
using Offset = MacOSViewEvents::Offset;
50+
const HostPlatformViewEvents &sourceValue,
51+
const HostPlatformViewEvents &defaultValue) {
52+
HostPlatformViewEvents result{};
53+
using Offset = HostPlatformViewEvents::Offset;
5054

5155
// Focus Events
5256
result[Offset::Focus] =
5357
convertRawProp(context, rawProps, "onFocus", sourceValue[Offset::Focus], defaultValue[Offset::Focus]);
5458
result[Offset::Blur] =
5559
convertRawProp(context, rawProps, "onBlur", sourceValue[Offset::Blur], defaultValue[Offset::Blur]);
60+
result[Offset::KeyDown] =
61+
convertRawProp(context, rawProps, "onKeyDown", sourceValue[Offset::KeyDown], defaultValue[Offset::KeyDown]);
62+
result[Offset::KeyUp] =
63+
convertRawProp(context, rawProps, "onKeyUp", sourceValue[Offset::KeyUp], defaultValue[Offset::KeyUp]);
5664

5765
return result;
5866
}

0 commit comments

Comments
 (0)