diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index c5d5284b636fbb..4f13e1ec49db7d 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -53,10 +53,6 @@ const View: component( importantForAccessibility, nativeID, tabIndex, - // [macOS - keyDownEvents, - keyUpEvents, - // macOS] ...otherProps }: ViewProps, forwardedRef, diff --git a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js index 75a200842e8bb7..84f5db15f92a37 100644 --- a/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/packages/react-native/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -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 = { @@ -31,12 +43,6 @@ const directEventTypes = { topDrop: { registrationName: 'onDrop', }, - topKeyUp: { - registrationName: 'onKeyUp', - }, - topKeyDown: { - registrationName: 'onKeyDown', - }, topMouseEnter: { registrationName: 'onMouseEnter', }, diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 9e841ba2051c3e..4f264cfaa0ad5d 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -16,7 +16,6 @@ #import #import #import -#import // [macOS] #import #import #import @@ -30,6 +29,11 @@ #import #endif +#if TARGET_OS_OSX // [macOS +#import +#import +#endif // macOS] + using namespace facebook::react; const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f; @@ -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(modifierFlags & NSEventModifierFlagOption), + .ctrlKey = static_cast(modifierFlags & NSEventModifierFlagControl), + .shiftKey = static_cast(modifierFlags & NSEventModifierFlagShift), + .metaKey = static_cast(modifierFlags & NSEventModifierFlagCommand), + .capsLockKey = static_cast(modifierFlags & NSEventModifierFlagCapsLock), + .numericPadKey = static_cast(modifierFlags & NSEventModifierFlagNumericPad), + .helpKey = static_cast(modifierFlags & NSEventModifierFlagHelp), + .functionKey = static_cast(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 diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index 7b47a846b5b4d9..969174624ac94e 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -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 *keyDownEvents; @property (nonatomic, copy) NSArray *keyUpEvents; diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 1546f17d7b8d55..1f1232106e77df 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -1578,63 +1578,31 @@ - (BOOL)performDragOperation:(id )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 *GetEventDispatchStateDictionary(NSEvent *event) { - static const char *key = "RCTEventDispatchStateDictionary"; - NSMutableDictionary *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 *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 *dict = GetEventDispatchStateDictionary(event); + NSArray *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 { diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index 761d8450e2661e..1181a95ad97374 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -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) RCT_EXPORT_VIEW_PROPERTY(keyUpEvents, NSArray) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp index 6ad5d85108777b..c56760a7cec0ac 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.cpp @@ -8,6 +8,7 @@ // [macOS] #include +#include namespace facebook::react { @@ -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 diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h index 132f1563e5a8ea..ba1e0d8c10611b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEventEmitter.h @@ -10,6 +10,7 @@ #pragma once #include +#include namespace facebook::react { @@ -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 \ No newline at end of file diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h similarity index 59% rename from packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h rename to packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h index 8657de54c77ed6..b9164ad8828f89 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/MacOSViewEvents.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewEvents.h @@ -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 { @@ -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; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp index ad224e03eb1c30..d9adb5325be877 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.cpp @@ -22,13 +22,13 @@ HostPlatformViewProps::HostPlatformViewProps( const HostPlatformViewProps& sourceProps, const RawProps& rawProps) : BaseViewProps(context, sourceProps, rawProps), - macOSViewEvents( + hostPlatformEvents( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() - ? sourceProps.macOSViewEvents + ? sourceProps.hostPlatformEvents : convertRawProp( context, rawProps, - sourceProps.macOSViewEvents, + sourceProps.hostPlatformEvents, {})), focusable( ReactNativeFeatureFlags::enableCppPropsIteratorSetter() @@ -47,19 +47,37 @@ HostPlatformViewProps::HostPlatformViewProps( rawProps, "enableFocusRing", sourceProps.enableFocusRing, + {})), + keyDownEvents( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.keyDownEvents + : convertRawProp( + context, + rawProps, + "keyDownEvents", + sourceProps.keyDownEvents, + {})), + keyUpEvents( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.keyUpEvents + : convertRawProp( + context, + rawProps, + "keyUpEvents", + sourceProps.keyUpEvents, {})) {} -#define MACOS_VIEW_EVENT_CASE(eventType) \ -case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ - const auto offset = MacOSViewEvents::Offset::eventType; \ - MacOSViewEvents defaultViewEvents{}; \ - bool res = defaultViewEvents[offset]; \ - if (value.hasValue()) { \ - fromRawValue(context, value, res); \ - } \ - macOSViewEvents[offset] = res; \ - return; \ -} +#define VIEW_EVENT_CASE_MACOS(eventType) \ + case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ + const auto offset = HostPlatformViewEvents::Offset::eventType; \ + HostPlatformViewEvents defaultViewEvents{}; \ + bool res = defaultViewEvents[offset]; \ + if (value.hasValue()) { \ + fromRawValue(context, value, res); \ + } \ + hostPlatformEvents[offset] = res; \ + return; \ + } void HostPlatformViewProps::setProp( const PropsParserContext& context, @@ -74,11 +92,14 @@ void HostPlatformViewProps::setProp( static auto defaults = HostPlatformViewProps{}; switch (hash) { + VIEW_EVENT_CASE_MACOS(Focus); + VIEW_EVENT_CASE_MACOS(Blur); + VIEW_EVENT_CASE_MACOS(KeyDown); + VIEW_EVENT_CASE_MACOS(KeyUp); RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); RAW_SET_PROP_SWITCH_CASE_BASIC(enableFocusRing); - MACOS_VIEW_EVENT_CASE(Focus); - MACOS_VIEW_EVENT_CASE(Blur); - + RAW_SET_PROP_SWITCH_CASE_BASIC(keyDownEvents); + RAW_SET_PROP_SWITCH_CASE_BASIC(keyUpEvents); } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h index b60a690aa5eb35..f2e4424f8818f4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/HostPlatformViewProps.h @@ -9,12 +9,14 @@ #pragma once -#include -#include #include #include +#include +#include + +#include -#include "MacOSViewEvents.h" +#include "HostPlatformViewEvents.h" namespace facebook::react { @@ -32,12 +34,14 @@ class HostPlatformViewProps : public BaseViewProps { const char* propName, const RawValue& value); - MacOSViewEvents macOSViewEvents{}; + HostPlatformViewEvents hostPlatformEvents{}; #pragma mark - Props bool focusable{false}; bool enableFocusRing{true}; + std::vector keyDownEvents{}; + std::vector keyUpEvents{}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h new file mode 100644 index 00000000000000..213e64eca68f07 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/macos/react/renderer/components/view/KeyEvent.h @@ -0,0 +1,137 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + // [macOS] + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +/* + * Describes a request to handle a key input. + */ +struct HandledKey { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + std::optional altKey{}; + + /* + * A flag indicating if the control key is pressed. + */ + std::optional ctrlKey{}; + + /* + * A flag indicating if the shift key is pressed. + */ + std::optional shiftKey{}; + + /* + * A flag indicating if the meta key is pressed. + */ + std::optional metaKey{}; +}; + +inline static bool operator==(const HandledKey& lhs, const HandledKey& rhs) { + return lhs.key == rhs.key && lhs.altKey == rhs.altKey && + lhs.ctrlKey == rhs.ctrlKey && lhs.shiftKey == rhs.shiftKey && + lhs.metaKey == rhs.metaKey; +} + +/** + * Key event emitted by handled key events. + */ +struct KeyEvent { + /** + * The key for the event aligned to https://www.w3.org/TR/uievents-key/. + */ + std::string key{}; + + /* + * A flag indicating if the alt key is pressed. + */ + bool altKey{false}; + + /* + * A flag indicating if the control key is pressed. + */ + bool ctrlKey{false}; + + /* + * A flag indicating if the shift key is pressed. + */ + bool shiftKey{false}; + + /* + * A flag indicating if the meta key is pressed. + */ + bool metaKey{false}; + + /* + * A flag indicating if the caps lock key is pressed. + */ + bool capsLockKey{false}; + + /* + * A flag indicating if the key on the numeric pad is pressed. + */ + bool numericPadKey{false}; + + /* + * A flag indicating if the help key is pressed. + */ + bool helpKey{false}; + + /* + * A flag indicating if a function key is pressed. + */ + bool functionKey{false}; +}; + +inline static bool operator==(const KeyEvent& lhs, const HandledKey& rhs) { + return lhs.key == rhs.key && + (!rhs.altKey.has_value() || lhs.altKey == *rhs.altKey) && + (!rhs.ctrlKey.has_value() || lhs.ctrlKey == *rhs.ctrlKey) && + (!rhs.shiftKey.has_value() || lhs.shiftKey == *rhs.shiftKey) && + (!rhs.metaKey.has_value() || lhs.metaKey == *rhs.metaKey); +} + +inline void fromRawValue( + const PropsParserContext& context, + const RawValue& value, + HandledKey& result) { + if (value.hasType>()) { + auto map = static_cast>(value); + for (const auto& pair : map) { + if (pair.first == "key") { + result.key = static_cast(pair.second); + } else if (pair.first == "altKey") { + result.altKey = static_cast(pair.second); + } else if (pair.first == "ctrlKey") { + result.ctrlKey = static_cast(pair.second); + } else if (pair.first == "shiftKey") { + result.shiftKey = static_cast(pair.second); + } else if (pair.first == "metaKey") { + result.metaKey = static_cast(pair.second); + } + } + } else if (value.hasType()) { + result.key = static_cast(value); + } +} + +} // namespace facebook::react diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 8cd1a11adf7f53..4726f5706b5918 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -12,14 +12,124 @@ import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; -const React = require('react'); -const ReactNative = require('react-native'); +import * as React from 'react'; -const {Button, ScrollView, StyleSheet, Text, TextInput, View} = ReactNative; +import { + Button, + Pressable, + StyleSheet, + Switch, + Text, + TextInput, + View, +} from 'react-native'; -function KeyEventExample(): React.Node { - // $FlowFixMe[missing-empty-array-annot] - const [log, setLog] = React.useState([]); +function BubblingExample(): React.Node { + const ref = React.useRef | null>(null); + const [eventLog, setEventLog] = React.useState>([]); + const [stopPropagationEnabled, setStopPropagationEnabled] = + React.useState(false); + + function appendEvent(eventName: string, source?: string) { + const limit = 12; + setEventLog((current: Array) => { + const prefix = source != null ? `${source}: ` : ''; + return [`${prefix}${eventName}`].concat(current.slice(0, limit - 1)); + }); + } + + return ( + + + Stop Propagation in Box 2: + setStopPropagationEnabled(value)} + /> + + { + appendEvent(`keyDown: ${ev.nativeEvent.key}`, 'Box 3'); + }}> + { + appendEvent( + `keyDown: ${ev.nativeEvent.key}`, + 'Box 2 keyDownEvents=[f,g]', + ); + if (stopPropagationEnabled) { + ev.stopPropagation(); + appendEvent('stopPropagation called', 'Box 2'); + } + }} + keyDownEvents={[{key: 'f'}, {key: 'g'}]}> + { + appendEvent(`keyDown: ${ev.nativeEvent.key}`, 'Box 1'); + }}> + + [ + styles.focusBox, + pressed && styles.focusBoxPressed, + ]} + nativeID="keyboard_events_focusable" + focusable={true} + onPress={() => { + ref.current?.focus(); + }} + onKeyDown={ev => { + appendEvent(`keyDown: ${ev.nativeEvent.key}`, 'Focusable'); + if ( + ev.nativeEvent.key === 'k' && + ev.nativeEvent.metaKey === true + ) { + appendEvent('Key command: Clear event log', 'Focusable'); + setTimeout(() => { + setEventLog([]); + }, 0); + } + }}> + Click to Focus + + + + + + + + + Event Log + [ + styles.clearButton, + pressed && styles.clearButtonPressed, + ]} + onPress={() => setEventLog([])}> + Clear + + + {eventLog.map((e, ii) => ( + + {e} + + ))} + + + ); +} + +function KeyboardEventExample(): React.Node { + const viewRef = React.useRef | null>(null); + const [log, setLog] = React.useState>([]); const clearLog = React.useCallback(() => { setLog([]); @@ -28,7 +138,7 @@ function KeyEventExample(): React.Node { const appendLog = React.useCallback( (line: string) => { const limit = 12; - let newLog = log.slice(0, limit - 1); + const newLog = log.slice(0, limit - 1); newLog.unshift(line); setLog(newLog); }, @@ -83,66 +193,159 @@ function KeyEventExample(): React.Node { onKeyUp: handleKeyUp, }; + React.useEffect(() => { + // Focus the first view on mount + viewRef.current?.focus(); + }, []); + return ( - - - - Key events are called when a component detects a key press.To tab - between views on macOS: Enable System Preferences / Keyboard / - Shortcuts > Use keyboard navigation to move focus between controls. - - - {viewText} - - {textInputText} - - - {textInputUnhandledText} - - -