diff --git a/.nx/version-plans/version-plan-1743631200000.md b/.nx/version-plans/version-plan-1743631200000.md new file mode 100644 index 000000000000..72eb948aa6d8 --- /dev/null +++ b/.nx/version-plans/version-plan-1743631200000.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +Backport fixes: default Text color to labelColor, Text overflow in ScrollView, validKeys compat layer diff --git a/package.json b/package.json index 3df4f2bf3490..fe20649f8b9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-macos/monorepo", - "version": "0.81.4", + "version": "0.81.5", "license": "MIT", "packageManager": "yarn@4.13.0", "scripts": { diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index ece22fb83dcc..72266bd2fa7e 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -381,7 +381,14 @@ function Pressable({ // [macOS acceptsFirstMouse: acceptsFirstMouse !== false && !disabled, enableFocusRing: enableFocusRing !== false && !disabled, - keyDownEvents: keyDownEvents ?? [{key: ' '}, {key: 'Enter'}], + keyDownEvents: + keyDownEvents ?? + // $FlowFixMe[unclear-type] Legacy props not in type definitions + (((props: any).validKeysDown: mixed) == null && + // $FlowFixMe[unclear-type] + ((props: any).passthroughAllKeyEvents: mixed) !== true + ? [{key: ' '}, {key: 'Enter'}] + : undefined), mouseDownCanMoveWindow: false, // macOS] }; diff --git a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 1c24dfa93f82..253c2a49e4ed 100644 --- a/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/packages/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -52,6 +52,18 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { captured: 'onSubmitEditingCapture', }, }, + topKeyDown: { + phasedRegistrationNames: { + bubbled: 'onKeyDown', + captured: 'onKeyDownCapture', + }, + }, + topKeyUp: { + phasedRegistrationNames: { + bubbled: 'onKeyUp', + captured: 'onKeyUpCapture', + }, + }, topTouchCancel: { phasedRegistrationNames: { bubbled: 'onTouchCancel', @@ -173,6 +185,8 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { clearTextOnSubmit: true, grammarCheck: true, hideVerticalScrollIndicator: true, + keyDownEvents: true, + keyUpEvents: true, pastedTypes: true, submitKeyEvents: true, tooltip: true, @@ -191,6 +205,8 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { onAutoCorrectChange: true, onSpellCheckChange: true, onGrammarCheckChange: true, + onKeyDown: true, + onKeyUp: true, // macOS] }), disableKeyboardShortcuts: true, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index a2572f4be88e..1ae22ed66776 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -60,7 +60,13 @@ import flattenStyle from '../../StyleSheet/flattenStyle'; import StyleSheet, {type TextStyleProp} from '../../StyleSheet/StyleSheet'; import Text from '../../Text/Text'; import TextAncestorContext from '../../Text/TextAncestorContext'; +// [macOS +import processLegacyKeyProps, { + hasLegacyKeyProps, + stripLegacyKeyProps, +} from '../../Utilities/normalizeLegacyHandledKeyEvents'; import Platform from '../../Utilities/Platform'; +// macOS] import useMergeRefs from '../../Utilities/useMergeRefs'; import TextInputState from './TextInputState'; import invariant from 'invariant'; @@ -386,6 +392,11 @@ function useTextInputStateSynchronization({ * */ function InternalTextInput(props: TextInputProps): React.Node { + // [macOS + const legacyKeyOverrides = hasLegacyKeyProps(props) + ? processLegacyKeyProps(props) + : null; + // macOS] const { 'aria-busy': ariaBusy, 'aria-checked': ariaChecked, @@ -400,7 +411,9 @@ function InternalTextInput(props: TextInputProps): React.Node { selectionHandleColor, cursorColor, ...otherProps - } = props; + // $FlowFixMe[unclear-type] + } = ({...props, ...legacyKeyOverrides}: any); // [macOS] + stripLegacyKeyProps(otherProps); // [macOS] const inputRef = useRef(null); @@ -581,10 +594,15 @@ function InternalTextInput(props: TextInputProps): React.Node { }; // [macOS + const _keyDownEvents = + legacyKeyOverrides?.keyDownEvents ?? props.keyDownEvents; + const _keyUpEvents = legacyKeyOverrides?.keyUpEvents ?? props.keyUpEvents; + const _origOnKeyDown = legacyKeyOverrides?.onKeyDown ?? props.onKeyDown; + const _origOnKeyUp = legacyKeyOverrides?.onKeyUp ?? props.onKeyUp; + const _onKeyDown = (event: KeyEvent) => { - const keyDownEvents = props.keyDownEvents; - if (keyDownEvents != null && !event.isPropagationStopped()) { - const isHandled = keyDownEvents.some( + if (_keyDownEvents != null && !event.isPropagationStopped()) { + const isHandled = _keyDownEvents.some( ({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => { return ( event.nativeEvent.key === key && @@ -599,13 +617,12 @@ function InternalTextInput(props: TextInputProps): React.Node { event.stopPropagation(); } } - props.onKeyDown?.(event); + _origOnKeyDown?.(event); }; const _onKeyUp = (event: KeyEvent) => { - const keyUpEvents = props.keyUpEvents; - if (keyUpEvents != null && !event.isPropagationStopped()) { - const isHandled = keyUpEvents.some( + if (_keyUpEvents != null && !event.isPropagationStopped()) { + const isHandled = _keyUpEvents.some( ({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => { return ( event.nativeEvent.key === key && @@ -620,7 +637,7 @@ function InternalTextInput(props: TextInputProps): React.Node { event.stopPropagation(); } } - props.onKeyUp?.(event); + _origOnKeyUp?.(event); }; // macOS] diff --git a/packages/react-native/Libraries/Components/View/View.js b/packages/react-native/Libraries/Components/View/View.js index fe619c977b57..4ba5a82df911 100644 --- a/packages/react-native/Libraries/Components/View/View.js +++ b/packages/react-native/Libraries/Components/View/View.js @@ -13,6 +13,12 @@ import type {ViewProps} from './ViewPropTypes'; import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; import TextAncestorContext from '../../Text/TextAncestorContext'; +// [macOS +import processLegacyKeyProps, { + hasLegacyKeyProps, + stripLegacyKeyProps, +} from '../../Utilities/normalizeLegacyHandledKeyEvents'; +// macOS] import ViewNativeComponent from './ViewNativeComponent'; import * as React from 'react'; import {use} from 'react'; @@ -30,13 +36,24 @@ component View( ) { const hasTextAncestor = use(TextAncestorContext); + // [macOS + const legacyKeyOverrides = hasLegacyKeyProps(props) + ? processLegacyKeyProps(props) + : null; + // macOS] + let actualView; // [macOS + const _keyDownEvents = + legacyKeyOverrides?.keyDownEvents ?? props.keyDownEvents; + const _keyUpEvents = legacyKeyOverrides?.keyUpEvents ?? props.keyUpEvents; + const _origOnKeyDown = legacyKeyOverrides?.onKeyDown ?? props.onKeyDown; + const _origOnKeyUp = legacyKeyOverrides?.onKeyUp ?? props.onKeyUp; + const _onKeyDown = (event: KeyEvent) => { - const keyDownEvents = props.keyDownEvents; - if (keyDownEvents != null && !event.isPropagationStopped()) { - const isHandled = keyDownEvents.some( + if (_keyDownEvents != null && !event.isPropagationStopped()) { + const isHandled = _keyDownEvents.some( ({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => { return ( event.nativeEvent.key === key && @@ -51,13 +68,12 @@ component View( event.stopPropagation(); } } - props.onKeyDown?.(event); + _origOnKeyDown?.(event); }; const _onKeyUp = (event: KeyEvent) => { - const keyUpEvents = props.keyUpEvents; - if (keyUpEvents != null && !event.isPropagationStopped()) { - const isHandled = keyUpEvents.some( + if (_keyUpEvents != null && !event.isPropagationStopped()) { + const isHandled = _keyUpEvents.some( ({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => { return ( event.nativeEvent.key === key && @@ -72,7 +88,7 @@ component View( event.stopPropagation(); } } - props.onKeyUp?.(event); + _origOnKeyUp?.(event); }; // macOS] @@ -96,10 +112,12 @@ component View( id, tabIndex, ...otherProps - } = props; + // $FlowFixMe[unclear-type] + } = ({...props, ...legacyKeyOverrides}: any); // [macOS] // Since we destructured props, we can now treat it as mutable const processedProps = otherProps as {...ViewProps}; + stripLegacyKeyProps(processedProps); // [macOS] const parsedAriaLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g); if (parsedAriaLabelledBy !== undefined) { @@ -195,7 +213,9 @@ component View( nativeID, tabIndex, ...otherProps - } = props; + // $FlowFixMe[unclear-type] + } = ({...props, ...legacyKeyOverrides}: any); // [macOS] + stripLegacyKeyProps(otherProps); // [macOS] const _accessibilityLabelledBy = ariaLabelledBy?.split(/\s*,\s*/g) ?? accessibilityLabelledBy; diff --git a/packages/react-native/Libraries/Text/RCTTextAttributes.mm b/packages/react-native/Libraries/Text/RCTTextAttributes.mm index 9b9622bad08e..59323a868ff8 100644 --- a/packages/react-native/Libraries/Text/RCTTextAttributes.mm +++ b/packages/react-native/Libraries/Text/RCTTextAttributes.mm @@ -277,7 +277,7 @@ - (CGFloat)effectiveFontSizeMultiplier - (RCTUIColor *)effectiveForegroundColor // [macOS] { - RCTUIColor *effectiveForegroundColor = _foregroundColor ?: [RCTUIColor blackColor]; // [macOS] + RCTUIColor *effectiveForegroundColor = _foregroundColor ?: [RCTTextAttributes defaultForegroundColor]; // [macOS] if (!isnan(_opacity)) { effectiveForegroundColor = diff --git a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 2929b16bbcc5..3ddc6016b5de 100644 --- a/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -69,7 +69,7 @@ - (instancetype)initWithFrame:(CGRect)frame _textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self]; self.backgroundColor = [RCTUIColor clearColor]; // [macOS] - self.textColor = [RCTUIColor blackColor]; // [macOS] + self.textColor = [RCTUIColor labelColor]; // [macOS] // This line actually removes 5pt (default value) left and right padding in UITextView. #if !TARGET_OS_OSX // [macOS] self.textContainer.lineFragmentPadding = 0; diff --git a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index 207b5d51986d..c4422af316b4 100644 --- a/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -127,7 +127,7 @@ - (void)enforceTextAttributesIfNeeded NSDictionary *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy]; if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) { - [textAttributes setValue:[RCTUIColor blackColor] forKey:NSForegroundColorAttributeName]; // [macOS] + [textAttributes setValue:[RCTUIColor labelColor] forKey:NSForegroundColorAttributeName]; // [macOS] } backedTextInputView.defaultTextAttributes = textAttributes; diff --git a/packages/react-native/Libraries/Utilities/normalizeLegacyHandledKeyEvents.js b/packages/react-native/Libraries/Utilities/normalizeLegacyHandledKeyEvents.js new file mode 100644 index 000000000000..3d8ac4ed0896 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/normalizeLegacyHandledKeyEvents.js @@ -0,0 +1,164 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// [macOS] +// Legacy validKeysDown/validKeysUp/passthroughAllKeyEvents compat layer. +// When removing legacy support, delete this file and its call sites. + +import type {HandledKeyEvent, KeyEvent} from '../Types/CoreEventTypes'; + +type LegacyHandledKeyEvent = string | HandledKeyEvent; + +function expandKey(entry: LegacyHandledKeyEvent): Array { + if (typeof entry !== 'string') { + return [entry]; + } + const out: Array = []; + const bools: Array = [false, true]; + for (const metaKey of bools) { + for (const ctrlKey of bools) { + for (const altKey of bools) { + for (const shiftKey of bools) { + out.push({altKey, ctrlKey, key: entry, metaKey, shiftKey}); + } + } + } + } + return out; +} + +function normalize( + legacy: ?$ReadOnlyArray, +): void | Array { + if (legacy == null) { + return undefined; + } + const result: Array = []; + for (const entry of legacy) { + result.push(...expandKey(entry)); + } + return result; +} + +function matchesEvent( + events: $ReadOnlyArray, + event: KeyEvent, +): boolean { + return events.some( + ({key, metaKey, ctrlKey, altKey, shiftKey}: HandledKeyEvent) => + event.nativeEvent.key === key && + Boolean(metaKey) === event.nativeEvent.metaKey && + Boolean(ctrlKey) === event.nativeEvent.ctrlKey && + Boolean(altKey) === event.nativeEvent.altKey && + Boolean(shiftKey) === event.nativeEvent.shiftKey, + ); +} + +export type LegacyKeyResult = { + keyDownEvents: void | Array, + keyUpEvents: void | Array, + onKeyDown: void | ((event: KeyEvent) => void), + onKeyUp: void | ((event: KeyEvent) => void), +}; + +/** + * Returns true if the props contain legacy key props that need processing. + */ +export function hasLegacyKeyProps(props: mixed): boolean { + // $FlowFixMe[unclear-type] + const p = (props: any); + return ( + p.validKeysDown != null || + p.validKeysUp != null || + p.passthroughAllKeyEvents != null + ); +} + +/** + * Strips legacy props from a props object (mutates). + */ +export function stripLegacyKeyProps(props: {+[string]: mixed}): void { + // $FlowFixMe[unclear-type] + const p = (props: any); + delete p.validKeysDown; + delete p.validKeysUp; + delete p.passthroughAllKeyEvents; +} + +/** + * Processes legacy validKeysDown/validKeysUp/passthroughAllKeyEvents props + * and returns the equivalent modern keyDownEvents/keyUpEvents and wrapped + * onKeyDown/onKeyUp handlers. + * + * Usage in component: + * if (hasLegacyKeyProps(props)) { + * const legacy = processLegacyKeyProps(props); + * // use legacy.keyDownEvents, legacy.onKeyDown, etc. + * } + */ +export default function processLegacyKeyProps( + // $FlowFixMe[unclear-type] + props: any, +): LegacyKeyResult { + const validKeysDown: ?$ReadOnlyArray = + props.validKeysDown; + const validKeysUp: ?$ReadOnlyArray = props.validKeysUp; + const passthroughAllKeyEvents: ?boolean = props.passthroughAllKeyEvents; + + const hasModernKeyDown = props.keyDownEvents != null; + const hasModernKeyUp = props.keyUpEvents != null; + const legacyPassthrough = + passthroughAllKeyEvents === true && !hasModernKeyDown; + + const gateKeyDown = + !hasModernKeyDown && validKeysDown != null && !legacyPassthrough; + const gateKeyUp = + !hasModernKeyUp && validKeysUp != null && !legacyPassthrough; + + const normalizedDown = props.keyDownEvents ?? normalize(validKeysDown); + const normalizedUp = props.keyUpEvents ?? normalize(validKeysUp); + + const keyDownEvents = legacyPassthrough ? undefined : normalizedDown; + const keyUpEvents = legacyPassthrough ? undefined : normalizedUp; + + const onKeyDown = + props.onKeyDown != null + ? (event: KeyEvent) => { + let isHandled = false; + if (normalizedDown != null && !event.isPropagationStopped()) { + isHandled = matchesEvent(normalizedDown, event); + if (isHandled && hasModernKeyDown) { + event.stopPropagation(); + } + } + if (!gateKeyDown || isHandled) { + props.onKeyDown?.(event); + } + } + : undefined; + + const onKeyUp = + props.onKeyUp != null + ? (event: KeyEvent) => { + let isHandled = false; + if (normalizedUp != null && !event.isPropagationStopped()) { + isHandled = matchesEvent(normalizedUp, event); + if (isHandled && hasModernKeyUp) { + event.stopPropagation(); + } + } + if (!gateKeyUp || isHandled) { + props.onKeyUp?.(event); + } + } + : undefined; + + return {keyDownEvents, keyUpEvents, onKeyDown, onKeyUp}; +} diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 8c309adba96c..4a80bdf5b26b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -184,6 +184,30 @@ - (void)dealloc #endif // [macOS] } +#if TARGET_OS_OSX // [macOS +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // On macOS, the _containerView is the NSScrollView's documentView and has autoresizingMask set so + // it fills the visible area before React's first layout pass. However, AppKit's autoresizing can + // corrupt the documentView's frame by adding the NSClipView's size delta to the container's + // dimensions (e.g., during initial tile or window resize), inflating it well beyond the correct + // content size. This produces massive horizontal and vertical overflow on first render. + // + // After React has set the content size via updateState:, we reset the documentView frame here to + // undo any autoresizing corruption. This runs after AppKit's layout (which triggers autoresizing), + // so it reliably corrects the frame. + if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) { + CGRect containerFrame = _containerView.frame; + if (!CGSizeEqualToSize(containerFrame.size, _contentSize)) { + containerFrame.size = _contentSize; + _containerView.frame = containerFrame; + } + } +} +#endif // macOS] + #if TARGET_OS_IOS - (void)_registerKeyboardListener { diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index 2982e5923693..f042d94777e6 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -178,7 +178,7 @@ TextAttributes TextAttributes::defaultTextAttributes() { static auto textAttributes = [] { auto textAttributes = TextAttributes{}; // Non-obvious (can be different among platforms) default text attributes. - textAttributes.foregroundColor = blackColor(); + textAttributes.foregroundColor = defaultForegroundTextColor(); // [macOS] textAttributes.backgroundColor = clearColor(); textAttributes.fontSize = 14.0; textAttributes.fontSizeMultiplier = 1.0; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Color.h b/packages/react-native/ReactCommon/react/renderer/graphics/Color.h index 5c2dd9aa509b..6fed98d9d9e7 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Color.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Color.h @@ -70,6 +70,7 @@ SharedColor colorFromRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a); SharedColor clearColor(); SharedColor blackColor(); SharedColor whiteColor(); +SharedColor defaultForegroundTextColor(); // [macOS] #ifdef RN_SERIALIZABLE_STATE inline folly::dynamic toDynamic(const SharedColor& sharedColor) { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 2b78e8e7f2cc..f54641e7484d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -10,6 +10,7 @@ #import #import // [macOS] #import +#import // [macOS] #import #import #import @@ -342,6 +343,16 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) return Color(wrapManagedObject(semanticColor)); } +// [macOS +SharedColor defaultForegroundTextColor() { + static SharedColor color = [] { + std::vector items = {"labelColor"}; + return SharedColor(Color::createSemanticColor(items)); + }(); + return color; +} +// macOS] + } // namespace facebook::react NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm index 4ce0166e34ce..8080fad2e8ce 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm @@ -144,7 +144,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex inline static RCTUIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes) // [macOS] { - RCTUIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ? RCTUIColorFromSharedColor(textAttributes.foregroundColor) : [RCTUIColor blackColor]; // [macOS] + RCTUIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [RCTUIColor labelColor]; // [macOS] if (!isnan(textAttributes.opacity)) { effectiveForegroundColor = [effectiveForegroundColor diff --git a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js index 6a5024e836b4..cad16accd852 100644 --- a/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js +++ b/packages/rn-tester/js/examples/KeyboardEventsExample/KeyboardEventsExample.js @@ -293,6 +293,101 @@ function KeyboardEventExample(): React.Node { ); } +/** + * Tests legacy validKeysDown/validKeysUp compat layer. + * Exercises the JS shim that translates legacy props to modern keyDownEvents. + */ +function LegacyValidKeysExample(): React.Node { + const ref1 = React.useRef | null>(null); + const ref2 = React.useRef | null>(null); + const ref3 = React.useRef | null>(null); + const [eventLog, setEventLog] = React.useState>([]); + + function appendEvent(eventName: string, source?: string) { + const limit = 12; + setEventLog((current: Array) => { + const prefix = source != null ? `${source}: ` : ''; + return [`${prefix}${eventName}`].concat(current.slice(0, limit - 1)); + }); + } + + return ( + + + These components use the legacy validKeysDown / validKeysUp props. The + JS compat layer converts them to modern keyDownEvents / keyUpEvents + under the hood. + + + + + validKeysDown: ['a', 'b', 'Enter'] (string format) + + {/* $FlowFixMe[prop-missing] Legacy props not in type definitions */} + ref1.current?.focus()} + onKeyDown={(event: KeyEvent) => { + appendEvent(`keyDown: ${formatKeyEvent(event)}`, 'String keys'); + }}> + + Click to focus — press 'a', 'b', or Enter + + + + + + + validKeysDown: [Cmd+s, Ctrl+z] (object format) + + {/* $FlowFixMe[prop-missing] Legacy props not in type definitions */} + ref2.current?.focus()} + onKeyDown={(event: KeyEvent) => { + appendEvent(`keyDown: ${formatKeyEvent(event)}`, 'Modifier keys'); + }}> + + Click to focus — press Cmd+S or Ctrl+Z + + + + + + + passthroughAllKeyEvents + validKeysDown: ['Enter'] + + {/* $FlowFixMe[prop-missing] Legacy props not in type definitions */} + ref3.current?.focus()} + onKeyDown={(event: KeyEvent) => { + appendEvent(`keyDown: ${formatKeyEvent(event)}`, 'Passthrough'); + }}> + + Click to focus — ALL keys should log + + + + + setEventLog([])} /> + + ); +} + const styles = StyleSheet.create({ eventLogBox: { padding: 10, @@ -468,4 +563,10 @@ exports.examples = [ return ; }, }, + { + title: 'Legacy validKeysDown / validKeysUp compat', + render: function (): React.Node { + return ; + }, + }, ];