diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index 5a7566e106fe25..5560626ffea930 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -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'; @@ -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, + 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, + + /** + * Array of keys to receive key up events for. These events have their default native behavior prevented. + * + * @platform macos */ - validKeysUp?: ?Array, + validKeysUp?: ?Array, /** * Specifies whether the view should receive the mouse down event when the diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 64e6adc28ea3fa..267b74332ed360 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -47,6 +47,7 @@ const UIView = { onDrop: true, onKeyDown: true, onKeyUp: true, + passthroughAllKeyEvents: true, validKeysDown: true, validKeysUp: true, draggedTypes: true, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 0ae5b364f9de1f..d845267e57373e 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -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, + validKeysDown?: ?Array, /** - * 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, + validKeysUp?: ?Array, |}>; // macOS] diff --git a/Libraries/NativeComponent/BaseViewConfig.macos.js b/Libraries/NativeComponent/BaseViewConfig.macos.js index 3040f72245b909..fd71051f0ce638 100644 --- a/Libraries/NativeComponent/BaseViewConfig.macos.js +++ b/Libraries/NativeComponent/BaseViewConfig.macos.js @@ -52,6 +52,7 @@ const validAttributesForNonEventProps = { draggedTypes: true, enableFocusRing: true, tooltip: true, + passthroughAllKeyEvents: true, validKeysDown: true, validKeysUp: true, mouseDownCanMoveWindow: true, diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index faf0c342efe051..c202dcfcd1a894 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -555,6 +555,14 @@ - (void)deleteBackward { } } #else // [macOS +- (BOOL)performKeyEquivalent:(NSEvent *)event { + if (!self.hasMarkedText && ![self.textInputDelegate textInputShouldHandleKeyEvent:event]) { + return YES; + } + + return [super performKeyEquivalent:event]; +} + - (void)keyDown:(NSEvent *)event { // If has marked text, handle by native and return // Do this check before textInputShouldHandleKeyEvent as that one attempts to send the event to JS diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index 58a662056ac4d8..3ff018581de060 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -21,6 +21,7 @@ #import #import // [macOS] #import "../RCTTextUIKit.h" // [macOS] +#import // [macOS] @implementation RCTBaseTextInputView { __weak RCTBridge *_bridge; @@ -668,7 +669,8 @@ - (BOOL)textInputShouldHandleDeleteForward:(__unused id)sender { } - (BOOL)hasValidKeyDownOrValidKeyUp:(NSString *)key { - return [self.validKeysDown containsObject:key] || [self.validKeysUp containsObject:key]; + return [RCTHandledKey key:key matchesFilter:self.validKeysDown] + || [RCTHandledKey key:key matchesFilter:self.validKeysUp]; } - (NSDragOperation)textInputDraggingEntered:(id)draggingInfo diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 2764540b180db1..994a8cce40646f 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -20,6 +20,8 @@ #import #endif +@class RCTHandledKey; // [macOS] + /** * This class provides a collection of conversion functions for mapping * JSON objects to native types and classes. These are useful when writing @@ -147,6 +149,8 @@ typedef BOOL css_backface_visibility_t; #if TARGET_OS_OSX // [macOS + (NSString *)accessibilityRoleFromTraits:(id)json; + ++ (NSArray *)RCTHandledKeyArray:(id)json; #endif // macOS] @end diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index c5ba2de0564cf7..7d0442e30f7e75 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -12,6 +12,7 @@ #import #import "RCTDefines.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTImageSource.h" #import "RCTParserUtils.h" #import "RCTUtils.h" @@ -1500,6 +1501,9 @@ + (NSString *)accessibilityRoleFromTraits:(id)json } return NSAccessibilityUnknownRole; } + +RCT_JSON_ARRAY_CONVERTER(RCTHandledKey); + #endif // macOS] @end diff --git a/React/Views/RCTHandledKey.h b/React/Views/RCTHandledKey.h new file mode 100644 index 00000000000000..f0d2562d46c9df --- /dev/null +++ b/React/Views/RCTHandledKey.h @@ -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 + +// 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 *)filter; ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)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 diff --git a/React/Views/RCTHandledKey.m b/React/Views/RCTHandledKey.m new file mode 100644 index 00000000000000..aa685c3b999044 --- /dev/null +++ b/React/Views/RCTHandledKey.m @@ -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 +#import +#import +#import +#import + +#if TARGET_OS_OSX + +@implementation RCTHandledKey + ++ (NSArray *)validModifiers { + // keep in sync with actual properties and RCTViewKeyboardEvent + return @[@"altKey", @"ctrlKey", @"metaKey", @"shiftKey"]; +} + ++ (BOOL)event:(NSEvent *)event matchesFilter:(NSArray *)filter { + for (RCTHandledKey *key in filter) { + if ([key matchesEvent:event]) { + return YES; + } + } + + return NO; +} + ++ (BOOL)key:(NSString *)key matchesFilter:(NSArray *)filter { + for (RCTHandledKey *aKey in filter) { + if ([[aKey key] isEqualToString:key]) { + return YES; + } + } + + return NO; +} + +- (instancetype)initWithKey:(NSString *)key { + if ((self = [super init])) { + self.key = key; + } + return self; +} + +- (BOOL)matchesEvent:(NSEvent *)event +{ + NSEventType type = [event type]; + if (type != NSEventTypeKeyDown && type != NSEventTypeKeyUp) { + RCTFatal(RCTErrorWithMessage([NSString stringWithFormat:@"Wrong event type (%d) sent to -[RCTHandledKey matchesEvent:]", (int)type])); + return NO; + } + + NSDictionary *body = [RCTViewKeyboardEvent bodyFromEvent:event]; + 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 *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 *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 diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 21e49494dfe58e..6a24d44341de75 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -23,6 +23,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @protocol RCTAutoInsetsProtocol; +@class RCTHandledKey; // [macOS] + @interface RCTView : RCTUIView // [macOS] // [macOS @@ -161,10 +163,14 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) RCTDirectEventBlock onDrop; // Keyboarding events +// NOTE does not properly work with single line text inputs (most key downs). This is because those are +// presumably handled by the window's field editor. To make it work, we'd need to look into providing +// a custom field editor for NSTextField controls. +@property (nonatomic, assign) BOOL passthroughAllKeyEvents; @property (nonatomic, copy) RCTDirectEventBlock onKeyDown; @property (nonatomic, copy) RCTDirectEventBlock onKeyUp; -@property (nonatomic, copy) NSArray *validKeysDown; -@property (nonatomic, copy) NSArray *validKeysUp; +@property (nonatomic, copy) NSArray *validKeysDown; +@property (nonatomic, copy) NSArray *validKeysUp; // Shadow Properties @property (nonatomic, strong) NSColor *shadowColor; diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 9f626631d3c688..641eaa15c90036 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -5,6 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +// [macOS +#import "objc/runtime.h" +#import "RCTHandledKey.h" +// macOS] #import "RCTView.h" #import @@ -1691,19 +1695,52 @@ - (BOOL)performDragOperation:(id )sender #pragma mark - Keyboard Events -- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { +// This dictionary is attached to the NSEvent being handled so we can ensure we only dispatch it +// once per RCTView\nativeTag. The reason we need to track this state is that certain React native +// views such as RCTUITextView inherit from views (such as NSTextView) which may or may not +// decide to bubble the event to the next responder, and we don't want to dispatch the same +// event more than once (e.g. first from RCTUITextView, and then from it's parent RCTView). +NSMutableDictionary *GetEventDispatchStateDictionary(NSEvent *event) { + static const char *key = "RCTEventDispatchStateDictionary"; + NSMutableDictionary *dict = objc_getAssociatedObject(event, key); + if (dict == nil) { + dict = [NSMutableDictionary new]; + objc_setAssociatedObject(event, key, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return dict; +} + +- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event shouldBlock:(BOOL *)shouldBlock { BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - // If the view is focusable and the component didn't explicity set the validKeysDown or Up, + // If the view is focusable and the component didn't explicity set the validKeysDown or validKeysUp, // allow enter/return and spacebar key events to mimic the behavior of native controls. if (self.focusable && validKeys == nil) { - validKeys = @[@"Enter", @" "]; + validKeys = @[ + [[RCTHandledKey alloc] initWithKey:@"Enter"], + [[RCTHandledKey alloc] initWithKey:@" "] + ]; } - // Only post events for keys we care about - if (![validKeys containsObject:key]) { + // If a view specifies a key, it will always be removed from the responder chain (i.e. "handled") + *shouldBlock = [RCTHandledKey event:event matchesFilter:validKeys]; + + // If an event isn't being removed from the queue, but was requested to "passthrough" by a view, + // we want to be sure we dispatch it only once for that view. See note for GetEventDispatchStateDictionary. + if ([self passthroughAllKeyEvents] && !*shouldBlock) { + NSNumber *tag = [self reactTag]; + NSMutableDictionary *dict = GetEventDispatchStateDictionary(event); + + if ([dict[tag] boolValue]) { + return nil; + } + + dict[tag] = @YES; + } + + // Don't pass events we don't care about + if (![self passthroughAllKeyEvents] && !*shouldBlock) { return nil; } @@ -1712,10 +1749,11 @@ - (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { - (BOOL)handleKeyboardEvent:(NSEvent *)event { if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { - RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; + BOOL shouldBlock = YES; + RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock]; if (keyboardEvent) { [_eventDispatcher sendEvent:keyboardEvent]; - return YES; + return shouldBlock; } } return NO; @@ -1733,4 +1771,5 @@ - (void)keyUp:(NSEvent *)event { } } #endif // macOS] - @end + +@end diff --git a/React/Views/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index cca78099bd0a8c..125586ee047b74 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -18,6 +18,7 @@ + (NSDictionary *)bodyFromEvent:(NSEvent *)event NSString *key = [self keyFromEvent:event]; NSEventModifierFlags modifierFlags = event.modifierFlags; + // when making changes here, also consider what should happen to RCTHandledKey. [macOS] return @{ @"key" : key, @"capsLockKey" : (modifierFlags & NSEventModifierFlagCapsLock) ? @YES : @NO, @@ -54,15 +55,15 @@ + (NSString *)keyFromEvent:(NSEvent *)event return @"Backspace"; } else if (code == NSDeleteFunctionKey) { return @"Delete"; - } else if (code == NSHomeFunctionKey) { - return @"Home"; - } else if (code == NSEndFunctionKey) { - return @"End"; - } else if (code == NSPageUpFunctionKey) { - return @"PageUp"; - } else if (code == NSPageDownFunctionKey) { - return @"PageDown"; - } + } else if (code == NSHomeFunctionKey) { + return @"Home"; + } else if (code == NSEndFunctionKey) { + return @"End"; + } else if (code == NSPageUpFunctionKey) { + return @"PageUp"; + } else if (code == NSPageDownFunctionKey) { + return @"PageDown"; + } return key; } diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 97050e9102c1c2..0e3e0d12fb2ca5 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -525,10 +525,11 @@ - (RCTShadowView *)shadowView RCT_EXPORT_VIEW_PROPERTY(onDragEnter, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDragLeave, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDrop, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(passthroughAllKeyEvents, BOOL) RCT_EXPORT_VIEW_PROPERTY(onKeyDown, RCTDirectEventBlock) // macOS keyboard events RCT_EXPORT_VIEW_PROPERTY(onKeyUp, RCTDirectEventBlock) // macOS keyboard events -RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) -RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysDown, NSArray) +RCT_EXPORT_VIEW_PROPERTY(validKeysUp, NSArray) #endif // macOS] diff --git a/React/Views/ScrollView/RCTScrollView.m b/React/Views/ScrollView/RCTScrollView.m index 1e337aff02e8f9..d9785da7945a69 100644 --- a/React/Views/ScrollView/RCTScrollView.m +++ b/React/Views/ScrollView/RCTScrollView.m @@ -10,6 +10,7 @@ #import // [macOS] #import "RCTConvert.h" +#import "RCTHandledKey.h" // [macOS] #import "RCTLog.h" #import "RCTScrollEvent.h" #import "RCTUIManager.h" @@ -1276,11 +1277,10 @@ - (void)uiManagerWillPerformMounting:(RCTUIManager *)manager #if TARGET_OS_OSX - (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - NSString *key = [RCTViewKeyboardEvent keyFromEvent:event]; + NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; // Only post events for keys we care about - if (![validKeys containsObject:key]) { + if (![RCTHandledKey event:event matchesFilter:validKeys]) { return nil; } diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 1ffef70b908583..40630a2028d682 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -13,88 +13,216 @@ const React = require('react'); const ReactNative = require('react-native'); import {Platform} from 'react-native'; -const {StyleSheet, Text, TextInput, View} = ReactNative; +import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + +const {Button, ScrollView, StyleSheet, Switch, Text, TextInput, View} = + ReactNative; + +const switchStyle = { + alignItems: 'center', + padding: 10, + flexDirection: 'row', + justifyContent: 'space-between', +}; function KeyEventExample(): React.Node { const [log, setLog] = React.useState([]); - const appendLog = (line: string) => { - const limit = 12; - let newLog = log.slice(0, limit - 1); - newLog.unshift(line); - setLog(newLog); - }; + + const clearLog = React.useCallback(() => { + setLog([]); + }, [setLog]); + + const appendLog = React.useCallback( + (line: string) => { + const limit = 12; + let newLog = log.slice(0, limit - 1); + newLog.unshift(line); + setLog(newLog); + }, + [log, setLog], + ); + + const handleKeyDown = React.useCallback( + (e: KeyEvent) => { + appendLog('Key Down:' + e.nativeEvent.key); + }, + [appendLog], + ); + + const handleKeyUp = React.useCallback( + (e: KeyEvent) => { + appendLog('Key Up:' + e.nativeEvent.key); + }, + [appendLog], + ); + + const [showView, setShowView] = React.useState(true); + const toggleShowView = React.useCallback( + (value: boolean) => { + setShowView(value); + }, + [setShowView], + ); + + const [showTextInput, setShowTextInput] = React.useState(true); + const toggleShowTextInput = React.useCallback( + (value: boolean) => { + setShowTextInput(value); + }, + [setShowTextInput], + ); + + const [showTextInput2, setShowTextInput2] = React.useState(true); + const toggleShowTextInput2 = React.useCallback( + (value: boolean) => { + setShowTextInput2(value); + }, + [setShowTextInput2], + ); + + const [passthroughAllKeyEvents, setPassthroughAllKeyEvents] = + React.useState(false); + const toggleSwitch = React.useCallback( + (value: boolean) => { + setPassthroughAllKeyEvents(value); + }, + [setPassthroughAllKeyEvents], + ); return ( - - - Key events are called when a component detects a key press.To tab - between views on macOS: Enable System Preferences / Keyboard / Shortcuts - > Use keyboard navigation to move focus between controls. - - - {Platform.OS === 'macos' ? ( - <> - View - - validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'} - validKeysUp: [c, d] - - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['c', 'd']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - TextInput - - validKeysDown: [ArrowRight, ArrowDown]{'\n'} - validKeysUp: [Escape, Enter] - - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['Escape', 'Enter']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - appendLog('Key Down:' + e.nativeEvent.key)} - validKeysUp={['Escape', 'Enter']} - onKeyUp={e => appendLog('Key Up:' + e.nativeEvent.key)} - /> - - validKeysDown: []{'\n'} - validKeysUp: [] - - - + + + Key events are called when a component detects a key press.To tab + between views on macOS: Enable System Preferences / Keyboard / + Shortcuts > Use keyboard navigation to move focus between controls. + + + {Platform.OS === 'macos' ? ( + <> + + View + + + {showView ? ( + <> + + validKeysDown: [g, Escape, Enter, ArrowLeft]{'\n'} + validKeysUp: [c, d] + + + + ) : null} + + TextInput + + + {showTextInput ? ( + <> + + validKeysDown: [ArrowRight, ArrowDown, Ctrl+Enter]{'\n'} + validKeysUp: [Escape, Enter] + + + + + ) : null} + + TextInput with no handled keys + + + {showTextInput2 ? ( + <> + + validKeysDown: []{'\n'} + validKeysUp: [] + + + + + ) : null} + + ) : null} + + {'Pass through all key events'} + - - ) : null} - {'Events:\n' + log.join('\n')} + +