Skip to content

Commit 55dbdfe

Browse files
Saadnajmitido64
andauthored
fix: keyDownEvents stops propogation, default events for Pressable (#2720)
## Summary: Two changes: - Similar to microsoft/react-native-windows#8077 , we would like to have JS event propagation (as well as native) stop if `keyDownEvents` or `keyUpEvents` is specified. - Add `Space` and `Enter` as default keyDown events for Pressable, to more closely match a button. ## Test Plan: Updated the test page: ![Screenshot 2025-10-07 at 5 29 00 PM](https://github.com/user-attachments/assets/9b2b5360-3b44-4221-b3ef-6856feb551f1) --------- Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com>
1 parent e6c7bbd commit 55dbdfe

File tree

6 files changed

+321
-71
lines changed

6 files changed

+321
-71
lines changed

packages/react-native/Libraries/Components/Pressable/Pressable.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,6 @@ function Pressable(
308308
onKeyDown,
309309
onKeyUp,
310310
keyDownEvents,
311-
keyUpEvents,
312311
acceptsFirstMouse,
313312
mouseDownCanMoveWindow,
314313
enableFocusRing,
@@ -356,9 +355,6 @@ function Pressable(
356355
const restPropsWithDefaults: React.ElementConfig<typeof View> = {
357356
...restProps,
358357
...android_rippleConfig?.viewProps,
359-
acceptsFirstMouse: acceptsFirstMouse !== false && !disabled, // [macOS]
360-
mouseDownCanMoveWindow: false, // [macOS]
361-
enableFocusRing: enableFocusRing !== false && !disabled,
362358
accessible: accessible !== false,
363359
accessibilityViewIsModal:
364360
restProps['aria-modal'] ?? restProps.accessibilityViewIsModal,
@@ -368,6 +364,12 @@ function Pressable(
368364
focusable: focusable !== false && !disabled, // macOS]
369365
accessibilityValue,
370366
hitSlop,
367+
// [macOS
368+
acceptsFirstMouse: acceptsFirstMouse !== false && !disabled,
369+
enableFocusRing: enableFocusRing !== false && !disabled,
370+
keyDownEvents: keyDownEvents ?? [{key: ' '}, {key: 'Enter'}],
371+
mouseDownCanMoveWindow: false,
372+
// macOS]
371373
};
372374

373375
const config = useMemo(

packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ exports[`<Pressable /> should render as expected: should deep render when mocked
2424
collapsable={false}
2525
enableFocusRing={true}
2626
focusable={true}
27+
keyDownEvents={
28+
Array [
29+
Object {
30+
"key": " ",
31+
},
32+
Object {
33+
"key": "Enter",
34+
},
35+
]
36+
}
2737
mouseDownCanMoveWindow={false}
2838
onBlur={[Function]}
2939
onClick={[Function]}
@@ -63,6 +73,16 @@ exports[`<Pressable /> should render as expected: should deep render when not mo
6373
collapsable={false}
6474
enableFocusRing={true}
6575
focusable={true}
76+
keyDownEvents={
77+
Array [
78+
Object {
79+
"key": " ",
80+
},
81+
Object {
82+
"key": "Enter",
83+
},
84+
]
85+
}
6686
mouseDownCanMoveWindow={false}
6787
onBlur={[Function]}
6888
onClick={[Function]}
@@ -102,6 +122,16 @@ exports[`<Pressable disabled={true} /> should be disabled when disabled is true:
102122
collapsable={false}
103123
enableFocusRing={false}
104124
focusable={false}
125+
keyDownEvents={
126+
Array [
127+
Object {
128+
"key": " ",
129+
},
130+
Object {
131+
"key": "Enter",
132+
},
133+
]
134+
}
105135
mouseDownCanMoveWindow={false}
106136
onBlur={[Function]}
107137
onClick={[Function]}
@@ -141,6 +171,16 @@ exports[`<Pressable disabled={true} /> should be disabled when disabled is true:
141171
collapsable={false}
142172
enableFocusRing={false}
143173
focusable={false}
174+
keyDownEvents={
175+
Array [
176+
Object {
177+
"key": " ",
178+
},
179+
Object {
180+
"key": "Enter",
181+
},
182+
]
183+
}
144184
mouseDownCanMoveWindow={false}
145185
onBlur={[Function]}
146186
onClick={[Function]}
@@ -180,6 +220,16 @@ exports[`<Pressable disabled={true} accessibilityState={{}} /> should be disable
180220
collapsable={false}
181221
enableFocusRing={false}
182222
focusable={false}
223+
keyDownEvents={
224+
Array [
225+
Object {
226+
"key": " ",
227+
},
228+
Object {
229+
"key": "Enter",
230+
},
231+
]
232+
}
183233
mouseDownCanMoveWindow={false}
184234
onBlur={[Function]}
185235
onClick={[Function]}
@@ -219,6 +269,16 @@ exports[`<Pressable disabled={true} accessibilityState={{}} /> should be disable
219269
collapsable={false}
220270
enableFocusRing={false}
221271
focusable={false}
272+
keyDownEvents={
273+
Array [
274+
Object {
275+
"key": " ",
276+
},
277+
Object {
278+
"key": "Enter",
279+
},
280+
]
281+
}
222282
mouseDownCanMoveWindow={false}
223283
onBlur={[Function]}
224284
onClick={[Function]}
@@ -258,6 +318,16 @@ exports[`<Pressable disabled={true} accessibilityState={{checked: true}} /> shou
258318
collapsable={false}
259319
enableFocusRing={false}
260320
focusable={false}
321+
keyDownEvents={
322+
Array [
323+
Object {
324+
"key": " ",
325+
},
326+
Object {
327+
"key": "Enter",
328+
},
329+
]
330+
}
261331
mouseDownCanMoveWindow={false}
262332
onBlur={[Function]}
263333
onClick={[Function]}
@@ -297,6 +367,16 @@ exports[`<Pressable disabled={true} accessibilityState={{checked: true}} /> shou
297367
collapsable={false}
298368
enableFocusRing={false}
299369
focusable={false}
370+
keyDownEvents={
371+
Array [
372+
Object {
373+
"key": " ",
374+
},
375+
Object {
376+
"key": "Enter",
377+
},
378+
]
379+
}
300380
mouseDownCanMoveWindow={false}
301381
onBlur={[Function]}
302382
onClick={[Function]}
@@ -336,6 +416,16 @@ exports[`<Pressable disabled={true} accessibilityState={{disabled: false}} /> sh
336416
collapsable={false}
337417
enableFocusRing={false}
338418
focusable={false}
419+
keyDownEvents={
420+
Array [
421+
Object {
422+
"key": " ",
423+
},
424+
Object {
425+
"key": "Enter",
426+
},
427+
]
428+
}
339429
mouseDownCanMoveWindow={false}
340430
onBlur={[Function]}
341431
onClick={[Function]}
@@ -375,6 +465,16 @@ exports[`<Pressable disabled={true} accessibilityState={{disabled: false}} /> sh
375465
collapsable={false}
376466
enableFocusRing={false}
377467
focusable={false}
468+
keyDownEvents={
469+
Array [
470+
Object {
471+
"key": " ",
472+
},
473+
Object {
474+
"key": "Enter",
475+
},
476+
]
477+
}
378478
mouseDownCanMoveWindow={false}
379479
onBlur={[Function]}
380480
onClick={[Function]}

packages/react-native/Libraries/Components/TextInput/TextInput.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {HostInstance} from '../../../src/private/types/HostInstance';
1212
import type {____TextStyle_Internal as TextStyleInternal} from '../../StyleSheet/StyleSheetTypes';
1313
import type {
1414
GestureResponderEvent,
15+
KeyEvent, // [macOS]
16+
HandledKeyEvent, // [macOS]
1517
NativeSyntheticEvent,
1618
ScrollEvent,
1719
} from '../../Types/CoreEventTypes';
@@ -1134,6 +1136,34 @@ export type TextInputProps = $ReadOnly<{
11341136
* unwanted edits without flicker.
11351137
*/
11361138
value?: ?Stringish,
1139+
1140+
// [macOS
1141+
/**
1142+
* An array of key events that should be handled by the TextInput.
1143+
* When a key event matches one of these specifications, event propagation will be stopped.
1144+
* @platform macos
1145+
*/
1146+
keyDownEvents?: ?$ReadOnlyArray<HandledKeyEvent>,
1147+
1148+
/**
1149+
* An array of key events that should be handled by the TextInput.
1150+
* When a key event matches one of these specifications, event propagation will be stopped.
1151+
* @platform macos
1152+
*/
1153+
keyUpEvents?: ?$ReadOnlyArray<HandledKeyEvent>,
1154+
1155+
/**
1156+
* Callback that is called when a key is pressed down.
1157+
* @platform macos
1158+
*/
1159+
onKeyDown?: ?(e: KeyEvent) => mixed,
1160+
1161+
/**
1162+
* Callback that is called when a key is released.
1163+
* @platform macos
1164+
*/
1165+
onKeyUp?: ?(e: KeyEvent) => mixed,
1166+
// macOS]
11371167
}>;
11381168

11391169
type ViewCommands = $NonMaybeType<
@@ -1640,6 +1670,50 @@ function InternalTextInput(props: TextInputProps): React.Node {
16401670
props.onScroll && props.onScroll(event);
16411671
};
16421672

1673+
// [macOS
1674+
const _onKeyDown = (event: KeyEvent) => {
1675+
const keyDownEvents = props.keyDownEvents;
1676+
if (keyDownEvents != null && !event.isPropagationStopped()) {
1677+
const isHandled = keyDownEvents.some(
1678+
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
1679+
return (
1680+
event.nativeEvent.key === key &&
1681+
Boolean(metaKey) === event.nativeEvent.metaKey &&
1682+
Boolean(ctrlKey) === event.nativeEvent.ctrlKey &&
1683+
Boolean(altKey) === event.nativeEvent.altKey &&
1684+
Boolean(shiftKey) === event.nativeEvent.shiftKey
1685+
);
1686+
},
1687+
);
1688+
if (isHandled === true) {
1689+
event.stopPropagation();
1690+
}
1691+
}
1692+
props.onKeyDown?.(event);
1693+
};
1694+
1695+
const _onKeyUp = (event: KeyEvent) => {
1696+
const keyUpEvents = props.keyUpEvents;
1697+
if (keyUpEvents != null && !event.isPropagationStopped()) {
1698+
const isHandled = keyUpEvents.some(
1699+
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
1700+
return (
1701+
event.nativeEvent.key === key &&
1702+
Boolean(metaKey) === event.nativeEvent.metaKey &&
1703+
Boolean(ctrlKey) === event.nativeEvent.ctrlKey &&
1704+
Boolean(altKey) === event.nativeEvent.altKey &&
1705+
Boolean(shiftKey) === event.nativeEvent.shiftKey
1706+
);
1707+
},
1708+
);
1709+
if (isHandled === true) {
1710+
event.stopPropagation();
1711+
}
1712+
}
1713+
props.onKeyUp?.(event);
1714+
};
1715+
// macOS]
1716+
16431717
let textInput = null;
16441718

16451719
const multiline = props.multiline ?? false;
@@ -1795,8 +1869,9 @@ function InternalTextInput(props: TextInputProps): React.Node {
17951869
onChange={_onChange}
17961870
onContentSizeChange={props.onContentSizeChange}
17971871
onFocus={_onFocus}
1798-
onKeyDown={props.onKeyDown} // [macOS]
1799-
onKeyUp={props.onKeyUp} // [macOS]
1872+
// $FlowFixMe[exponential-spread]
1873+
{...(otherProps.onKeyDown && {onKeyDown: _onKeyDown})} // [macOS]
1874+
{...(otherProps.onKeyUp && {onKeyUp: _onKeyUp})} // [macOS]
18001875
onScroll={_onScroll}
18011876
onSelectionChange={_onSelectionChange}
18021877
onSelectionChangeShouldSetResponder={emptyFunctionThatReturnsTrue}

packages/react-native/Libraries/Components/View/View.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {ViewProps} from './ViewPropTypes';
1313
import TextAncestor from '../../Text/TextAncestor';
1414
import ViewNativeComponent from './ViewNativeComponent';
1515
import * as React from 'react';
16+
import type {KeyEvent, HandledKeyEvent} from '../../Types/CoreEventTypes'; // [macOS]
1617

1718
export type Props = ViewProps;
1819

@@ -94,6 +95,50 @@ const View: component(
9495
};
9596
}
9697

98+
// [macOS
99+
const _onKeyDown = (event: KeyEvent) => {
100+
const keyDownEvents = otherProps.keyDownEvents;
101+
if (keyDownEvents != null && !event.isPropagationStopped()) {
102+
const isHandled = keyDownEvents.some(
103+
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
104+
return (
105+
event.nativeEvent.key === key &&
106+
Boolean(metaKey) === event.nativeEvent.metaKey &&
107+
Boolean(ctrlKey) === event.nativeEvent.ctrlKey &&
108+
Boolean(altKey) === event.nativeEvent.altKey &&
109+
Boolean(shiftKey) === event.nativeEvent.shiftKey
110+
);
111+
},
112+
);
113+
if (isHandled === true) {
114+
event.stopPropagation();
115+
}
116+
}
117+
otherProps.onKeyDown?.(event);
118+
};
119+
120+
const _onKeyUp = (event: KeyEvent) => {
121+
const keyUpEvents = otherProps.keyUpEvents;
122+
if (keyUpEvents != null && !event.isPropagationStopped()) {
123+
const isHandled = keyUpEvents.some(
124+
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
125+
return (
126+
event.nativeEvent.key === key &&
127+
Boolean(metaKey) === event.nativeEvent.metaKey &&
128+
Boolean(ctrlKey) === event.nativeEvent.ctrlKey &&
129+
Boolean(altKey) === event.nativeEvent.altKey &&
130+
Boolean(shiftKey) === event.nativeEvent.shiftKey
131+
);
132+
},
133+
);
134+
if (isHandled === true) {
135+
event.stopPropagation();
136+
}
137+
}
138+
otherProps.onKeyUp?.(event);
139+
};
140+
// macOS]
141+
97142
const actualView = (
98143
<ViewNativeComponent
99144
{...otherProps}
@@ -112,6 +157,9 @@ const View: component(
112157
: importantForAccessibility
113158
}
114159
nativeID={id ?? nativeID}
160+
// $FlowFixMe[exponential-spread]
161+
{...(otherProps.onKeyDown && {onKeyDown: _onKeyDown})} // [macOS]
162+
{...(otherProps.onKeyUp && {onKeyUp: _onKeyUp})} // [macOS]
115163
ref={forwardedRef}
116164
/>
117165
);

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3429,6 +3429,10 @@ export type TextInputProps = $ReadOnly<{
34293429
submitBehavior?: ?SubmitBehavior,
34303430
style?: ?TextStyleProp,
34313431
value?: ?Stringish,
3432+
keyDownEvents?: ?$ReadOnlyArray<HandledKeyEvent>,
3433+
keyUpEvents?: ?$ReadOnlyArray<HandledKeyEvent>,
3434+
onKeyDown?: ?(e: KeyEvent) => mixed,
3435+
onKeyUp?: ?(e: KeyEvent) => mixed,
34323436
}>;
34333437
export type TextInputComponentStatics = $ReadOnly<{
34343438
State: $ReadOnly<{

0 commit comments

Comments
 (0)