From e361b577bfc3e38b867d2aa8cb667c57ddd0be86 Mon Sep 17 00:00:00 2001 From: Navneet Kambo Date: Mon, 3 Jul 2023 08:59:02 -0700 Subject: [PATCH 1/4] [Key handling] pass through all keys; allow specifying modifiers for validKeys[Down|Up] There are scenarios where it might be necessary to look at the incoming events without removing from the system queue. Currently that's impossible today on React Native macOS, since views are required to specify `validKeysDown` or `validKeysUp`, and such events are always removed from the queue. To mitigate, let's add a new `passthroughAllKeyEvents` prop to `RCTView`. We could keep it forever (towards an interest to reduce event spam from native to JS), or we could use it towards the path to making it the default behavior (stage 1: default false, i.e. opt in, stage 2: default true, i.e. opt out, stage 3: remove, is default behavior). - React/Views/RCTView.h - React/Views/RCTView.m - React/Views/RCTViewManager.m Note that this doesn't properly work with `RCTUITextField` (i.e. single line text fields). From what I can tell, that would need us to possibly provide a custom field editor for the window. I am scoping this out for this PR. Another peculiarity to note is regarding `RCTUITextView` (i.e. multi line text fields). Here, it looks like the text view itself isn't exposed to the JS (this view doesn't have a `nativeTag`), so there's a `RCTView` holding a child `RCTUITextView` where the former dispatches events to JS on behalf for the latter. The reason this matters (specifically for "pass through" events) is because the latter can dispatch certain events to the JS, and then depending on the super class implementation (`NSTextView`), it may or may not *also* pass the `NSEvent` to the next responder (i.e. parent view, i.e. `RCTView`). Passing the action to the next responder *can* cause us to send duplicate JS events for the same `NSEvent`. I couldn't find anything in macOS APIs to determine if the view the event was generated for is a specific view, so I am introducing a book-keeping mechanism to not send duplicate events. Introduce `RCTHandledKey` for specifying modifiers for `validKeysDown` and `validKeysUp`. Behavior noted in type definitions. - Libraries/Text/TextInput/RCTBaseTextInputView.m - React/Base/RCTConvert.h - React/Base/RCTConvert.m - React/Views/RCTHandledKey.h - React/Views/RCTHandledKey.m - React/Views/RCTView.h - React/Views/RCTView.m - React/Views/RCTViewKeyboardEvent.m - React/Views/RCTViewManager.m - React/Views/ScrollView/RCTScrollView.m macOS *usually* does things on key down (as opposed to, say, Win32, which seems to *usually* does things on key up). Like `RCTUITextField`, passs `performKeyEquivalent:` to `textInputDelegate` so we can handle the alternate `keyDown:` path (e.g. Cmd+A). This will be needed for properly handling keystrokes that go through said alternate path. There are probably several other selectors that also need implementing (`deleteBackward:`) to full pass through every possible key, but I am leaving that for some other time. - Libraries/Text/TextInput/Multiline/RCTUITextView.m Make a totally unrelated fix to `RCTSwitch`. In a test page where I added an on-by-default switch, I noticed the first toggle (ON->OFF) doesn't do anything. The second toggle (OFF->ON) then doesn't (expectedly) do anything. Found wrong behavior on the switch test page -- tempted to instead remove `wasOn`, but for now repeating the pattern in `setOn:animated:` - React/Views/RCTSwitch.m Flow stuff. `passthroughAllKeyEvents` is now a valid thing to pass to `View` types. - Libraries/Components/View/ReactNativeViewAttributes.js - Libraries/Components/View/ViewPropTypes.js - Libraries/NativeComponent/BaseViewConfig.macos.js Update signatures for `validKeysDown` and `validKeysUp` - Libraries/Components/View/ViewPropTypes.js Remove duplicated specifications on `Pressable`. Just use the one from `View`. As a benefit, future changes allow us to not have to touch `Pressable` anymore. - Libraries/Components/Pressable/Pressable.js - Libraries/Components/View/ViewPropTypes.js Update test pages with `passthoughAllKeyEvents` and the keyboard events page with an example modifier usage. - packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js - packages/rn-tester/js/examples/TextInput/TextInputSharedExamples.js Testing: * Using the keyboard events test page, validate "pass through" of all events for simple view, single line text input, multi line text input. Sanity test existing (non-"pass through") behavior. * Using the text input test page, ordering of `keyDown` and `keyUp` events w.r.t. other events (such as `keyPress` -- which isn't dispatched for every key) * Using the switch test page, sanity test switch behaviors --- Libraries/Components/Pressable/Pressable.js | 24 +- .../View/ReactNativeViewAttributes.js | 1 + Libraries/Components/View/ViewPropTypes.js | 52 +++- .../NativeComponent/BaseViewConfig.macos.js | 1 + .../Text/TextInput/Multiline/RCTUITextView.m | 8 + .../Text/TextInput/RCTBaseTextInputView.m | 4 +- React/Base/RCTConvert.h | 4 + React/Base/RCTConvert.m | 4 + React/Views/RCTHandledKey.h | 37 +++ React/Views/RCTHandledKey.m | 136 +++++++++ React/Views/RCTSwitch.m | 8 + React/Views/RCTView.h | 10 +- React/Views/RCTView.m | 59 +++- React/Views/RCTViewKeyboardEvent.m | 19 +- React/Views/RCTViewManager.m | 5 +- React/Views/ScrollView/RCTScrollView.m | 6 +- .../KeyboardEventsExample.js | 278 +++++++++++++----- .../TextInput/TextInputSharedExamples.js | 67 ++++- 18 files changed, 592 insertions(+), 131 deletions(-) create mode 100644 React/Views/RCTHandledKey.h create mode 100644 React/Views/RCTHandledKey.m diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index 5a7566e106fe25..4da1f724c38f4e 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -12,7 +12,6 @@ import type { BlurEvent, // [macOS FocusEvent, - KeyEvent, LayoutEvent, MouseEvent, PressEvent, @@ -26,6 +25,7 @@ import type { AccessibilityState, AccessibilityValue, } from '../View/ViewAccessibility'; +import type {KeyboardEventProps} from '../View/ViewPropTypes'; // [macOS] import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; @@ -174,27 +174,7 @@ type Props = $ReadOnly<{| */ onBlur?: ?(event: BlurEvent) => void, - /** - * Called after a key down event is detected. - */ - onKeyDown?: ?(event: KeyEvent) => void, - - /** - * Called after a key up event is detected. - */ - onKeyUp?: ?(event: KeyEvent) => void, - - /** - * Array of keys to receive key down events for - * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", - */ - validKeysDown?: ?Array, - - /** - * Array of keys to receive key up events for - * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", - */ - validKeysUp?: ?Array, + ...KeyboardEventProps, // [macOS] /** * Specifies whether the view should receive the mouse down event when the diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 64e6adc28ea3fa..267b74332ed360 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -47,6 +47,7 @@ const UIView = { onDrop: true, onKeyDown: true, onKeyUp: true, + passthroughAllKeyEvents: true, validKeysDown: true, validKeysUp: true, draggedTypes: true, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 0ae5b364f9de1f..fd0f6d15b6d9ac 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -100,22 +100,64 @@ type DirectEventProps = $ReadOnly<{| |}>; // [macOS -type KeyboardEventProps = $ReadOnly<{| +/** + * Represents a key that could be passed to `validKeysDown` and `validKeysUp`. + * + * `key` is the actual key, such as "a", or one of the special values: + * "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + * "Backspace", "Delete", "Home", "End", "PageUp", "PageDown". + * + * The rest are modifiers that when absent mean don't care (either state -- true or false -- + * matches), and when present require that modifier state to match. All present modifers must + * match for the event to pass the filter. + * + * @platform macos + */ +export type HandledKey = $ReadOnly<{| + key: string, + capsLock?: ?boolean, + shift?: ?boolean, + ctrl?: ?boolean, + alt?: ?boolean, + meta?: ?boolean, + numericPad?: ?boolean, + help?: ?boolean, + function?: ?boolean, +|}>; + +export type KeyboardEventProps = $ReadOnly<{| + /** + * Called after a key down event is detected. + */ onKeyDown?: ?(event: KeyEvent) => void, + + /** + * Called after a key up event is detected. + */ onKeyUp?: ?(event: KeyEvent) => void, + + /** + * When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in + * `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp` + * are still removed from the event queue, but the others are not. + * + * @platform macos + */ + passthroughAllKeyEvents?: ?boolean, + /** - * Array of keys to receive key down events for + * Array of keys to receive key down events for. These events are always removed from the system event queue. * * @platform macos */ - validKeysDown?: ?Array, + validKeysDown?: ?Array, /** - * Array of keys to receive key up events for + * Array of keys to receive key up events for. These events are always removed from the system event queue. * * @platform macos */ - validKeysUp?: ?Array, + validKeysUp?: ?Array, |}>; // macOS] diff --git a/Libraries/NativeComponent/BaseViewConfig.macos.js b/Libraries/NativeComponent/BaseViewConfig.macos.js index 3040f72245b909..fd71051f0ce638 100644 --- a/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -52,6 +52,7 @@ const validAttributesForNonEventProps = { draggedTypes: true, enableFocusRing: true, tooltip: true, + passthroughAllKeyEvents: true, validKeysDown: true, validKeysUp: true, mouseDownCanMoveWindow: true, diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index faf0c342efe051..c202dcfcd1a894 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -555,6 +555,14 @@ - (void)deleteBackward { } } #else // [macOS +- (BOOL)performKeyEquivalent:(NSEvent *)event { + if (!self.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + return YES; + } + + return [super performKeyEquivalent:event]; +} + - (void)keyDown:(NSEvent *)event { // If has marked text, handle by native and return // Do this check before textInputShouldHandleKeyEvent as that one attempts to send the event to JS diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 58a662056ac4d8..b06feb30d9af8b 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -21,6 +21,7 @@ #import #import // [macOS] #import "../RCTTextUIKit.h" // [macOS] +#import "RCTHandledKey.h" // [macOS] @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; @@ -668,7 +669,8 @@ - (BOOL)textInputShouldHandleDeleteForward:(__unused id)sender { } - (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key { - return [self.validKeysDown containsObject:key] || [self.validKeysUp containsObject:key]; + return [RCTHandledKey key:key matchesFilter:self.validKeysDown] + || [RCTHandledKey key:key matchesFilter:self.validKeysUp]; } - (NSDragOperation)textInputDraggingEntered:(id)draggingInfo diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 2764540b180db1..994a8cce40646f 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -20,6 +20,8 @@ #import #endif +@class RCTHandledKey; // [macOS] + /** * This class provides a collection of conversion functions for mapping * JSON objects to native types and classes. These are useful when writing @@ -147,6 +149,8 @@ typedef BOOL css_backface_visibility_t; #if TARGET_OS_OSX // [macOS + (NSString *)accessibilityRoleFromTraits:(id)json; + ++ (NSArray *)RCTHandledKeyArray:(id)json; #endif // macOS] @end diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index c5ba2de0564cf7..7d0442e30f7e75 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -12,6 +12,7 @@ #import #import "RCTDefines.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTImageSource.h" #import "RCTParserUtils.h" #import "RCTUtils.h" @@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json } return NSAccessibilityUnknownRole; } + +RCT_JSON_ARRAY_CONVERTER(RCTHandledKey); + #endif // macOS] @end diff --git a/React/Views/RCTHandledKey.h b/React/Views/RCTHandledKey.h new file mode 100644 index 00000000000000..e9eeec0078cfd1 --- /dev/null +++ b/React/Views/RCTHandledKey.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#if TARGET_OS_OSX // [macOS +#import + +@interface RCTHandledKey : NSObject + ++ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray *)filter; ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)filter; + +- (instancetype)initWithKey:(NSString *)key; +- (BOOL)matchesEvent:(NSEvent *)event; + +@property (nonatomic, copy) NSString *key; +@property (nonatomic, assign) NSNumber *capsLock; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *shift; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *ctrl; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *alt; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *meta; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *numericPad; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *help; // boolean; nil == don't care +@property (nonatomic, assign) NSNumber *function; // boolean; nil == don't care + +@end + +@interface RCTConvert (RCTHandledKey) + ++ (RCTHandledKey *)RCTHandledKey:(id)json; + +@end + +#endif // macOS] diff --git a/React/Views/RCTHandledKey.m b/React/Views/RCTHandledKey.m new file mode 100644 index 00000000000000..3418e68ea0ced0 --- /dev/null +++ b/React/Views/RCTHandledKey.m @@ -0,0 +1,136 @@ +/* + * 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. + */ + +#import "objc/runtime.h" +#import +#import +#import +#import +#import + +#if TARGET_OS_OSX // [macOS + +@implementation RCTHandledKey + ++ (NSArray *)validModifiers { + // keep in sync with actual properties and RCTViewKeyboardEvent + return @[@"capsLock", @"shift", @"ctrl", @"alt", @"meta", @"numericPad", @"help", @"function"]; +} + ++ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray *)filter { + for (RCTHandledKey *key in filter) { + if ([key matchesEvent:event]) { + return YES; + } + } + + return NO; +} + ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)filter { + for (RCTHandledKey *aKey in filter) { + if ([[aKey key] isEqualToString:key]) { + return YES; + } + } + + return NO; +} + +- (instancetype)initWithKey:(NSString *)key { + if ((self = [super init])) { + self.key = key; + } + return self; +} + +- (BOOL)matchesEvent:(NSEvent *)event +{ + NSEventType type = [event type]; + if (type != NSEventTypeKeyDown && type != NSEventTypeKeyUp) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Wrong event type (%d) sent to -[RCTHandledKey matchesEvent:]", (int)type])); + return NO; + } + + NSDictionary *body = [RCTViewKeyboardEvent bodyFromEvent:event]; + if (![body[@"key"] isEqualToString:self.key]) { + return NO; + } + + NSArray *modifiers = [RCTHandledKey validModifiers]; + for (NSString *modifier in modifiers) { + NSString *modifierKey = [modifier stringByAppendingString:@"Key"]; + NSNumber *myValue = [self valueForKey:modifier]; + + if (myValue == nil) { + continue; + } + + NSNumber *eventValue = (NSNumber *)body[modifierKey]; + if (eventValue == nil) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has missing value for %@", modifierKey])); + return NO; + } + + if (![eventValue isKindOfClass:[NSNumber class]]) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has unexpected value of class %@ for %@", + NSStringFromClass(object_getClass(eventValue)), modifierKey])); + return NO; + } + + if (![myValue isEqualToNumber:body[modifierKey]]) { + return NO; + } + } + + return YES; // keys matched; all present modifiers matched +} + +@end + +@implementation RCTConvert (RCTHandledKey) + ++ (RCTHandledKey *)RCTHandledKey:(id)json +{ + if ([json isKindOfClass:[NSString class]]) { + return [[RCTHandledKey alloc] initWithKey:(NSString *)json]; + } + + if ([json isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)json; + NSString *key = dict[@"key"]; + if (key == nil) { + RCTLogConvertError(dict, @"a RCTHandledKey -- must include \"key\""); + return nil; + } + + RCTHandledKey *handledKey = [[RCTHandledKey alloc] initWithKey:key]; + NSArray *modifiers = RCTHandledKey.validModifiers; + for (NSString *key in modifiers) { + id value = dict[key]; + if (value == nil) { + continue; + } + + if (![value isKindOfClass:[NSNumber class]]) { + RCTLogConvertError(value, @"a boolean"); + return nil; + } + + [handledKey setValue:@([(NSNumber *)value boolValue]) forKey:key]; + } + + return handledKey; + } + + RCTLogConvertError(json, @"a RCTHandledKey -- allowed types are string and object"); + return nil; +} + +@end + +#endif // macOS] diff --git a/React/Views/RCTSwitch.m b/React/Views/RCTSwitch.m index 0cce9e180357de..5feb1100b2a4ce 100644 --- a/React/Views/RCTSwitch.m +++ b/React/Views/RCTSwitch.m @@ -11,6 +11,14 @@ @implementation RCTSwitch +// [macOS +- (void)setOn:(BOOL)on +{ + _wasOn = on; + [super setOn:on]; +} +// macOS] + - (void)setOn:(BOOL)on animated:(BOOL)animated { _wasOn = on; diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 21e49494dfe58e..6a24d44341de75 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -23,6 +23,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @protocol RCTAutoInsetsProtocol; +@class RCTHandledKey; // [macOS] + @interface RCTView : RCTUIView // [macOS] // [macOS @@ -161,10 +163,14 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) RCTDirectEventBlock onDrop; // Keyboarding events +// 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, assign) BOOL passthroughAllKeyEvents; @property (nonatomic, copy) RCTDirectEventBlock onKeyDown; @property (nonatomic, copy) RCTDirectEventBlock onKeyUp; -@property (nonatomic, copy) NSArray *validKeysDown; -@property (nonatomic, copy) NSArray *validKeysUp; +@property (nonatomic, copy) NSArray *validKeysDown; +@property (nonatomic, copy) NSArray *validKeysUp; // Shadow Properties @property (nonatomic, strong) NSColor *shadowColor; diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 9f626631d3c688..74eab91d1c0425 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -5,6 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +// [macOS +#import "objc/runtime.h" +#import "RCTHandledKey.h" +// macOS] #import "RCTView.h" #import @@ -1691,19 +1695,52 @@ - (BOOL)performDragOperation:(id )sender #pragma mark - Keyboard Events -- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { +// 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; +} + +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event shouldBlock:(BOOL *)shouldBlock { BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - // If the view is focusable and the component didn't explicity set the validKeysDown or Up, + // If the view is focusable and the component didn't explicity set the validKeysDown or validKeysUp, // allow enter/return and spacebar key events to mimic the behavior of native controls. if (self.focusable && validKeys == nil) { - validKeys = @[@"Enter", @" "]; + validKeys = @[ + [[RCTHandledKey alloc] initWithKey:@"Enter"], + [[RCTHandledKey alloc] initWithKey:@" "] + ]; } - // Only post events for keys we care about - if (![validKeys containsObject:key]) { + // If a view specifies a key, it will always be removed from the system event queue (i.e. "handled") + *shouldBlock = [RCTHandledKey event:event matchesFilter:validKeys]; + + // If an event isn't being removed from the queue, but was requested to "passthrough" by a view, + // we want to be sure we dispatch it only once for that view. See note for GetEventDispatchStateDictionary. + if ([self passthroughAllKeyEvents] && !*shouldBlock) { + NSNumber *tag = [self reactTag]; + NSMutableDictionary *dict = GetEventDispatchStateDictionary(event); + + if ([dict[tag] boolValue]) { + return nil; + } + + dict[tag] = @YES; + } + + // Don't pass events we don't care about + if (![self passthroughAllKeyEvents] && !*shouldBlock) { return nil; } @@ -1712,10 +1749,11 @@ - (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { - (BOOL)handleKeyboardEvent:(NSEvent *)event { if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { - RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; + BOOL shouldBlock = YES; + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock]; if (keyboardEvent) { [_eventDispatcher sendEvent:keyboardEvent]; - return YES; + return shouldBlock; } } return NO; @@ -1733,4 +1771,5 @@ - (void)keyUp:(NSEvent *)event { } } #endif // macOS] - @end + +@end diff --git a/React/Views/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index cca78099bd0a8c..125586ee047b74 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -18,6 +18,7 @@ + (NSDictionary *)bodyFromEvent:(NSEvent *)event NSString *key = [self keyFromEvent:event]; NSEventModifierFlags modifierFlags = event.modifierFlags; + // when making changes here, also consider what should happen to RCTHandledKey. [macOS] return @{ @"key" : key, @"capsLockKey" : (modifierFlags & NSEventModifierFlagCapsLock) ? @YES : @NO, @@ -54,15 +55,15 @@ + (NSString *)keyFromEvent:(NSEvent *)event return @"Backspace"; } else if (code == NSDeleteFunctionKey) { return @"Delete"; - } else if (code == NSHomeFunctionKey) { - return @"Home"; - } else if (code == NSEndFunctionKey) { - return @"End"; - } else if (code == NSPageUpFunctionKey) { - return @"PageUp"; - } else if (code == NSPageDownFunctionKey) { - return @"PageDown"; - } + } else if (code == NSHomeFunctionKey) { + return @"Home"; + } else if (code == NSEndFunctionKey) { + return @"End"; + } else if (code == NSPageUpFunctionKey) { + return @"PageUp"; + } else if (code == NSPageDownFunctionKey) { + return @"PageDown"; + } return key; } diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 97050e9102c1c2..0e3e0d12fb2ca5 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -525,10 +525,11 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDragLeave, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(passthroughAllKeyEvents, BOOL) RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock) // macOS keyboard events RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock) // macOS keyboard events -RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) -RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) #endif // macOS] diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 1e337aff02e8f9..d9785da7945a69 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -10,6 +10,7 @@ #import // [macOS] #import "RCTConvert.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTLog.h" #import "RCTScrollEvent.h" #import "RCTUIManager.h" @@ -1276,11 +1277,10 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager #if TARGET_OS_OSX - (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; // Only post events for keys we care about - if (![validKeys containsObject:key]) { + if (![RCTHandledKey event:event matchesFilter:validKeys]) { return nil; } diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 1ffef70b908583..8565f43d4ad260 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -13,88 +13,216 @@ const React = require('react'); const ReactNative = require('react-native'); import {Platform} from 'react-native'; -const {StyleSheet, Text, TextInput, View} = ReactNative; +import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + +const {Button, ScrollView, StyleSheet, Switch, Text, TextInput, View} = + ReactNative; + +const switchStyle = { + alignItems: 'center', + padding: 10, + flexDirection: 'row', + justifyContent: 'space-between', +}; function KeyEventExample(): React.Node { const [log, setLog] = React.useState([]); - const appendLog = (line: string) => { - const limit = 12; - let newLog = log.slice(0, limit - 1); - newLog.unshift(line); - setLog(newLog); - }; + + const clearLog = React.useCallback(() => { + setLog([]); + }, [setLog]); + + const appendLog = React.useCallback( + (line: string) => { + const limit = 12; + let newLog = log.slice(0, limit - 1); + newLog.unshift(line); + setLog(newLog); + }, + [log, setLog], + ); + + const handleKeyDown = React.useCallback( + (e: KeyEvent) => { + appendLog('Key Down:' + e.nativeEvent.key); + }, + [appendLog], + ); + + const handleKeyUp = React.useCallback( + (e: KeyEvent) => { + appendLog('Key Up:' + e.nativeEvent.key); + }, + [appendLog], + ); + + const [showView, setShowView] = React.useState(true); + const toggleShowView = React.useCallback( + (value: boolean) => { + setShowView(value); + }, + [setShowView], + ); + + const [showTextInput, setShowTextInput] = React.useState(true); + const toggleShowTextInput = React.useCallback( + (value: boolean) => { + setShowTextInput(value); + }, + [setShowTextInput], + ); + + const [showTextInput2, setShowTextInput2] = React.useState(true); + const toggleShowTextInput2 = React.useCallback( + (value: boolean) => { + setShowTextInput2(value); + }, + [setShowTextInput2], + ); + + const [passthroughAllKeyEvents, setPassthroughAllKeyEvents] = + React.useState(false); + const toggleSwitch = React.useCallback( + (value: boolean) => { + setPassthroughAllKeyEvents(value); + }, + [setPassthroughAllKeyEvents], + ); 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. - - - {Platform.OS === 'macos' ? ( - <> - View - - validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'} - validKeysUp: [c, d] - - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['c', 'd']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - TextInput - - validKeysDown: [ArrowRight, ArrowDown]{'\n'} - validKeysUp: [Escape, Enter] - - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['Escape', 'Enter']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['Escape', 'Enter']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - - validKeysDown: []{'\n'} - validKeysUp: [] - - - + + + 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. + + + {Platform.OS === 'macos' ? ( + <> + + View + + + {showView ? ( + <> + + validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'} + validKeysUp: [c, d] + + + + ) : null} + + TextInput + + + {showTextInput ? ( + <> + + validKeysDown: [ArrowRight, ArrowDown, Ctrl+Enter]{'\n'} + validKeysUp: [Escape, Enter] + + + + + ) : null} + + TextInput with no handled keys + + + {showTextInput2 ? ( + <> + + validKeysDown: []{'\n'} + validKeysUp: [] + + + + + ) : null} + + ) : null} + + {'Pass through all key events'} + - - ) : null} - {'Events:\n' + log.join('\n')} + +