forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Key handling] pass through all keys; allow specifying modifiers for …
…validKeys[Down|Up] (#1867) (#1866) * [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 * feedback * feedback #2 * PR feedback --------- Co-authored-by: Saad Najmi <[email protected]>
- Loading branch information
Showing
17 changed files
with
618 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
// [macOS] | ||
|
||
#if TARGET_OS_OSX | ||
#import <React/RCTConvert.h> | ||
|
||
// This class is used for specifying key filtering e.g. for -[RCTView validKeysDown] and -[RCTView validKeysUp] | ||
// Also see RCTViewKeyboardEvent, which is a React representation of an actual NSEvent that is dispatched to JS. | ||
@interface RCTHandledKey : NSObject | ||
|
||
+ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray<RCTHandledKey *> *)filter; | ||
+ (BOOL)key:(NSString *)key matchesFilter:(NSArray<RCTHandledKey *> *)filter; | ||
|
||
- (instancetype)initWithKey:(NSString *)key; | ||
- (BOOL)matchesEvent:(NSEvent *)event; | ||
|
||
@property (nonatomic, copy) NSString *key; | ||
|
||
// For the following modifiers, nil means we don't care about the presence of the modifier when filtering the key | ||
// They are still expected to be only boolean when not nil. | ||
@property (nonatomic, assign) NSNumber *altKey; | ||
@property (nonatomic, assign) NSNumber *ctrlKey; | ||
@property (nonatomic, assign) NSNumber *metaKey; | ||
@property (nonatomic, assign) NSNumber *shiftKey; | ||
|
||
@end | ||
|
||
@interface RCTConvert (RCTHandledKey) | ||
|
||
+ (RCTHandledKey *)RCTHandledKey:(id)json; | ||
|
||
@end | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
// [macOS] | ||
|
||
#import "objc/runtime.h" | ||
#import <React/RCTAssert.h> | ||
#import <React/RCTUtils.h> | ||
#import <RCTConvert.h> | ||
#import <RCTHandledKey.h> | ||
#import <RCTViewKeyboardEvent.h> | ||
|
||
#if TARGET_OS_OSX | ||
|
||
@implementation RCTHandledKey | ||
|
||
+ (NSArray<NSString *> *)validModifiers { | ||
// keep in sync with actual properties and RCTViewKeyboardEvent | ||
return @[@"altKey", @"ctrlKey", @"metaKey", @"shiftKey"]; | ||
} | ||
|
||
+ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray<RCTHandledKey *> *)filter { | ||
for (RCTHandledKey *key in filter) { | ||
if ([key matchesEvent:event]) { | ||
return YES; | ||
} | ||
} | ||
|
||
return NO; | ||
} | ||
|
||
+ (BOOL)key:(NSString *)key matchesFilter:(NSArray<RCTHandledKey *> *)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]; | ||
NSString *key = body[@"key"]; | ||
if (key == nil) { | ||
RCTFatal(RCTErrorWithMessage(@"Event body has missing value for 'key'")); | ||
return NO; | ||
} | ||
|
||
if (![key isEqualToString:self.key]) { | ||
return NO; | ||
} | ||
|
||
NSArray<NSString *> *modifiers = [RCTHandledKey validModifiers]; | ||
for (NSString *modifier in modifiers) { | ||
NSNumber *myValue = [self valueForKey:modifier]; | ||
|
||
if (myValue == nil) { | ||
continue; | ||
} | ||
|
||
NSNumber *eventValue = (NSNumber *)body[modifier]; | ||
if (eventValue == nil) { | ||
RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has missing value for '%@'", modifier])); | ||
return NO; | ||
} | ||
|
||
if (![eventValue isKindOfClass:[NSNumber class]]) { | ||
RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Event body has unexpected value of class '%@' for '%@'", | ||
NSStringFromClass(object_getClass(eventValue)), modifier])); | ||
return NO; | ||
} | ||
|
||
if (![myValue isEqualToNumber:body[modifier]]) { | ||
return NO; | ||
} | ||
} | ||
|
||
return YES; // keys matched; all present modifiers matched | ||
} | ||
|
||
@end | ||
|
||
@implementation RCTConvert (RCTHandledKey) | ||
|
||
+ (RCTHandledKey *)RCTHandledKey:(id)json | ||
{ | ||
// legacy way of specifying validKeysDown and validKeysUp -- here we ignore the modifiers when comparing to the NSEvent | ||
if ([json isKindOfClass:[NSString class]]) { | ||
return [[RCTHandledKey alloc] initWithKey:(NSString *)json]; | ||
} | ||
|
||
// modern way of specifying validKeys and validKeysUp -- here we assume missing modifiers to mean false\NO | ||
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<NSString *> *modifiers = RCTHandledKey.validModifiers; | ||
for (NSString *key in modifiers) { | ||
id value = dict[key]; | ||
if (value == nil) { | ||
value = @NO; // assume NO -- instead of nil i.e. "don't care" unlike the string case above. | ||
} | ||
|
||
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 |
Oops, something went wrong.