Skip to content

Commit 138d442

Browse files
committed
fix(fabric): Add back compat layer for validKeysDown and validKeysUp (microsoft#2879)
Cherry-pick of open PR microsoft#2879. Adds a JS shim that transforms legacy validKeysDown/validKeysUp props into the new onKeyDown/onKeyUp handler format for backward compatibility.
1 parent 59b8964 commit 138d442

6 files changed

Lines changed: 350 additions & 12 deletions

File tree

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,14 @@ function Pressable({
381381
// [macOS
382382
acceptsFirstMouse: acceptsFirstMouse !== false && !disabled,
383383
enableFocusRing: enableFocusRing !== false && !disabled,
384-
keyDownEvents: keyDownEvents ?? [{key: ' '}, {key: 'Enter'}],
384+
keyDownEvents:
385+
keyDownEvents ??
386+
// $FlowFixMe[unclear-type] Legacy props not in type definitions
387+
(((props: any).validKeysDown: mixed) == null &&
388+
// $FlowFixMe[unclear-type]
389+
((props: any).passthroughAllKeyEvents: mixed) !== true
390+
? [{key: ' '}, {key: 'Enter'}]
391+
: undefined),
385392
mouseDownCanMoveWindow: false,
386393
// macOS]
387394
};

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = {
5252
captured: 'onSubmitEditingCapture',
5353
},
5454
},
55+
topKeyDown: {
56+
phasedRegistrationNames: {
57+
bubbled: 'onKeyDown',
58+
captured: 'onKeyDownCapture',
59+
},
60+
},
61+
topKeyUp: {
62+
phasedRegistrationNames: {
63+
bubbled: 'onKeyUp',
64+
captured: 'onKeyUpCapture',
65+
},
66+
},
5567
topTouchCancel: {
5668
phasedRegistrationNames: {
5769
bubbled: 'onTouchCancel',
@@ -173,6 +185,8 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = {
173185
clearTextOnSubmit: true,
174186
grammarCheck: true,
175187
hideVerticalScrollIndicator: true,
188+
keyDownEvents: true,
189+
keyUpEvents: true,
176190
pastedTypes: true,
177191
submitKeyEvents: true,
178192
tooltip: true,
@@ -191,6 +205,8 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = {
191205
onAutoCorrectChange: true,
192206
onSpellCheckChange: true,
193207
onGrammarCheckChange: true,
208+
onKeyDown: true,
209+
onKeyUp: true,
194210
// macOS]
195211
}),
196212
disableKeyboardShortcuts: true,

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ import StyleSheet, {type TextStyleProp} from '../../StyleSheet/StyleSheet';
6161
import Text from '../../Text/Text';
6262
import TextAncestorContext from '../../Text/TextAncestorContext';
6363
import Platform from '../../Utilities/Platform';
64+
// [macOS
65+
import processLegacyKeyProps, {
66+
hasLegacyKeyProps,
67+
stripLegacyKeyProps,
68+
} from '../../Utilities/normalizeLegacyHandledKeyEvents';
69+
// macOS]
6470
import useMergeRefs from '../../Utilities/useMergeRefs';
6571
import TextInputState from './TextInputState';
6672
import invariant from 'invariant';
@@ -386,6 +392,23 @@ function useTextInputStateSynchronization({
386392
*
387393
*/
388394
function InternalTextInput(props: TextInputProps): React.Node {
395+
// [macOS Legacy keyboard event compat — to remove, delete this block and its import
396+
const usingLegacyKeyboardProps = hasLegacyKeyProps(props);
397+
// $FlowFixMe[unclear-type]
398+
const effectiveProps: any = usingLegacyKeyboardProps ? ({...props}: any) : props;
399+
if (usingLegacyKeyboardProps) {
400+
stripLegacyKeyProps(effectiveProps);
401+
const legacy = processLegacyKeyProps(props);
402+
effectiveProps.keyDownEvents = legacy.keyDownEvents;
403+
effectiveProps.keyUpEvents = legacy.keyUpEvents;
404+
if (legacy.onKeyDown != null) {
405+
effectiveProps.onKeyDown = legacy.onKeyDown;
406+
}
407+
if (legacy.onKeyUp != null) {
408+
effectiveProps.onKeyUp = legacy.onKeyUp;
409+
}
410+
}
411+
// macOS]
389412
const {
390413
'aria-busy': ariaBusy,
391414
'aria-checked': ariaChecked,
@@ -400,7 +423,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
400423
selectionHandleColor,
401424
cursorColor,
402425
...otherProps
403-
} = props;
426+
} = effectiveProps;
404427

405428
const inputRef = useRef<null | TextInputInstance>(null);
406429

@@ -582,7 +605,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
582605

583606
// [macOS
584607
const _onKeyDown = (event: KeyEvent) => {
585-
const keyDownEvents = props.keyDownEvents;
608+
const keyDownEvents = effectiveProps.keyDownEvents;
586609
if (keyDownEvents != null && !event.isPropagationStopped()) {
587610
const isHandled = keyDownEvents.some(
588611
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
@@ -599,11 +622,11 @@ function InternalTextInput(props: TextInputProps): React.Node {
599622
event.stopPropagation();
600623
}
601624
}
602-
props.onKeyDown?.(event);
625+
effectiveProps.onKeyDown?.(event);
603626
};
604627

605628
const _onKeyUp = (event: KeyEvent) => {
606-
const keyUpEvents = props.keyUpEvents;
629+
const keyUpEvents = effectiveProps.keyUpEvents;
607630
if (keyUpEvents != null && !event.isPropagationStopped()) {
608631
const isHandled = keyUpEvents.some(
609632
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
@@ -620,7 +643,7 @@ function InternalTextInput(props: TextInputProps): React.Node {
620643
event.stopPropagation();
621644
}
622645
}
623-
props.onKeyUp?.(event);
646+
effectiveProps.onKeyUp?.(event);
624647
};
625648
// macOS]
626649

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

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import type {ViewProps} from './ViewPropTypes';
1313

1414
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
1515
import TextAncestorContext from '../../Text/TextAncestorContext';
16+
// [macOS
17+
import processLegacyKeyProps, {
18+
hasLegacyKeyProps,
19+
stripLegacyKeyProps,
20+
} from '../../Utilities/normalizeLegacyHandledKeyEvents';
21+
// macOS]
1622
import ViewNativeComponent from './ViewNativeComponent';
1723
import * as React from 'react';
1824
import {use} from 'react';
@@ -30,11 +36,29 @@ component View(
3036
) {
3137
const hasTextAncestor = use(TextAncestorContext);
3238

39+
// [macOS Legacy keyboard event compat — to remove, delete this block and its import
40+
const usingLegacyKeyboardProps = hasLegacyKeyProps(props);
41+
// $FlowFixMe[unclear-type]
42+
const effectiveProps: any = usingLegacyKeyboardProps ? ({...props}: any) : props;
43+
if (usingLegacyKeyboardProps) {
44+
stripLegacyKeyProps(effectiveProps);
45+
const legacy = processLegacyKeyProps(props);
46+
effectiveProps.keyDownEvents = legacy.keyDownEvents;
47+
effectiveProps.keyUpEvents = legacy.keyUpEvents;
48+
if (legacy.onKeyDown != null) {
49+
effectiveProps.onKeyDown = legacy.onKeyDown;
50+
}
51+
if (legacy.onKeyUp != null) {
52+
effectiveProps.onKeyUp = legacy.onKeyUp;
53+
}
54+
}
55+
// macOS]
56+
3357
let actualView;
3458

3559
// [macOS
3660
const _onKeyDown = (event: KeyEvent) => {
37-
const keyDownEvents = props.keyDownEvents;
61+
const keyDownEvents = effectiveProps.keyDownEvents;
3862
if (keyDownEvents != null && !event.isPropagationStopped()) {
3963
const isHandled = keyDownEvents.some(
4064
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
@@ -51,11 +75,11 @@ component View(
5175
event.stopPropagation();
5276
}
5377
}
54-
props.onKeyDown?.(event);
78+
effectiveProps.onKeyDown?.(event);
5579
};
5680

5781
const _onKeyUp = (event: KeyEvent) => {
58-
const keyUpEvents = props.keyUpEvents;
82+
const keyUpEvents = effectiveProps.keyUpEvents;
5983
if (keyUpEvents != null && !event.isPropagationStopped()) {
6084
const isHandled = keyUpEvents.some(
6185
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => {
@@ -72,7 +96,7 @@ component View(
7296
event.stopPropagation();
7397
}
7498
}
75-
props.onKeyUp?.(event);
99+
effectiveProps.onKeyUp?.(event);
76100
};
77101
// macOS]
78102

@@ -96,7 +120,7 @@ component View(
96120
id,
97121
tabIndex,
98122
...otherProps
99-
} = props;
123+
} = effectiveProps;
100124

101125
// Since we destructured props, we can now treat it as mutable
102126
const processedProps = otherProps as {...ViewProps};
@@ -195,7 +219,7 @@ component View(
195219
nativeID,
196220
tabIndex,
197221
...otherProps
198-
} = props;
222+
} = effectiveProps;
199223
const _accessibilityLabelledBy =
200224
ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy;
201225

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
// [macOS]
12+
// Legacy validKeysDown/validKeysUp/passthroughAllKeyEvents compat layer.
13+
// When removing legacy support, delete this file and its call sites.
14+
15+
import type {HandledKeyEvent, KeyEvent} from '../Types/CoreEventTypes';
16+
17+
type LegacyHandledKeyEvent = string | HandledKeyEvent;
18+
19+
function expandKey(entry: LegacyHandledKeyEvent): Array<HandledKeyEvent> {
20+
if (typeof entry !== 'string') {
21+
return [entry];
22+
}
23+
const out: Array<HandledKeyEvent> = [];
24+
const bools: Array<boolean> = [false, true];
25+
for (const metaKey of bools) {
26+
for (const ctrlKey of bools) {
27+
for (const altKey of bools) {
28+
for (const shiftKey of bools) {
29+
out.push({altKey, ctrlKey, key: entry, metaKey, shiftKey});
30+
}
31+
}
32+
}
33+
}
34+
return out;
35+
}
36+
37+
function normalize(
38+
legacy: ?$ReadOnlyArray<LegacyHandledKeyEvent>,
39+
): void | Array<HandledKeyEvent> {
40+
if (legacy == null) {
41+
return undefined;
42+
}
43+
const result: Array<HandledKeyEvent> = [];
44+
for (const entry of legacy) {
45+
result.push(...expandKey(entry));
46+
}
47+
return result;
48+
}
49+
50+
function matchesEvent(
51+
events: $ReadOnlyArray<HandledKeyEvent>,
52+
event: KeyEvent,
53+
): boolean {
54+
return events.some(
55+
({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) =>
56+
event.nativeEvent.key === key &&
57+
Boolean(metaKey) === event.nativeEvent.metaKey &&
58+
Boolean(ctrlKey) === event.nativeEvent.ctrlKey &&
59+
Boolean(altKey) === event.nativeEvent.altKey &&
60+
Boolean(shiftKey) === event.nativeEvent.shiftKey,
61+
);
62+
}
63+
64+
export type LegacyKeyResult = {
65+
keyDownEvents: void | Array<HandledKeyEvent>,
66+
keyUpEvents: void | Array<HandledKeyEvent>,
67+
onKeyDown: void | ((event: KeyEvent) => void),
68+
onKeyUp: void | ((event: KeyEvent) => void),
69+
};
70+
71+
/**
72+
* Returns true if the props contain legacy key props that need processing.
73+
*/
74+
export function hasLegacyKeyProps(props: mixed): boolean {
75+
// $FlowFixMe[unclear-type]
76+
const p = (props: any);
77+
return (
78+
p.validKeysDown != null ||
79+
p.validKeysUp != null ||
80+
p.passthroughAllKeyEvents != null
81+
);
82+
}
83+
84+
/**
85+
* Strips legacy props from a props object (mutates).
86+
*/
87+
export function stripLegacyKeyProps(props: {+[string]: mixed}): void {
88+
// $FlowFixMe[unclear-type]
89+
const p = (props: any);
90+
delete p.validKeysDown;
91+
delete p.validKeysUp;
92+
delete p.passthroughAllKeyEvents;
93+
}
94+
95+
/**
96+
* Processes legacy validKeysDown/validKeysUp/passthroughAllKeyEvents props
97+
* and returns the equivalent modern keyDownEvents/keyUpEvents and wrapped
98+
* onKeyDown/onKeyUp handlers.
99+
*
100+
* Usage in component:
101+
* if (hasLegacyKeyProps(props)) {
102+
* const legacy = processLegacyKeyProps(props);
103+
* // use legacy.keyDownEvents, legacy.onKeyDown, etc.
104+
* }
105+
*/
106+
export default function processLegacyKeyProps(
107+
// $FlowFixMe[unclear-type]
108+
props: any,
109+
): LegacyKeyResult {
110+
const validKeysDown: ?$ReadOnlyArray<LegacyHandledKeyEvent> =
111+
props.validKeysDown;
112+
const validKeysUp: ?$ReadOnlyArray<LegacyHandledKeyEvent> =
113+
props.validKeysUp;
114+
const passthroughAllKeyEvents: ?boolean = props.passthroughAllKeyEvents;
115+
116+
const hasModernKeyDown = props.keyDownEvents != null;
117+
const hasModernKeyUp = props.keyUpEvents != null;
118+
const legacyPassthrough =
119+
passthroughAllKeyEvents === true && !hasModernKeyDown;
120+
121+
const gateKeyDown =
122+
!hasModernKeyDown && validKeysDown != null && !legacyPassthrough;
123+
const gateKeyUp =
124+
!hasModernKeyUp && validKeysUp != null && !legacyPassthrough;
125+
126+
const normalizedDown =
127+
props.keyDownEvents ?? normalize(validKeysDown);
128+
const normalizedUp =
129+
props.keyUpEvents ?? normalize(validKeysUp);
130+
131+
const keyDownEvents = legacyPassthrough ? undefined : normalizedDown;
132+
const keyUpEvents = legacyPassthrough ? undefined : normalizedUp;
133+
134+
const onKeyDown =
135+
props.onKeyDown != null
136+
? (event: KeyEvent) => {
137+
let isHandled = false;
138+
if (normalizedDown != null && !event.isPropagationStopped()) {
139+
isHandled = matchesEvent(normalizedDown, event);
140+
if (isHandled && hasModernKeyDown) {
141+
event.stopPropagation();
142+
}
143+
}
144+
if (!gateKeyDown || isHandled) {
145+
props.onKeyDown?.(event);
146+
}
147+
}
148+
: undefined;
149+
150+
const onKeyUp =
151+
props.onKeyUp != null
152+
? (event: KeyEvent) => {
153+
let isHandled = false;
154+
if (normalizedUp != null && !event.isPropagationStopped()) {
155+
isHandled = matchesEvent(normalizedUp, event);
156+
if (isHandled && hasModernKeyUp) {
157+
event.stopPropagation();
158+
}
159+
}
160+
if (!gateKeyUp || isHandled) {
161+
props.onKeyUp?.(event);
162+
}
163+
}
164+
: undefined;
165+
166+
return {keyDownEvents, keyUpEvents, onKeyDown, onKeyUp};
167+
}

0 commit comments

Comments
 (0)