Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Match native View.keyDownEvents behavior in JS.",
"packageName": "react-native-windows",
"email": "igklemen@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<View testID="pressable_feedback_events">
<View
style={[styles.row, styles.centered]}
onKeyDownCapture={event => myKeyDownCapture(event)}>
<Pressable
style={styles.wrapper}
onKeyDown={event => myKeyDown(event)}
onKeyUp={event => myKeyUp(event)}
onPress={() => appendEvent('press')}>
<Text style={styles.button}>Press Me</Text>
</Pressable>
</View>
<View style={styles.eventLogBox}>
{eventLog.map((e, ii) => (
<Text key={ii}>{e}</Text>
))}
</View>
<Switch onValueChange={toggleSwitch2} value={shouldStopPropagation} />
<View
style={styles.row}
onKeyDown={event => logEvent('outer keyDown ' + event.nativeEvent.code)}
onKeyDownCapture={event =>
logEvent('outer keyDownCapture ' + event.nativeEvent.code)
}>
<Pressable
keyDownEvents={[
{code: 'KeyW', handledEventPhase: 3},
{code: 'KeyE', handledEventPhase: 1},
]}
onKeyDown={event => logEvent('keyDown ' + event.nativeEvent.code)}
onKeyDownCapture={event =>
logEvent('keyDownCapture ' + event.nativeEvent.code)
}
onPress={() => {
setTimesPressed(current => current + 1);
logEvent('pressed ' + timesPressed);
}}>
{({pressed}) => (
<Text style={styles.text}>{pressed ? 'Pressed!' : 'Press Me'}</Text>
)}
</Pressable>
</View>

<View style={styles.eventLogBox}>
{eventLog.map((e, ii) => (
<Text key={ii}>{e}</Text>
))}
</View>
</>
);
Expand Down Expand Up @@ -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 <Pressable> is wrapper in a <View>, 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 <PressWithKeyCapture />;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
Slider,
Switch,
} = require('react-native');
const {useState} = React;

const TextInputSharedExamples = require('./TextInputSharedExamples');

Expand Down Expand Up @@ -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 (
<>
<View
focusable
style={styles.row}
keyDownEvents={[
{code: 'KeyW', handledEventPhase: 3},
{code: 'KeyE', handledEventPhase: 1},
]}
onKeyDown={event => logEvent('outer keyDown ' + event.nativeEvent.code)}
onKeyDownCapture={event =>
logEvent('outer keyDownCapture ' + event.nativeEvent.code)
}>
<Text>some text to focus on</Text>
<TextInput
placeholder="Click inside the box to observe events being fired."
style={[styles.singleLineWithHeightTextInput]}
onKeyDown={event =>
logEvent('textinput keyDown ' + event.nativeEvent.code)
}
onKeyUp={event =>
logEvent('textinput keyUp ' + event.nativeEvent.code)
}
keyDownEvents={[
{code: 'KeyW', handledEventPhase: 3},
{code: 'KeyE', handledEventPhase: 1},
]}
/>
</View>
<View style={styles.eventLogBox}>
{eventLog.map((e, ii) => (
<Text key={ii}>{e}</Text>
))}
</View>
</>
);
}

const styles = StyleSheet.create({
multiline: {
height: 60,
Expand Down Expand Up @@ -487,5 +536,11 @@ exports.examples = ([
);
},
},
{
title: 'Stop propagation sample',
render: function(): React.Node {
return <PropagationSample />;
},
},
// Windows]
]: Array<RNTesterExampleModuleItem>);
21 changes: 21 additions & 0 deletions vnext/src/Libraries/Components/Pressable/Pressable.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -137,6 +138,26 @@ type Props = $ReadOnly<{|
*/
onKeyUp?: ?(event: KeyEvent) => mixed,

/*
* List of keys handled only by JS.
*/
keyDownEvents?: ?$ReadOnlyArray<HandledKeyboardEvent>,

/*
* List of keys to be handled only by JS.
*/
keyUpEvents?: ?$ReadOnlyArray<HandledKeyboardEvent>,

/*
* 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.
Expand Down
52 changes: 52 additions & 0 deletions vnext/src/Libraries/Components/TextInput/TextInput.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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]
Expand Down
58 changes: 57 additions & 1 deletion vnext/src/Libraries/Components/View/View.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,6 +31,50 @@ const View: React.AbstractComponent<
ViewProps,
React.ElementRef<typeof ViewNativeComponent>,
> = 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) {
Copy link
Contributor

@jonthysell jonthysell Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a constant somewhere we can use (or define) so as to not require a magic number here (and elsewhere)?

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
Expand All @@ -39,7 +86,16 @@ const View: React.AbstractComponent<
!hasTextAncestor,
'Nesting of <View> within <Text> is not currently supported.',
);
return <ViewNativeComponent {...props} ref={forwardedRef} />;
return (
<ViewNativeComponent
{...props}
ref={forwardedRef}
onKeyDown={_keyDown}
onKeyDownCapture={_keyDownCapture}
onKeyUp={_keyUp}
onKeyUpCapture={_keyUpCapture}
/>
);
}}
</TextAncestor.Consumer>
// Windows]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,11 @@ type WindowsViewProps = $ReadOnly<{|
* @platform windows
*/
onKeyUp?: ?(e: KeyEvent) => void,
onKeyUpCapture?: ?(e: KeyEvent) => void,
keyUpEvents?: ?$ReadOnlyArray<HandledKeyboardEvent>,

onKeyDown?: ?(e: KeyEvent) => void,
onKeyDownCapture?: ?(e: KeyEvent) => void,
keyDownEvents?: ?$ReadOnlyArray<HandledKeyboardEvent>,
/**
* Specifies the Tooltip for the view
Expand Down