Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Key handling] pass through all keys; allow specifying modifiers for validKeys[Down|Up] #1867

Merged
merged 5 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
24 changes: 2 additions & 22 deletions Libraries/Components/Pressable/Pressable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
BlurEvent,
// [macOS
FocusEvent,
KeyEvent,
LayoutEvent,
MouseEvent,
PressEvent,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string>,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
*/
validKeysUp?: ?Array<string>,
...KeyboardEventProps, // [macOS]
nakambo marked this conversation as resolved.
Show resolved Hide resolved

/**
* Specifies whether the view should receive the mouse down event when the
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const UIView = {
onDrop: true,
onKeyDown: true,
onKeyUp: true,
passthroughAllKeyEvents: true,
validKeysDown: true,
validKeysUp: true,
draggedTypes: true,
Expand Down
52 changes: 47 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 --
nakambo marked this conversation as resolved.
Show resolved Hide resolved
* 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,
|}>;
nakambo marked this conversation as resolved.
Show resolved Hide resolved

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,
nakambo marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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<string>,
validKeysDown?: ?Array<string | HandledKey>,
nakambo marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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<string>,
validKeysUp?: ?Array<string | HandledKey>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const validAttributesForNonEventProps = {
draggedTypes: true,
enableFocusRing: true,
tooltip: true,
passthroughAllKeyEvents: true,
validKeysDown: true,
validKeysUp: true,
mouseDownCanMoveWindow: true,
Expand Down
8 changes: 8 additions & 0 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
nakambo marked this conversation as resolved.
Show resolved Hide resolved

- (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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import "RCTHandledKey.h" // [macOS]
nakambo marked this conversation as resolved.
Show resolved Hide resolved

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand Down Expand Up @@ -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<NSDraggingInfo>)draggingInfo
Expand Down
4 changes: 4 additions & 0 deletions React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#endif

@class RCTHandledKey; // [macOS]
Saadnajmi marked this conversation as resolved.
Show resolved Hide resolved

/**
* This class provides a collection of conversion functions for mapping
* JSON objects to native types and classes. These are useful when writing
Expand Down Expand Up @@ -147,6 +149,8 @@ typedef BOOL css_backface_visibility_t;

#if TARGET_OS_OSX // [macOS
+ (NSString *)accessibilityRoleFromTraits:(id)json;

+ (NSArray<RCTHandledKey *> *)RCTHandledKeyArray:(id)json;
#endif // macOS]
@end

Expand Down
4 changes: 4 additions & 0 deletions React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import <CoreText/CoreText.h>

#import "RCTDefines.h"
#import "RCTHandledKey.h" // [macOS]
#import "RCTImageSource.h"
#import "RCTParserUtils.h"
#import "RCTUtils.h"
Expand Down Expand Up @@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);
nakambo marked this conversation as resolved.
Show resolved Hide resolved

#endif // macOS]

@end
Expand Down
37 changes: 37 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line number Diff line number Diff line change
@@ -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
nakambo marked this conversation as resolved.
Show resolved Hide resolved
#import <React/RCTConvert.h>

@interface RCTHandledKey : NSObject
nakambo marked this conversation as resolved.
Show resolved Hide resolved

+ (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;
@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
nakambo marked this conversation as resolved.
Show resolved Hide resolved

@end

@interface RCTConvert (RCTHandledKey)

+ (RCTHandledKey *)RCTHandledKey:(id)json;

@end

#endif // macOS]
136 changes: 136 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line number Diff line number Diff line change
@@ -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 <React/RCTAssert.h>
#import <React/RCTUtils.h>
#import <RCTConvert.h>
#import <RCTHandledKey.h>
#import <RCTViewKeyboardEvent.h>

#if TARGET_OS_OSX // [macOS
nakambo marked this conversation as resolved.
Show resolved Hide resolved

@implementation RCTHandledKey

+ (NSArray<NSString *> *)validModifiers {
// keep in sync with actual properties and RCTViewKeyboardEvent
return @[@"capsLock", @"shift", @"ctrl", @"alt", @"meta", @"numericPad", @"help", @"function"];
}

+ (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];
if (![body[@"key"] isEqualToString:self.key]) {
return NO;
}

NSArray<NSString *> *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]]) {
nakambo marked this conversation as resolved.
Show resolved Hide resolved
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) {
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]
8 changes: 8 additions & 0 deletions React/Views/RCTSwitch.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@

@implementation RCTSwitch

// [macOS
- (void)setOn:(BOOL)on
{
_wasOn = on;
[super setOn:on];
}
// macOS]
nakambo marked this conversation as resolved.
Show resolved Hide resolved

- (void)setOn:(BOOL)on animated:(BOOL)animated
{
_wasOn = on;
Expand Down
Loading