Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 18 additions & 6 deletions Libraries/Components/Pressable/Pressable.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ import type {
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility';
import type {HandledKeyboardEvent} from '../View/ViewPropTypes'; // [macOS]

import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
Expand DownExpand Up@@ -185,16 +186,27 @@ type Props = $ReadOnly<{|
onKeyUp?: ?(event: KeyEvent) => void,

/**
* Array of keys to receive key down events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* When `true`, allows `onKeyDown` and `onKeyUp` to receive events not specified in
* `validKeysDown` and `validKeysUp`, respectively. Events matching `validKeysDown` and `validKeysUp`
* still have their native default behavior prevented, but the others do not.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
passthroughAllKeyEvents?: ?boolean,

/**
* Array of keys to receive key up events for
* For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
* Array of keys to receive key down events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,

/**
* 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 numberDiff line numberDiff 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
46 changes: 41 additions & 5 deletions Libraries/Components/View/ViewPropTypes.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,22 +100,58 @@ 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 false.
*
* @platform macos
*/
export type HandledKeyboardEvent = $ReadOnly<{|
altKey?: ?boolean,
ctrlKey?: ?boolean,
metaKey?: ?boolean,
shiftKey?: ?boolean,
key: string,
|}>;

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 have their default native behavior prevented.
*
* @platform macos
*/
validKeysDown?: ?Array<string>,
validKeysDown?: ?Array<string | HandledKeyboardEvent>,

/**
* Array of keys to receive key up events for
* Array of keys to receive key up events for. These events have their default native behavior prevented.
*
* @platform macos
*/
validKeysUp?: ?Array<string>,
validKeysUp?: ?Array<string | HandledKeyboardEvent>,
|}>;
// macOS]

Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeComponent/BaseViewConfig.macos.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -550,6 +550,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
Expand Down
4 changes: 3 additions & 1 deletion Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@
#import <React/RCTTextSelection.h>
#import <React/RCTUITextView.h> // [macOS]
#import "../RCTTextUIKit.h" // [macOS]
#import <React/RCTHandledKey.h> // [macOS]

@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
Expand DownExpand Up@@ -662,7 +663,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 numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,8 @@
#import <WebKit/WebKit.h>
#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
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand Up@@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json
}
return NSAccessibilityUnknownRole;
}

RCT_JSON_ARRAY_CONVERTER(RCTHandledKey);

#endif // macOS]

@end
Expand Down
40 changes: 40 additions & 0 deletions React/Views/RCTHandledKey.h
Original file line numberDiff line numberDiff 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
145 changes: 145 additions & 0 deletions React/Views/RCTHandledKey.m
Original file line numberDiff line numberDiff 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
Loading