diff --git a/change/react-native-windows-1c57a8ec-8618-42b7-84a2-d786d50b5108.json b/change/react-native-windows-1c57a8ec-8618-42b7-84a2-d786d50b5108.json new file mode 100644 index 00000000000..230e4684336 --- /dev/null +++ b/change/react-native-windows-1c57a8ec-8618-42b7-84a2-d786d50b5108.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Match native View.keyDownEvents behavior in JS.", + "packageName": "react-native-windows", + "email": "igklemen@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/tester/src/js/examples/Pressable/PressableExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/Pressable/PressableExample.windows.js index 5a5420dda86..873bf2fa1a0 100644 --- a/packages/@react-native-windows/tester/src/js/examples/Pressable/PressableExample.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/Pressable/PressableExample.windows.js @@ -350,61 +350,47 @@ function PressWithOnKeyDown() { function PressWithKeyCapture() { const [eventLog, setEventLog] = useState([]); - const [shouldStopPropagation, setShouldStopPropagation] = useState(false); - const toggleSwitch2 = () => - setShouldStopPropagation(shouldStopPropagation => !shouldStopPropagation); + const [timesPressed, setTimesPressed] = useState(0); - function appendEvent(eventName) { + function logEvent(eventName) { const limit = 6; setEventLog(current => { return [eventName].concat(current.slice(0, limit - 1)); }); - } - - function myKeyDown(event) { - appendEvent('keyDown ' + event.nativeEvent.code); - - if (shouldStopPropagation) { - event.stopPropagation(); - } - } - - function myKeyUp(event) { - appendEvent('keyUp ' + event.nativeEvent.code); - - if (shouldStopPropagation) { - event.stopPropagation(); - } - } - - function myKeyDownCapture(event) { - appendEvent('keyDownCapture ' + event.nativeEvent.code); - - if (shouldStopPropagation) { - event.stopPropagation(); - } + console.log(eventName); } return ( <> - - myKeyDownCapture(event)}> - myKeyDown(event)} - onKeyUp={event => myKeyUp(event)} - onPress={() => appendEvent('press')}> - Press Me - - - - {eventLog.map((e, ii) => ( - {e} - ))} - - + logEvent('outer keyDown ' + event.nativeEvent.code)} + onKeyDownCapture={event => + logEvent('outer keyDownCapture ' + event.nativeEvent.code) + }> + logEvent('keyDown ' + event.nativeEvent.code)} + onKeyDownCapture={event => + logEvent('keyDownCapture ' + event.nativeEvent.code) + } + onPress={() => { + setTimesPressed(current => current + 1); + logEvent('pressed ' + timesPressed); + }}> + {({pressed}) => ( + {pressed ? 'Pressed!' : 'Press Me'} + )} + + + + + {eventLog.map((e, ii) => ( + {e} + ))} ); @@ -658,9 +644,9 @@ exports.examples = [ { title: 'OnKeyDownCapture on Pressable (View)', description: ('You can intercept routed KeyDown/KeyUp events by specifying the onKeyDownCapture/onKeyUpCapture callbacks.' + - " In the example below, set focus to the 'Press me' element (by tabbing into it), and start pressing letters (or any other keys) on the keyboard to observe the event log below." + - " Additionally, it's possible to control whether the intercepted event will continue down the route to its originating element, by toggling the switch below." + - " This specifies that the stopPropagation() method will be called on the event. When the switch is on, you'll see the KeyDown event doesn't show up after the KeyDownCapture for a given key.": string), + " In the example below, a is wrapper in a , and each specifies onKeyDown and onKeyDownCapture callbacks. Set focus to the 'Press me' element by tabbing into it, and start pressing letters on the keyboard to observe the event log below." + + " Additionally, because the keyDownEvents prop is specified - keyDownEvents={[{code: 'KeyW', handledEventPhase: 3}, {code: 'KeyE', handledEventPhase: 1}]} - " + + 'for these keys the event routing will be interrupted (by a call to event.stopPropagation()) at the phase specified (3 - bubbling, 1 - capturing) to match processing on the native side.': string), render: function(): React.Node { return ; }, diff --git a/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js b/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js index f06629623dc..4956c04ecb0 100644 --- a/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js +++ b/packages/@react-native-windows/tester/src/js/examples/TextInput/TextInputExample.windows.js @@ -20,6 +20,7 @@ const { Slider, Switch, } = require('react-native'); +const {useState} = React; const TextInputSharedExamples = require('./TextInputSharedExamples'); @@ -156,6 +157,54 @@ class PressInOutEvents extends React.Component< } } +function PropagationSample() { + const [eventLog, setEventLog] = useState([]); + + function logEvent(eventName) { + const limit = 6; + setEventLog(current => { + return [eventName].concat(current.slice(0, limit - 1)); + }); + console.log(eventName); + } + return ( + <> + logEvent('outer keyDown ' + event.nativeEvent.code)} + onKeyDownCapture={event => + logEvent('outer keyDownCapture ' + event.nativeEvent.code) + }> + some text to focus on + + logEvent('textinput keyDown ' + event.nativeEvent.code) + } + onKeyUp={event => + logEvent('textinput keyUp ' + event.nativeEvent.code) + } + keyDownEvents={[ + {code: 'KeyW', handledEventPhase: 3}, + {code: 'KeyE', handledEventPhase: 1}, + ]} + /> + + + {eventLog.map((e, ii) => ( + {e} + ))} + + + ); +} + const styles = StyleSheet.create({ multiline: { height: 60, @@ -487,5 +536,11 @@ exports.examples = ([ ); }, }, + { + title: 'Stop propagation sample', + render: function(): React.Node { + return ; + }, + }, // Windows] ]: Array); diff --git a/vnext/src/Libraries/Components/Pressable/Pressable.windows.js b/vnext/src/Libraries/Components/Pressable/Pressable.windows.js index baa0355db78..f21414ef245 100644 --- a/vnext/src/Libraries/Components/Pressable/Pressable.windows.js +++ b/vnext/src/Libraries/Components/Pressable/Pressable.windows.js @@ -31,6 +31,7 @@ import type { FocusEvent, KeyEvent, // Windows] } from '../../Types/CoreEventTypes'; +import type {HandledKeyboardEvent} from '../../Components/View/ViewPropTypes'; import View from '../View/View'; import TextInputState from '../TextInput/TextInputState'; @@ -137,6 +138,26 @@ type Props = $ReadOnly<{| */ onKeyUp?: ?(event: KeyEvent) => mixed, + /* + * List of keys handled only by JS. + */ + keyDownEvents?: ?$ReadOnlyArray, + + /* + * List of keys to be handled only by JS. + */ + keyUpEvents?: ?$ReadOnlyArray, + + /* + * Called in the tunneling phase after a key up event is detected. + */ + onKeyDownCapture?: ?(event: KeyEvent) => void, + + /* + * Called in the tunneling phase after a key up event is detected. + */ + onKeyUpCapture?: ?(event: KeyEvent) => void, + /** * Either view styles or a function that receives a boolean reflecting whether * the component is currently pressed and returns view styles. diff --git a/vnext/src/Libraries/Components/TextInput/TextInput.windows.js b/vnext/src/Libraries/Components/TextInput/TextInput.windows.js index ade042bbe69..4cb9200e34c 100644 --- a/vnext/src/Libraries/Components/TextInput/TextInput.windows.js +++ b/vnext/src/Libraries/Components/TextInput/TextInput.windows.js @@ -48,6 +48,7 @@ let RCTMultilineTextInputView; let RCTMultilineTextInputNativeCommands; let WindowsTextInput; // [Windows] let WindowsTextInputCommands; // [Windows] +import type {KeyEvent} from '../../Types/CoreEventTypes'; // [Windows] // [Windows if (Platform.OS === 'android') { @@ -1142,6 +1143,53 @@ function InternalTextInput(props: Props): React.Node { // TextInput handles onBlur and onFocus events // so omitting onBlur and onFocus pressability handlers here. const {onBlur, onFocus, ...eventHandlers} = usePressability(config) || {}; + const eventPhase = Object.freeze({Capturing: 1, Bubbling: 3}); + const _keyDown = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyDownEvents) { + if ( + event.nativeEvent.code == el.code && + el.handledEventPhase == eventPhase.Bubbling + ) { + event.stopPropagation(); + } + } + } + props.onKeyDown && props.onKeyDown(event); + }; + + const _keyUp = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 3) { + event.stopPropagation(); + } + } + } + props.onKeyUp && props.onKeyUp(event); + }; + + const _keyDownCapture = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyDownEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } + } + props.onKeyDownCapture && props.onKeyDownCapture(event); + }; + + const _keyUpCapture = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } + } + props.onKeyUpCapture && props.onKeyUpCapture(event); + }; if (Platform.OS === 'ios') { const RCTTextInputView = @@ -1245,6 +1293,10 @@ function InternalTextInput(props: Props): React.Node { onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue} selection={selection} text={text} + onKeyDown={_keyDown} + onKeyDownCapture={_keyDownCapture} + onKeyUp={_keyUp} + onKeyUpCapture={_keyUpCapture} /> ); } // Windows] diff --git a/vnext/src/Libraries/Components/View/View.windows.js b/vnext/src/Libraries/Components/View/View.windows.js index 0c2e89fe0b9..f441f1e6a13 100644 --- a/vnext/src/Libraries/Components/View/View.windows.js +++ b/vnext/src/Libraries/Components/View/View.windows.js @@ -14,6 +14,9 @@ import ViewNativeComponent from './ViewNativeComponent'; import TextAncestor from '../../Text/TextAncestor'; import * as React from 'react'; import invariant from 'invariant'; // [Windows] +// [Windows +import type {KeyEvent} from '../../Types/CoreEventTypes'; +// Windows] export type Props = ViewProps; @@ -28,6 +31,50 @@ const View: React.AbstractComponent< ViewProps, React.ElementRef, > = React.forwardRef((props: ViewProps, forwardedRef) => { + const _keyDown = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyDownEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 3) { + event.stopPropagation(); + } + } + } + props.onKeyDown && props.onKeyDown(event); + }; + + const _keyUp = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 3) { + event.stopPropagation(); + } + } + } + props.onKeyUp && props.onKeyUp(event); + }; + + const _keyDownCapture = (event: KeyEvent) => { + if (props.keyDownEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyDownEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } + } + props.onKeyDownCapture && props.onKeyDownCapture(event); + }; + + const _keyUpCapture = (event: KeyEvent) => { + if (props.keyUpEvents && event.isPropagationStopped() !== true) { + for (const el of props.keyUpEvents) { + if (event.nativeEvent.code == el.code && el.handledEventPhase == 1) { + event.stopPropagation(); + } + } + } + props.onKeyUpCapture && props.onKeyUpCapture(event); + }; + return ( // [Windows // In core this is a TextAncestor.Provider value={false} See @@ -39,7 +86,16 @@ const View: React.AbstractComponent< !hasTextAncestor, 'Nesting of within is not currently supported.', ); - return ; + return ( + + ); }} // Windows] diff --git a/vnext/src/Libraries/Components/View/ViewPropTypes.windows.js b/vnext/src/Libraries/Components/View/ViewPropTypes.windows.js index 61e0083a576..3203140279e 100644 --- a/vnext/src/Libraries/Components/View/ViewPropTypes.windows.js +++ b/vnext/src/Libraries/Components/View/ViewPropTypes.windows.js @@ -395,9 +395,11 @@ type WindowsViewProps = $ReadOnly<{| * @platform windows */ onKeyUp?: ?(e: KeyEvent) => void, + onKeyUpCapture?: ?(e: KeyEvent) => void, keyUpEvents?: ?$ReadOnlyArray, onKeyDown?: ?(e: KeyEvent) => void, + onKeyDownCapture?: ?(e: KeyEvent) => void, keyDownEvents?: ?$ReadOnlyArray, /** * Specifies the Tooltip for the view