diff --git a/Libraries/Components/Pressable/Pressable.js b/Libraries/Components/Pressable/Pressable.js index 22bad0b977e9d1..b0cbe191a59820 100644 --- a/Libraries/Components/Pressable/Pressable.js +++ b/Libraries/Components/Pressable/Pressable.js @@ -151,24 +151,50 @@ type Props = $ReadOnly<{| onBlur?: ?(event: BlurEvent) => mixed, /** - * Called after a key down event is detected. + * Fired when a key is pressed. If validKeysDown is set, only those keys will fire this event. + * + * @platform macos */ - onKeyDown?: ?(event: KeyEvent) => mixed, + onKeyDown?: ?(e: KeyEvent) => void, /** - * Called after a key up event is detected. + * Array of keyboard events whose natiev handling should be supressed. Use with `onKeyDown` + * to handle a keyboard event purely in JS. + * + * @platform macos */ - onKeyUp?: ?(event: KeyEvent) => mixed, + keyDownEvents?: ?$ReadOnlyArray, /** - * Array of keys to receive key down events for - * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + * @deprecated use `keyDownEvents` instead. + * Array of keys to receive key down events for. + * If undefined, all keyboard events will fire `onKeyUp`. + * + * @platform macos */ validKeysDown?: ?Array, /** - * Array of keys to receive key up events for - * For arrow keys, add "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", + * Fired when a key is pressed. If validKeysDown is set, only those keys will fire this event. + * + * @platform macos + */ + onKeyUp?: ?(e: KeyEvent) => void, + + /** + * Array of keyboard events whose natiev handling should be supressed. Use with `onUp` + * to handle a keyboard event purely in JS. + * + * @platform macos + */ + keyUpEvents?: ?$ReadOnlyArray, + + /** + * @deprecated use `keyUpEvents` instead. + * Array of keys to receive key up events for. + * If undefined, all keyboard events will fire `onKeyUp` + * + * @platform macos */ validKeysUp?: ?Array, // ]TODO(macOS GH#774) diff --git a/Libraries/Components/View/PlatformViewPropTypes.macos.js b/Libraries/Components/View/PlatformViewPropTypes.macos.js deleted file mode 100644 index ebe52224bd3a00..00000000000000 --- a/Libraries/Components/View/PlatformViewPropTypes.macos.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @providesModule PlatformViewPropTypes - * @format - * @flow - */ - -// TODO(macOS GH#774) - -'use strict'; - -export type PlatformViewPropTypes = {}; diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index 816284710ed9d6..dd6965caed7c28 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -47,6 +47,8 @@ const UIView = { onKeyUp: true, validKeysDown: true, validKeysUp: true, + keyDownEvents: true, + keyUpEvents: true, draggedTypes: true, nextKeyViewTag: true, // ]TODO(macOS GH#774) diff --git a/Libraries/Components/View/ReactNativeViewViewConfig.js b/Libraries/Components/View/ReactNativeViewViewConfig.js index 5902cc231a06e2..bf8749de29330e 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfig.js +++ b/Libraries/Components/View/ReactNativeViewViewConfig.js @@ -21,6 +21,7 @@ const ReactNativeViewConfig: ViewConfig = { Constants: {}, bubblingEventTypes: { ...ReactNativeViewViewConfigAndroid.bubblingEventTypes, + ...ReactNativeViewViewConfigMacOS.bubblingEventTypes, // [macOS] topBlur: { phasedRegistrationNames: { bubbled: 'onBlur', @@ -106,12 +107,6 @@ const ReactNativeViewConfig: ViewConfig = { topMagicTap: { registrationName: 'onMagicTap', }, - topKeyUp: { - registrationName: 'onKeyUp', - }, - topKeyDown: { - registrationName: 'onKeyDown', - }, topPointerEnter: { registrationName: 'pointerenter', }, diff --git a/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js b/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js index af2b5ac0ef9bb7..3ad87b3506716b 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js +++ b/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js @@ -14,6 +14,14 @@ const ReactNativeViewViewConfigMacOS = { uiViewClassName: 'RCTView', + bubblingEventTypes: { + topKeyUp: { + registrationName: 'onKeyUp', + }, + topKeyDown: { + registrationName: 'onKeyDown', + }, + }, directEventTypes: { topDoubleClick: { registrationName: 'onDoubleClick', @@ -40,20 +48,11 @@ const ReactNativeViewViewConfigMacOS = { draggedTypes: true, enableFocusRing: true, nextKeyViewTag: true, - onBlur: true, - onClick: true, - onDoubleClick: true, - onDragEnter: true, - onDragLeave: true, - onDrop: true, - onFocus: true, - onKeyDown: true, - onKeyUp: true, - onMouseEnter: true, - onMouseLeave: true, tooltip: true, validKeysDown: true, validKeysUp: true, + keyDownEvents: true, + keyUpEvents: true }, }; diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 44159cba9f8d54..ef647b7969ce90 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -13,7 +13,8 @@ import type {ViewProps} from './ViewPropTypes'; import ViewNativeComponent from './ViewNativeComponent'; import TextAncestor from '../../Text/TextAncestor'; import * as React from 'react'; -import invariant from 'invariant'; // TODO(macOS GH#774) +import invariant from 'invariant'; // [macOS] +import type {KeyEvent} from '../../Types/CoreEventTypes'; // [macOS] export type Props = ViewProps; @@ -28,9 +29,48 @@ const View: React.AbstractComponent< ViewProps, React.ElementRef, > = React.forwardRef((props: ViewProps, forwardedRef) => { + // [macOS + const {onKeyDown, onKeyUp, validKeysDown, validKeysUp} = props; + + invariant( + // $FlowFixMe Wanting to catch untyped usages + validKeysDown === undefined, + 'Support for the "acceptsKeyboardFocus" property has been deprecated in favor of "keyDownEvents"', + ); + + invariant( + // $FlowFixMe Wanting to catch untyped usages + validKeysUp === undefined, + 'Support for the "acceptsKeyboardFocus" property has been removed in favor of "keyUpEvents"', + ); + + // To support the deprecated validKeysDown prop, suppress bubbling if it is defined + const onKeyDownWithLegacyBehavior = (e: KeyEvent) => { + if (validKeysDown) { + e.stopPropogation(); + } + onKeyDown?.(); + }; + + // To support the deprecated validKeysUp prop, suppress bubbling if it is defined + const onKeyUpWithLegacyBehavior = (e: KeyEvent) => { + if (validKeysUp) { + e.stopPropogation(); + } + onKeyUp?.(); + }; + // macOS] + return ( - + {/* [macOS */} + + {/* macOS] */} ); }); diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 17ae8e52807e4f..de25b898e08bec 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -41,8 +41,6 @@ export type ViewLayoutEvent = LayoutEvent; type BubblingEventProps = $ReadOnly<{| onBlur?: ?(event: BlurEvent) => mixed, onFocus?: ?(event: FocusEvent) => mixed, - onKeyDown?: ?(event: KeyEvent) => mixed, // TODO(macOS GH#774) - onKeyUp?: ?(event: KeyEvent) => mixed, // TODO(macOS GH#774) |}>; type DirectEventProps = $ReadOnly<{| @@ -405,7 +403,64 @@ type IOSViewProps = $ReadOnly<{| |}>; // [TODO(macOS GH#774) +type HandledKeyboardEvent = $ReadOnly<{| + altKey?: ?boolean, + ctrlKey?: ?boolean, + metaKey?: ?boolean, + shiftKey?: ?boolean, + code: string, + handledEventPhase?: number, +|}>; + type MacOSViewProps = $ReadOnly<{| + /** + * Fired when a key is pressed. If validKeysDown is set, only those keys will fire this event. + * + * @platform macos + */ + onKeyDown?: ?(e: KeyEvent) => void, + + /** + * Array of keyboard events whose natiev handling should be supressed. Use with `onKeyDown` + * to handle a keyboard event purely in JS. + * + * @platform macos + */ + keyDownEvents?: ?$ReadOnlyArray, + + /** + * @deprecated use `keyDownEvents` instead. + * Array of keys to receive key down events for. + * If undefined, all keyboard events will fire `onKeyUp`. + * + * @platform macos + */ + validKeysDown?: ?Array, + + /** + * Fired when a key is pressed. If validKeysDown is set, only those keys will fire this event. + * + * @platform macos + */ + onKeyUp?: ?(e: KeyEvent) => void, + + /** + * Array of keyboard events whose natiev handling should be supressed. Use with `onUp` + * to handle a keyboard event purely in JS. + * + * @platform macos + */ + keyUpEvents?: ?$ReadOnlyArray, + + /** + * @deprecated use `keyUpEvents` instead. + * Array of keys to receive key up events for. + * If undefined, all keyboard events will fire `onKeyUp` + * + * @platform macos + */ + validKeysUp?: ?Array, + /** * Fired when a dragged element enters a valid drop target * @@ -455,20 +510,6 @@ type MacOSViewProps = $ReadOnly<{| */ enableFocusRing?: ?boolean, - /** - * Array of keys to receive key down events for - * - * @platform macos - */ - validKeysDown?: ?Array, - - /** - * Array of keys to receive key up events for - * - * @platform macos - */ - validKeysUp?: ?Array, - /** * Enables Drag'n'Drop Support for certain types of dragged types * diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 6b21fab5cf234a..cc5b31f721ac99 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -1334,22 +1334,17 @@ class VirtualizedList extends React.PureComponent { this.props.onPreferredScrollerStyleDidChange; const invertedDidChange = this.props.onInvertedDidChange; - const isFirstRowSelected = this.state.selectedRowIndex === this.state.first; - const isLastRowSelected = this.state.selectedRowIndex === this.state.last; - - // Don't pass in ArrowUp/ArrowDown at the top/bottom of the list so that keyboard event can bubble - let _validKeysDown = ['Home', 'End']; - if (!isFirstRowSelected) { - _validKeysDown.push('ArrowUp'); - } - if (!isLastRowSelected) { - _validKeysDown.push('ArrowDown'); - } + let _keyDownEvents = [ + {key: 'Home'}, + {key: 'End'}, + {key: 'ArrowDown'}, + {key: 'ArrowUp'}, + ]; const keyboardNavigationProps = { focusable: true, - validKeysDown: _validKeysDown, onKeyDown: this._handleKeyDown, + keyDownEvents: __keyDownEvents, }; // ]TODO(macOS GH#774) const onRefresh = props.onRefresh; diff --git a/Libraries/Renderer/shims/ReactNativeTypes.js b/Libraries/Renderer/shims/ReactNativeTypes.js index 36c455b0f43e7e..9c089dcc0e9fb6 100644 --- a/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/Libraries/Renderer/shims/ReactNativeTypes.js @@ -100,8 +100,6 @@ export type PartialViewConfig = $ReadOnly<{ export type NativeMethods = $ReadOnly<{| blur(): void, focus(): void, - onKeyDown(): void, - onKeyUp(): void, measure(callback: MeasureOnSuccessCallback): void, measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, measureLayout( diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 83e9edc5d02b3c..649dcf7d786202 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -141,10 +141,12 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) RCTDirectEventBlock onDrop; // Keyboarding events -@property (nonatomic, copy) RCTDirectEventBlock onKeyDown; -@property (nonatomic, copy) RCTDirectEventBlock onKeyUp; -@property (nonatomic, copy) NSArray *validKeysDown; -@property (nonatomic, copy) NSArray *validKeysUp; +@property (nonatomic, copy) RCTBubblingEventBlock onKeyDown; +@property (nonatomic, copy) RCTBubblingEventBlock onKeyUp; +@property (nonatomic, copy) NSArray *validKeysDown; +@property (nonatomic, copy) NSArray *validKeysUp; +@property (nonatomic, copy) NSArray *keyDownEvents; +@property (nonatomic, copy) NSArray *keyUpEvents; // Shadow Properties @property (nonatomic, strong) NSColor *shadowColor; diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index e0b9a904aa6d84..64e6fe0a2f07a2 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -1675,7 +1675,15 @@ - (RCTViewKeyboardEvent*)keyboardEvent:(NSEvent*)event { return [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; } -- (BOOL)handleKeyboardEvent:(NSEvent *)event { +// If validKeysUp or validKeysDown is defined, use the legacy keyboard event handling behavior +- (void)shouldUseLegacyKeyboardBehaviorForEvent:(NSEvent*)event { + BOOL keyDown = event.type == NSEventTypeKeyDown; + NSArray *validKeys = keyDown ? [self validKeysDown] : [self validKeysUp]; + return (validKeys == nil); +} + +// 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) { RCTViewKeyboardEvent *keyboardEvent = [self keyboardEvent:event]; if (keyboardEvent) { @@ -1686,13 +1694,49 @@ - (BOOL)handleKeyboardEvent:(NSEvent *)event { 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). +- (BOOL)handleKeyboardEventModern:(NSEvent*)event { + RCTViewKeyboardEvent *keyEvent = [RCTViewKeyboardEvent keyEventFromEvent:event reactTag:self.reactTag]; + + // 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 keyDown = event.type == NSEventTypeKeyDown; + bool handledKeyEvents = keyDown ? [self keyDownEvents] : [self keyUpEvents]; + + BOOL shouldSuppressNativeHandling = NO; + for (RCTHandledKeyboardEvent *handledEvent in handledKeyEvents) { + if ([keyEvent matchesHandledEvent:handledEvent]) { + shouldSuppressNativeHandling = YES; + break; + } + } + return shouldSuppressNativeHandling; +} + - (void)keyDown:(NSEvent *)event { - if (![self handleKeyboardEvent:event]) { + BOOL shouldUseLegacyBehavior = [self shouldUseLegacyKeyboardBehaviorForEvent:event]; + + SEL handleKeyboardEvent = shouldUseLegacyBehavior ? + @selector(handleKeyboardEventLegacy) : + @selector(handleKeyboardEventModern); + + if (![self handleKeyboardEventSelector:event]) { [super keyDown:event]; } } - (void)keyUp:(NSEvent *)event { + BOOL shouldUseLegacyBehavior = [self shouldUseLegacyKeyboardBehaviorForEvent:event]; + + SEL handleKeyboardEvent = shouldUseLegacyBehavior ? + @selector(handleKeyboardEventLegacy) : + @selector(handleKeyboardEventModern); + if (![self handleKeyboardEvent:event]) { [super keyUp:event]; } diff --git a/React/Views/RCTViewKeyboardEvent.m b/React/Views/RCTViewKeyboardEvent.m index 3ec04a2e350eb3..466ae7408c10a9 100644 --- a/React/Views/RCTViewKeyboardEvent.m +++ b/React/Views/RCTViewKeyboardEvent.m @@ -65,7 +65,8 @@ + (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 +// 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 { // Ignore "dead keys" (key press that waits for another key to make a character)