diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 73d32d1285fab0..480d7461761815 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -20,7 +20,7 @@ const View = require('./View/View'); const invariant = require('invariant'); -import type {PressEvent} from '../Types/CoreEventTypes'; +import type {PressEvent, KeyEvent} from '../Types/CoreEventTypes'; import type {FocusEvent, BlurEvent} from './TextInput/TextInput'; // TODO(OSS Candidate ISS#2710739) import type {ColorValue} from '../StyleSheet/StyleSheetTypes'; @@ -113,6 +113,28 @@ type ButtonProps = $ReadOnly<{| * Handler to be called when the button loses key focus */ onFocus?: ?(e: FocusEvent) => void, + + /** + * Handler to be called when a key down press is detected + */ + onKeyDown?: ?(e: KeyEvent) => void, + + /** + * Handler to be called when a key up press is detected + */ + onKeyUp?: ?(e: KeyEvent) => void, + + /* + * Array of keys to receive key down events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysUp?: ?Array, // ]TODO(OSS Candidate ISS#2710739) |}>; @@ -163,6 +185,10 @@ class Button extends React.Component { testID, onFocus, // TODO(OSS Candidate ISS#2710739) onBlur, // TODO(OSS Candidate ISS#2710739) + onKeyDown, + validKeysDown, + validKeysUp, + onKeyUp, } = this.props; const buttonStyles = [styles.button]; const textStyles = [styles.text]; @@ -207,6 +233,10 @@ class Button extends React.Component { onPress={onPress} onFocus={onFocus} // TODO(OSS Candidate ISS#2710739) onBlur={onBlur} // TODO(OSS Candidate ISS#2710739) + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} + validKeysDown={validKeysDown} + validKeysUp={validKeysUp} touchSoundDisabled={touchSoundDisabled}> diff --git a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap index aada55fc3c066a..00b9b67f112640 100644 --- a/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ b/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap @@ -9,6 +9,8 @@ exports[` should render as expected: should deep render when mocked onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -29,6 +31,8 @@ exports[` should render as expected: should deep render when not mo onBlur={[Function]} onClick={[Function]} onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap index 2469722ff60bc1..91acebe0e5774b 100644 --- a/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap +++ b/Libraries/Components/TextInput/__tests__/__snapshots__/TextInput-test.js.snap @@ -13,6 +13,8 @@ exports[`TextInput tests should render as expected: should deep render when mock onChange={[Function]} onClick={[Function]} onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} @@ -42,6 +44,8 @@ exports[`TextInput tests should render as expected: should deep render when not onChange={[Function]} onClick={[Function]} onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.js b/Libraries/Components/Touchable/TouchableNativeFeedback.js index cdcb63956ebb67..fe7c878f6835b6 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.js @@ -82,6 +82,18 @@ type Props = $ReadOnly<{| */ nextFocusUp?: ?number, + /* + * Array of keys to receive key down events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysUp?: ?Array, + /** * Set to true to add the ripple effect to the foreground of the view, instead * of the background. This is useful if one of your child views has a @@ -297,6 +309,8 @@ class TouchableNativeFeedback extends React.Component { nextFocusUp: this.props.nextFocusUp, onLayout: this.props.onLayout, testID: this.props.testID, + validKeysDown: this.props.validKeysDown, + validKeysUp: this.props.validKeysUp, }, ...children, ); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 4381977e2b4193..c85586dca856d9 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -40,6 +40,17 @@ type Props = $ReadOnly<{| style?: ?ViewStyleProp, hostRef: React.Ref, + /* + * Array of keys to receive key down events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysUp?: ?Array, |}>; type State = $ReadOnly<{| @@ -165,6 +176,18 @@ class TouchableOpacity extends React.Component { this.props.onFocus(event); } }, + onKeyDown: event => { + if (this.props.onKeyDown != null) { + this.props.onKeyDown(event); + } + }, + onKeyUp: event => { + if (this.props.onKeyUp != null) { + this.props.onKeyUp(event); + } + }, + validKeysDown: this.props.validKeysDown, + validKeysUp: this.props.validKeysUp, onLongPress: this.props.onLongPress, onPress: this.props.onPress, onPressIn: event => { @@ -279,6 +302,10 @@ class TouchableOpacity extends React.Component { onDrop={this.props.onDrop} onFocus={this.props.onFocus} onBlur={this.props.onBlur} + onKeyDown={this.props.onKeyDown} + onKeyUp={this.props.onKeyUp} + validKeysDown={this.props.validKeysDown} + validKeysUp={this.props.validKeysUp} draggedTypes={this.props.draggedTypes} // ]TODO(macOS ISS#2323203) ref={this.props.hostRef} {...eventHandlersWithoutBlurAndFocus}> diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index de448985aac3f9..47553abe7bb944 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -26,6 +26,7 @@ import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type { BlurEvent, FocusEvent, + KeyEvent, LayoutEvent, PressEvent, MouseEvent, // TODO(macOS ISS#2323203) @@ -64,6 +65,10 @@ type Props = $ReadOnly<{| onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed, onBlur?: ?(event: BlurEvent) => mixed, onFocus?: ?(event: FocusEvent) => mixed, + onKeyDown?: ?(event: KeyEvent) => mixed, + onKeyUp?: ?(event: KeyEvent) => mixed, + validKeysDown?: ?Array, + validKeysUp?: ?Array, onLayout?: ?(event: LayoutEvent) => mixed, onLongPress?: ?(event: PressEvent) => mixed, onPress?: ?(event: PressEvent) => mixed, @@ -106,6 +111,10 @@ const PASSTHROUGH_PROPS = [ 'onAccessibilityAction', 'onBlur', 'onFocus', + 'onKeyDown', + 'onKeyUp', + 'validKeysDown', + 'validKeysUp', 'onLayout', 'onMouseEnter', // [TODO(macOS ISS#2323203) 'onMouseLeave', @@ -227,6 +236,10 @@ function createPressabilityConfig(props: Props): PressabilityConfig { android_disableSound: props.touchSoundDisabled, onBlur: props.onBlur, onFocus: props.onFocus, + onKeyDown: props.onKeyDown, + onKeyUp: props.onKeyUp, + validKeysDown: props.validKeysDown, + validKeysUp: props.validKeysUp, onLongPress: props.onLongPress, onPress: props.onPress, onPressIn: props.onPressIn, diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap index 7b68b8593fd50a..c9397c0641ceff 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap @@ -7,6 +7,8 @@ exports[`TouchableHighlight renders correctly 1`] = ` enableFocusRing={true} focusable={false} onClick={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} onResponderRelease={[Function]} diff --git a/Libraries/Components/View/ReactNativeViewAttributes.js b/Libraries/Components/View/ReactNativeViewAttributes.js index b1956afc3f7c71..cddfffa15d18ee 100644 --- a/Libraries/Components/View/ReactNativeViewAttributes.js +++ b/Libraries/Components/View/ReactNativeViewAttributes.js @@ -42,6 +42,10 @@ const UIView = { onDragEnter: true, onDragLeave: true, onDrop: true, + onKeyDown: true, + onKeyUp: true, + validKeysDown: true, + validKeysUp: true, draggedTypes: true, // ]TODO(macOS ISS#2323203) style: ReactNativeStyleAttributes, }; diff --git a/Libraries/Components/View/ReactNativeViewViewConfig.js b/Libraries/Components/View/ReactNativeViewViewConfig.js index 8858f62d5b0be4..df8b840a3e17b9 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfig.js +++ b/Libraries/Components/View/ReactNativeViewViewConfig.js @@ -45,6 +45,18 @@ const ReactNativeViewConfig = { captured: 'onFocusCapture', }, }, + topKeyUp: { + phasedRegistrationNames: { + bubbled: 'onKeyUp', + captured: 'onKeyUpCapture', + }, + }, + topKeyDown: { + phasedRegistrationNames: { + bubbled: 'onKeyDown', + captured: 'onKeyDownCapture', + }, + }, topKeyPress: { phasedRegistrationNames: { bubbled: 'onKeyPress', @@ -343,6 +355,8 @@ const ReactNativeViewConfig = { : {process: require('../../StyleSheet/processTransform')}): any), translateX: true, translateY: true, + validKeysDown: true, + validKeysUp: true, width: true, zIndex: true, }, diff --git a/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js b/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js index c28419bd527042..291bd7b7f9ae63 100644 --- a/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js +++ b/Libraries/Components/View/ReactNativeViewViewConfigMacOS.js @@ -47,6 +47,10 @@ const ReactNativeViewViewConfigMacOS = { onDragLeave: true, onDrop: true, onFocus: true, + onKeyDown: true, + onKeyUp: true, + validKeysDown: true, + validKeysUp: true, onMouseEnter: true, onMouseLeave: true, tooltip: true, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 0265dd1267160f..8b23249c3d1827 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -18,6 +18,7 @@ import type { Layout, LayoutEvent, ScrollEvent, // TODO(macOS ISS#2323203) + KeyEvent, } from '../../Types/CoreEventTypes'; import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; import type {Node} from 'react'; @@ -33,6 +34,8 @@ import type { // [TODO(macOS ISS#2323203) import type {DraggedTypesType} from '../View/DraggedType'; +//$FlowFixMe +import {array} from 'yargs'; // ]TODO(macOS ISS#2323203) export type ViewLayout = Layout; @@ -41,6 +44,8 @@ export type ViewLayoutEvent = LayoutEvent; type BubblingEventProps = $ReadOnly<{| onBlur?: ?(event: BlurEvent) => mixed, onFocus?: ?(event: FocusEvent) => mixed, + onKeyDown?: ?(event: KeyEvent) => mixed, + onKeyUp?: ?(event: KeyEvent) => mixed, |}>; type DirectEventProps = $ReadOnly<{| @@ -599,6 +604,18 @@ export type ViewProps = $ReadOnly<{| */ enableFocusRing?: ?boolean, // TODO(macOS ISS#2323203) + /* + * Array of keys to receive key down events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysDown?: ?array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysUp?: ?array, + /** * Enables Dran'n'Drop Support for certain types of dragged types * diff --git a/Libraries/Pressability/Pressability.js b/Libraries/Pressability/Pressability.js index f02c7339bbcca9..a5640fd20dbee8 100644 --- a/Libraries/Pressability/Pressability.js +++ b/Libraries/Pressability/Pressability.js @@ -17,6 +17,7 @@ import {normalizeRect, type RectOrSize} from '../StyleSheet/Rect'; import type { BlurEvent, FocusEvent, + KeyEvent, PressEvent, MouseEvent, } from '../Types/CoreEventTypes'; @@ -93,6 +94,28 @@ export type PressabilityConfig = $ReadOnly<{| */ onFocus?: ?(event: FocusEvent) => mixed, + /* + * Called after a key down event is detected. + */ + onKeyDown?: ?(event: KeyEvent) => mixed, + + /* + * Called after a key up event is detected. + */ + onKeyUp?: ?(event: KeyEvent) => mixed, + + /* + * Array of keys to receive key down events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysDown?: ?Array, + + /* + * Array of keys to receive key up events for + * For arrow keys, add "leftArrow", "rightArrow", "upArrow", "downArrow", + */ + validKeysUp?: ?Array, + /** * Called when the hover is activated to provide visual feedback. */ @@ -156,6 +179,8 @@ export type EventHandlers = $ReadOnly<{| onBlur: (event: BlurEvent) => void, onClick: (event: PressEvent) => void, onFocus: (event: FocusEvent) => void, + onKeyDown: (event: KeyEvent) => void, + onKeyUp: (event: KeyEvent) => void, onMouseEnter?: (event: MouseEvent) => void, onMouseLeave?: (event: MouseEvent) => void, onResponderGrant: (event: PressEvent) => void, @@ -446,6 +471,21 @@ export default class Pressability { }, }; + const keyEventHandlers = { + onKeyDown: (event: KeyEvent): void => { + const {onKeyDown} = this._config; + if (onKeyDown != null) { + onKeyDown(event); + } + }, + onKeyUp: (event: KeyEvent): void => { + const {onKeyUp} = this._config; + if (onKeyUp != null) { + onKeyUp(event); + } + }, + }; + const responderEventHandlers = { onStartShouldSetResponder: (): boolean => { const {disabled} = this._config; @@ -602,6 +642,7 @@ export default class Pressability { ...focusEventHandlers, ...responderEventHandlers, ...mouseEventHandlers, + ...keyEventHandlers, }; } diff --git a/Libraries/Renderer/shims/ReactNativeTypes.js b/Libraries/Renderer/shims/ReactNativeTypes.js index 10244570b65354..ec68139ab57208 100644 --- a/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/Libraries/Renderer/shims/ReactNativeTypes.js @@ -80,6 +80,8 @@ export type ViewConfigGetter = () => ReactNativeBaseComponentViewConfig<>; export type NativeMethods = { blur(): void, focus(): void, + onKeyDown(): void, + onKeyUp(): void, measure(callback: MeasureOnSuccessCallback): void, measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, measureLayout( diff --git a/Libraries/Types/CoreEventTypes.js b/Libraries/Types/CoreEventTypes.js index 065657395ef739..11c1b7100a5d3c 100644 --- a/Libraries/Types/CoreEventTypes.js +++ b/Libraries/Types/CoreEventTypes.js @@ -152,6 +152,26 @@ export type FocusEvent = SyntheticEvent< |}>, >; +export type KeyEvent = SyntheticEvent< + $ReadOnly<{| + // Modifier keys + capsLockKey: boolean, + shiftKey: boolean, + controlKey: boolean, + optionKey: boolean, + commandKey: boolean, + numericPadKey: boolean, + helpKey: boolean, + functionKey: boolean, + // Key options + leftArrowKey: boolean, + rightArrowKey: boolean, + upArrowKey: boolean, + downArrowKey: boolean, + key: string, + |}>, +>; + export type MouseEvent = SyntheticEvent< $ReadOnly<{| clientX: number, diff --git a/RNTester/Podfile.lock b/RNTester/Podfile.lock index 5909e08136818e..5cea5f4e0e0cc5 100644 --- a/RNTester/Podfile.lock +++ b/RNTester/Podfile.lock @@ -563,4 +563,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 18ca7d3b0e7db79041574a8bb6200b9e1c2d5359 -COCOAPODS: 1.9.1 +COCOAPODS: 1.9.1 \ No newline at end of file diff --git a/RNTester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/RNTester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js new file mode 100644 index 00000000000000..9544c8919719b8 --- /dev/null +++ b/RNTester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; // TODO(OSS Candidate ISS#2710739) + +const React = require('react'); +const ReactNative = require('react-native'); +import {Platform} from 'react-native'; +const {Button, PlatformColor, StyleSheet, Text, View} = ReactNative; + +import type {KeyEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + +type State = { + eventStream: string, + characters: string, +}; + +class KeyEventExample extends React.Component<{}, State> { + state: State = { + eventStream: '', + characters: '', + }; + + onKeyDownEvent: (e: KeyEvent) => void = (e: KeyEvent) => { + console.log('received view key down event\n', e.nativeEvent.key); + this.setState({characters: e.nativeEvent.key}); + this.setState(prevState => ({ + eventStream: + prevState.eventStream + prevState.characters + '\nKey Down: ', + })); + }; + + onKeyUpEvent: (e: KeyEvent) => void = (e: KeyEvent) => { + console.log('received key up event\n', e.nativeEvent.key); + this.setState({characters: e.nativeEvent.key}); + this.setState(prevState => ({ + eventStream: prevState.eventStream + prevState.characters + '\nKey Up: ', + })); + }; + + render() { + return ( + + Key events are called when a component detects a key press. + + {Platform.OS === 'macos' ? ( + +