diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index dd87f8004dc28d..8b8ce52a8963c4 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -11,7 +11,6 @@ #import #import #import // [macOS] -#import // [macOS] #import #if TARGET_OS_OSX // [macOS @@ -169,8 +168,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) NSArray *validKeysDown; @property (nonatomic, copy) NSArray *validKeysUp; -@property (nonatomic, copy) NSArray *keyDownEvents; -@property (nonatomic, copy) NSArray *keyUpEvents; +@property (nonatomic, copy) NSArray *keyDownEvents; +@property (nonatomic, copy) NSArray *keyUpEvents; /** * Note: does not properly work with single line text inputs (most key downs). This is because those are diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index a8d5a4c5006544..cb2e5fb1e66872 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -1710,105 +1710,103 @@ - (BOOL)performDragOperation:(id )sender return dict; } -- (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event shouldBlock:(BOOL *)shouldBlock { +/// Use the legacy validKeysDown/validKeysUp/passthroughAllKeyEvents API. +/// Returns a BOOL of whether to prevent the default behavior. +/// the NSEvent is handled natively normally ( A.K.A: sent up the NSResponder chain ) unless `validKeysDown` or `validKeysUp` +/// contains the event. This prevents the default native behavior and sends the event to JS as a direct event. +/// The prop `passthroughAllKeyEvents` can be used to pass through all keyboard events to JS unconditionally. It does not, however, +/// have any effect on whether the default native behavior is prevented. +- (BOOL)handleKeyboardEventLegacy:(NSEvent *)event { BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *validKeys = keyDown ? self.validKeysDown : self.validKeysUp; - + NSArray *keyEventsToBlock = keyDown ? [self validKeysDown] : [self validKeysUp]; // 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 = @[ + if ([self focusable] && keyEventsToBlock == nil) { + keyEventsToBlock = @[ [[RCTHandledKey alloc] initWithKey:@"Enter"], [[RCTHandledKey alloc] initWithKey:@" "] ]; } - // If a view specifies a key, it will always be removed from the responder chain (i.e. "handled") - *shouldBlock = [RCTHandledKey event:event matchesFilter:validKeys]; + BOOL eventMatchesFilter = [RCTHandledKey event:event matchesFilter:keyEventsToBlock]; - // If an event isn't being removed from the queue, but was requested to "passthrough" by a view, + BOOL shouldDispatchEvent = eventMatchesFilter; + // If an event isn't having it's native behavior prevented, 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) { + if ([self passthroughAllKeyEvents] && !eventMatchesFilter) { NSNumber *tag = [self reactTag]; NSMutableDictionary *dict = GetEventDispatchStateDictionary(event); if ([dict[tag] boolValue]) { - return nil; - } - - dict[tag] = @YES; + shouldDispatchEvent = NO; + } else { + shouldDispatchEvent = YES; + dict[tag] = @YES; + } } - // Don't pass events we don't care about - if (![self passthroughAllKeyEvents] && !*shouldBlock) { - return nil; + if (shouldDispatchEvent) { + RCTViewKeyboardEvent *keyboardEvent = [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; + [_eventDispatcher sendEvent:keyboardEvent]; } - return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; + return eventMatchesFilter; } -// Only send events to JS that are defined in validKeysDown. Bubbling happens only natively -- (BOOL)handleKeyboardEventLegacy:(NSEvent *)event { - if (event.type == NSEventTypeKeyDown ? self.onKeyDown : self.onKeyUp) { - BOOL shouldBlock = YES; - RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event shouldBlock:&shouldBlock]; - if (keyboardEvent) { - [_eventDispatcher sendEvent:keyboardEvent]; - return shouldBlock; - } - } - return NO; -} - -// Send all keyboard events to JS. Suppress native bubbling if keyEvent matches keyDownEvents. -// Returns whether native bubbling should be suppressed (i.e: don't call super). +/// Use the cross platform `keyDownEvents/keyUpEvents` API to handle keyboard events: +/// All keyboard events are sent to JS unconditionally, and are bubbling events. The NSEvent is handled natively normally +/// ( A.K.A: sent up the NSResponder chain ) unless `keyFownEvents` or `keyUpEvents` is specified. - (BOOL)handleKeyboardEventModern:(NSEvent*)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *keyEventsToBlock = keyDown ? [self keyDownEvents] : [self keyUpEvents]; + + + // To ensure we only dispatch one keyboard event to JS, only dispatch it if we are the first responder. + BOOL isFirstResponder = self == [[self window] firstResponder]; + if (isFirstResponder) { RCTViewKeyboardEvent *keyboardEvent = [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; + [_eventDispatcher sendEvent:keyboardEvent]; + } - // To ensure we only dispatch one keyboard event to JS, only dispatch it if we are the first responder. - BOOL isFirstResponder = self == [[self window] firstResponder]; - if (isFirstResponder) { - [_eventDispatcher sendEvent:keyboardEvent]; - } + BOOL eventMatchesFilter = [RCTHandledKey event:event matchesFilter:keyEventsToBlock]; - BOOL keyDown = event.type == NSEventTypeKeyDown; - NSArray *handledKeyEvents = keyDown ? [self keyDownEvents] : [self keyUpEvents]; + return eventMatchesFilter; +} - BOOL shouldSuppressNativeHandling = NO; - for (RCTHandledKeyboardEvent *handledEvent in handledKeyEvents) { - if ([RCTViewKeyboardEvent event:event matches:handledEvent]) { - shouldSuppressNativeHandling = YES; - break; - } - } - return shouldSuppressNativeHandling; +/// This method will dispatch the keyboard event to JS if needed, based on the current Feature Flag. +/// Returns: a boolean indicating whether we prevent the default native behavior (aka: calling super ). +- (BOOL)handleKeyboardEvent:(NSEvent *)event { + BOOL shouldUseCrossPlatformKeyboardEventAPI = RCTGetEnableCrossPlatformKeyboardEventAPI(); + BOOL shouldPreventNativeBehavior = NO; + + if (shouldUseCrossPlatformKeyboardEventAPI) { + shouldPreventNativeBehavior = [self handleKeyboardEventModern:event]; + } else { + shouldPreventNativeBehavior = [self handleKeyboardEventLegacy:event]; + } + + return shouldPreventNativeBehavior; } - (void)keyDown:(NSEvent *)event { - BOOL shouldUseKeyDownEvents = RCTGetEnableCrossPlatformKeyboardEventAPI(); + // Ignore "dead keys" (key press that waits for another key to make a character) + if (!event.charactersIgnoringModifiers.length) { + [super keyDown:event]; + } - if (shouldUseKeyDownEvents) { - if (![self handleKeyboardEventModern:event]) { - [super keyDown:event]; - } - } else { - if (![self handleKeyboardEventLegacy:event]) { - [super keyDown:event]; - } + if (![self handleKeyboardEvent:event]) { + [super keyDown:event]; } } - (void)keyUp:(NSEvent *)event { - BOOL shouldUseKeyUpEvents = RCTGetEnableCrossPlatformKeyboardEventAPI(); - - if (shouldUseKeyUpEvents) { - if (![self handleKeyboardEventModern:event]) { - [super keyDown:event]; - } - } else { - if (![self handleKeyboardEventLegacy:event]) { - [super keyDown:event]; - } + // Ignore "dead keys" (key press that waits for another key to make a character) + if (!event.charactersIgnoringModifiers.length) { + [super keyUp:event]; + } + + if (![self handleKeyboardEvent:event]) { + [super keyUp:event]; } } #endif // macOS] diff --git a/React/Views/RCTViewKeyboardEvent.h b/React/Views/RCTViewKeyboardEvent.h index bd7c9a163d85bc..e93a37a270917f 100644 --- a/React/Views/RCTViewKeyboardEvent.h +++ b/React/Views/RCTViewKeyboardEvent.h @@ -25,9 +25,8 @@ @interface RCTViewKeyboardEvent : RCTComponentEvent + (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag; -+ (NSDictionary *)bodyFromEvent:(NSEvent *)event; + (NSString *)keyFromEvent:(NSEvent *)event; -+ (BOOL)event:(NSEvent *)event matches:(RCTHandledKeyboardEvent *)handledEvent; ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event; @end diff --git a/React/Views/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index b93c65f2866e90..3c3affcbb8083a 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -18,25 +18,6 @@ @implementation RCTHandledKeyboardEvent @implementation RCTViewKeyboardEvent -+ (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, - @"shiftKey" : (modifierFlags & NSEventModifierFlagShift) ? @YES : @NO, - @"ctrlKey" : (modifierFlags & NSEventModifierFlagControl) ? @YES : @NO, - @"altKey" : (modifierFlags & NSEventModifierFlagOption) ? @YES : @NO, - @"metaKey" : (modifierFlags & NSEventModifierFlagCommand) ? @YES : @NO, - @"numericPadKey" : (modifierFlags & NSEventModifierFlagNumericPad) ? @YES : @NO, - @"helpKey" : (modifierFlags & NSEventModifierFlagHelp) ? @YES : @NO, - @"functionKey" : (modifierFlags & NSEventModifierFlagFunction) ? @YES : @NO, - }; -} - + (NSString *)keyFromEvent:(NSEvent *)event { NSString *key = event.charactersIgnoringModifiers; @@ -73,63 +54,37 @@ + (NSString *)keyFromEvent:(NSEvent *)event return key; } -// Keyboard mappings are aligned cross-platform as much as possible as per this doc -// https://github.com/microsoft/react-native-windows/blob/master/vnext/proposals/active/keyboard-reconcile-desktop.md -+ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag ++ (NSDictionary *)bodyFromEvent:(NSEvent *)event { - // Ignore "dead keys" (key press that waits for another key to make a character) - if (!event.charactersIgnoringModifiers.length) { - return nil; - } + NSString *key = [self keyFromEvent:event]; + NSEventModifierFlags modifierFlags = event.modifierFlags; - return [[self alloc] initWithName:(event.type == NSEventTypeKeyDown ? @"keyDown" : @"keyUp") - viewTag:reactTag - body:[self bodyFromEvent:event]]; + // when making changes here, also consider what should happen to RCTHandledKey. [macOS] + return @{ + @"key" : key, + @"capsLockKey" : (modifierFlags & NSEventModifierFlagCapsLock) ? @YES : @NO, + @"shiftKey" : (modifierFlags & NSEventModifierFlagShift) ? @YES : @NO, + @"ctrlKey" : (modifierFlags & NSEventModifierFlagControl) ? @YES : @NO, + @"altKey" : (modifierFlags & NSEventModifierFlagOption) ? @YES : @NO, + @"metaKey" : (modifierFlags & NSEventModifierFlagCommand) ? @YES : @NO, + @"numericPadKey" : (modifierFlags & NSEventModifierFlagNumericPad) ? @YES : @NO, + @"helpKey" : (modifierFlags & NSEventModifierFlagHelp) ? @YES : @NO, + @"functionKey" : (modifierFlags & NSEventModifierFlagFunction) ? @YES : @NO, + }; } -+ (BOOL)event:(NSEvent *)event matches:(RCTHandledKeyboardEvent *)handledEvent +// Keyboard mappings are aligned cross-platform as much as possible as per this doc +// https://github.com/microsoft/react-native-windows/blob/master/vnext/proposals/active/keyboard-reconcile-desktop.md ++ (instancetype)keyEventFromEvent:(NSEvent *)event reactTag:(NSNumber *)reactTag { - NSDictionary *body = [self bodyFromEvent:event]; - - if ([body[@"key"] isEqualToString:[handledEvent key]] && - ((BOOL)body[@"capsLockKey"]) == [handledEvent capsLockKey] && - ((BOOL)body[@"shiftKey"]) == [handledEvent shiftKey] && - ((BOOL)body[@"ctrlKey"]) == [handledEvent ctrlKey] && - ((BOOL)body[@"altKey"]) == [handledEvent altKey] && - ((BOOL)body[@"metaKey"]) == [handledEvent metaKey] && - ((BOOL)body[@"helpKey"]) == [handledEvent helpKey] && - ((BOOL)body[@"functionKey"]) == [handledEvent functionKey]) - { - return YES; - } - return NO; -} + NSString *eventName = event.type == NSEventTypeKeyDown ? @"keyDown" : @"keyUp"; + NSDictionary *eventBody = [self bodyFromEvent:event]; -@end - -@implementation RCTConvert (RCTViewKeyboardEvent) - -+ (RCTHandledKeyboardEvent *)RCTHandledKeyboardEvent:(id)json -{ - if (!json) { - return nil; - } - RCTHandledKeyboardEvent *event = [RCTHandledKeyboardEvent new]; - - [event setKey:json[@"key"]]; - [event setCapsLockKey:json[@"capsLockKey"]]; - [event setShiftKey:json[@"shiftKey"]]; - [event setCtrlKey:json[@"ctrlKey"]]; - [event setAltKey:json[@"altKey"]]; - [event setMetaKey:json[@"metaKey"]]; - [event setNumericPadKey:json[@"numericPadKey"]]; - [event setHelpKey:json[@"helpKey"]]; - [event setFunctionKey:json[@"functionKey"]]; - - return event; + return [[self alloc] initWithName:eventName + viewTag:reactTag + body:eventBody]; } - @end #endif // TARGET_OS_OSX diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index ac1fa81594cad8..9549aeec98621d 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -478,43 +478,43 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 8fa3cd00fa17ef6c3221e5fd283fa93e81d23017 DoubleConversion: acaf5db79676d2e9119015819153f0f99191de12 - FBLazyVector: ea648c2a8a4d504bf47c7c8981122b3c300cad85 - FBReactNativeSpec: 913035bab4d6efb72e771cdc4a143ac724319ce9 + FBLazyVector: aaf7c42c36d9ae50fec21130ecd50c495d7eb0b5 + FBReactNativeSpec: 83c6f0716c3a3a5b7763b86e066a27e2443bd100 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 6df0a3d6e2750a50609471fd1a01fd2948d405b5 OCMock: 9491e4bec59e0b267d52a9184ff5605995e74be8 RCT-Folly: bf7b4921a91932051ebf1c5c2d297154b1fa3c8c - RCTRequired: 241e63c5b46fed005e1801ceb5d56e7dcf77ceee - RCTTypeSafety: 8461c615ee936ac389ade393c26ee3f72517fed3 - React: 9978d06656c9fbd3c61b670faae2ddf47ec0c93a - React-callinvoker: d43b46ff3c917b9c7a51e492f2817a8d91c27923 + RCTRequired: 53a389c87d41c3119ae62ee29412c9816e06973b + RCTTypeSafety: e6f1bd887865c98cb3968492e41373903fb7301c + React: 7c98fa48fe5fbce7c7c68d0c960889d3dd114aa9 + React-callinvoker: 707bca1db2f3fd561f06a0f8bd0cb3d0aec3375f React-Codegen: e60d0b9dffceed22f60cbbc186b65a7f4ef8457a - React-Core: 638f852b2f98405beef7dbc64c4d7685b2c4c4e4 - React-CoreModules: a75838d9c516992d727b06c9a263cedcf99bab79 - React-cxxreact: 22e1ab09cb50fbd59ec10da107ea2b4f3c02e468 - React-jsc: 13780890c05d13bef966367bad4de65ba80fc48d - React-jsi: 41cdb6dad3d4d7eed9acc2f324014679dbd16f78 - React-jsiexecutor: f0a5803cb4246c57b54bb186553e23bf7362c7fe - React-jsinspector: cd6a8b2ca24062415fcc0784e385f314d924f8fc - React-logger: 199f21ddf9192a21c07137dfa7d6b932a0842722 - React-perflogger: d4162e3487687256aec336deaa6cf819ef0e5c3d - React-RCTActionSheet: a4458fe15680df3116c54cd06cc3a3582eb8f97a - React-RCTAnimation: 11d8701e1ae7c33146e32c1a6d27992abc6eee83 - React-RCTAppDelegate: dcb7f293abf3e130e65c777c332fc850fd2202a5 - React-RCTBlob: 4f7e7eb0364c5e97e04209601cf0304375137e97 - React-RCTImage: 395518d2bf719a1c549d499fb2c74fb937233e32 - React-RCTLinking: c214279bdc4ac003c45e131b1af25c28b4538d34 - React-RCTNetwork: d1d7676299ccb0161ebdac4d2e6a4c26823002d3 - React-RCTPushNotification: 2f2245d6666b101a8b035732506c7115ac948b2b - React-RCTSettings: 57f4e9c04ea2e4afe146273abcde0e280b16266f - React-RCTTest: b40b075cd160fc0e7d0a2125ec789794b7366ce9 - React-RCTText: 522128331c35c87ba2165089b1dc85d96bbf9490 - React-RCTVibration: 047a108a8f5cc2213da3e6f77a444bda8b7035a7 - React-runtimeexecutor: db529d629e198eef4644883d4e17879df1bac496 - ReactCommon: 79847d831baa98cc56368aa8a223df62cd6b9287 + React-Core: 4eacfdd7a4da4d04e5fd57b825ad33cfcadc9310 + React-CoreModules: 3790603ff27daba9e1db9144003297f4f1f3d255 + React-cxxreact: fb88b1e02d0308e43f4313c468875188a5f793bd + React-jsc: aa15646e8c0143d5babc17904994f32040ae53c1 + React-jsi: edbb5a71f12d022d61f81737035985b320e8ec71 + React-jsiexecutor: ff7a9d4b4a7078051793a7002f50a935cf371aac + React-jsinspector: 14cc8b3b6125c6628a03dfa6e1d16d1cbf6a9358 + React-logger: 86e09ebef186c2e5e676c24868819dcbc6fd9f03 + React-perflogger: 86df11c869e8676b7d643c16b98e6cf7e1b310e7 + React-RCTActionSheet: 41d5b2d65266e12ba65aa1d4c609472f76eaf6ad + React-RCTAnimation: 0e05dab7b2b5132c357a92f8ae16db1ea5b7974b + React-RCTAppDelegate: 15b73a5f1caeb71ad10b0acab315258d05efad79 + React-RCTBlob: ae19f7768632c614f71c1fe481c90f59fb1fc89d + React-RCTImage: f24e928ae771f1cdbcb50cb56f86fff46aff354a + React-RCTLinking: 8d92c4cc919de16e9591b80feeb2579619e0399c + React-RCTNetwork: 6d17d38e233021216bd245f85c5f54d508ae88a6 + React-RCTPushNotification: 1f24facd7ecd0724765454d69c4fa2542a9bd6ee + React-RCTSettings: 9dbaaa0a9519db5daeb05bcad46b4997600c558f + React-RCTTest: 01d353ff83d7e9e1c54e2873bddbf46b00079e3e + React-RCTText: 48e18ef3bc16387bae2b6266c7f929ed9565214d + React-RCTVibration: f7e5c2bf1a75f53fd7831bb800e98dd6ea29aacf + React-runtimeexecutor: 6d97f760d7762c275b1f352e9bbc1a0cce566781 + ReactCommon: 7b7c865b0f4020805ce3888893932a9061030644 ScreenshotManager: e688dd0e3723eead7e15ffbf188956353819ab68 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 - Yoga: 6f7f992970217d3fc99aa8590e53ad4fafdb5686 + Yoga: 0648fcdb51957da9efc911e220b8239facb5ab90 PODFILE CHECKSUM: af970e71231450a2c7904bcd9e9e774b46488574 diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 40630a2028d682..510ebbf5ed52b9 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -25,7 +25,7 @@ const switchStyle = { justifyContent: 'space-between', }; -function KeyEventExample(): React.Node { +function KeyboardEventExample(): React.Node { const [log, setLog] = React.useState([]); const clearLog = React.useCallback(() => { @@ -115,6 +115,7 @@ function KeyEventExample(): React.Node { style={styles.row} passthroughAllKeyEvents={passthroughAllKeyEvents} validKeysDown={['g', 'Escape', 'Enter', 'ArrowLeft']} + keyDownEvents={['g', 'Escape', 'Enter', 'ArrowLeft']} onKeyDown={handleKeyDown} validKeysUp={['c', 'd']} onKeyUp={handleKeyUp} @@ -245,13 +246,13 @@ const styles = StyleSheet.create({ }, }); -exports.title = 'Key Events'; +exports.title = 'Keyboard Events'; exports.description = 'Examples that show how Key events can be used.'; exports.examples = [ { - title: 'KeyEventExample', + title: 'KeyboaradEventExample', render: function (): React.Element { - return ; + return ; }, }, ];