From 558a1fc88a7e8426196c8d332802596e714e21ad Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:31:16 -0800 Subject: [PATCH 1/3] Pressables should take focus on press --- vnext/overrides.json | 18 +- .../Components/Pressable/Pressable.d.ts | 175 ++++++++++++++++++ .../Components/Pressable/Pressable.windows.js | 23 ++- 3 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts diff --git a/vnext/overrides.json b/vnext/overrides.json index dde793f56ae..b18095c4121 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -234,18 +234,18 @@ "baseHash": "aabf033b596a472c9da8b3eb4ab82d2a55acd5da", "issue": 4054 }, - { - "type": "derived", - "file": "src-win/index.windows.js.flow", - "baseFile": "packages/react-native/index.js.flow", - "baseHash": "bb6c7873aa350c235aad233dd75656237d16d79c" - }, { "type": "derived", "file": "src-win/index.windows.js", "baseFile": "packages/react-native/index.js", "baseHash": "c5d0dfd40d0fb7c197790b1f23f4f8b9ca835047" }, + { + "type": "derived", + "file": "src-win/index.windows.js.flow", + "baseFile": "packages/react-native/index.js.flow", + "baseHash": "bb6c7873aa350c235aad233dd75656237d16d79c" + }, { "type": "platform", "file": "src-win/IntegrationTests/BlobTest.js" @@ -386,6 +386,12 @@ "type": "platform", "file": "src-win/Libraries/Components/Popup/PopupNativeComponent.js" }, + { + "type": "derived", + "file": "src-win/Libraries/Components/Pressable/Pressable.d.ts", + "baseFile": "packages/react-native/Libraries/Components/Pressable/Pressable.d.ts", + "baseHash": "d4dd8fc82436617bfeca9807be274aa5416d748c" + }, { "type": "patch", "file": "src-win/Libraries/Components/Pressable/Pressable.windows.js", diff --git a/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts b/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts new file mode 100644 index 00000000000..334ba2c9816 --- /dev/null +++ b/vnext/src-win/Libraries/Components/Pressable/Pressable.d.ts @@ -0,0 +1,175 @@ +/** + * 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. + * + * @format + */ + +import type * as React from 'react'; +import {Insets} from '../../../types/public/Insets'; +import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet'; +import {ViewStyle} from '../../StyleSheet/StyleSheetTypes'; +import { + GestureResponderEvent, + MouseEvent, + NativeSyntheticEvent, + TargetedEvent, +} from '../../Types/CoreEventTypes'; +import {View} from '../View/View'; +import {AccessibilityProps} from '../View/ViewAccessibility'; +import {ViewProps} from '../View/ViewPropTypes'; + +export interface PressableStateCallbackType { + readonly pressed: boolean; +} + +export interface PressableAndroidRippleConfig { + color?: null | ColorValue | undefined; + borderless?: null | boolean | undefined; + radius?: null | number | undefined; + foreground?: null | boolean | undefined; +} + +export interface PressableProps + extends AccessibilityProps, + Omit { + /** + * Called when the hover is activated to provide visual feedback. + */ + onHoverIn?: null | ((event: MouseEvent) => void) | undefined; + + /** + * Called when the hover is deactivated to undo visual feedback. + */ + onHoverOut?: null | ((event: MouseEvent) => void) | undefined; + + /** + * Called when a single tap gesture is detected. + */ + onPress?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a touch is engaged before `onPress`. + */ + onPressIn?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a touch is released before `onPress`. + */ + onPressOut?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called when a long-tap gesture is detected. + */ + onLongPress?: null | ((event: GestureResponderEvent) => void) | undefined; + + /** + * Called after the element loses focus. + * @platform macos windows + */ + onBlur?: + | null + | ((event: NativeSyntheticEvent) => void) + | undefined; + + /** + * Called after the element is focused. + * @platform macos windows + */ + onFocus?: + | null + | ((event: NativeSyntheticEvent) => void) + | undefined; + + /** + * Either children or a render prop that receives a boolean reflecting whether + * the component is currently pressed. + */ + children?: + | React.ReactNode + | ((state: PressableStateCallbackType) => React.ReactNode) + | undefined; + + /** + * Whether a press gesture can be interrupted by a parent gesture such as a + * scroll event. Defaults to true. + */ + cancelable?: null | boolean | undefined; + + /** + * Duration to wait after hover in before calling `onHoverIn`. + * @platform macos windows + */ + delayHoverIn?: number | null | undefined; + + /** + * Duration to wait after hover out before calling `onHoverOut`. + * @platform macos windows + */ + delayHoverOut?: number | null | undefined; + + /** + * Duration (in milliseconds) from `onPressIn` before `onLongPress` is called. + */ + delayLongPress?: null | number | undefined; + + /** + * Whether the press behavior is disabled. + */ + disabled?: null | boolean | undefined; + + //[ Windows + /** + * When the pressable is pressed it will take focus + * Default value: true + */ + focusOnPress?: null | boolean | undefined; + // Windows] + + /** + * Additional distance outside of this view in which a press is detected. + */ + hitSlop?: null | Insets | number | undefined; + + /** + * Additional distance outside of this view in which a touch is considered a + * press before `onPressOut` is triggered. + */ + pressRetentionOffset?: null | Insets | number | undefined; + + /** + * If true, doesn't play system sound on touch. + */ + android_disableSound?: null | boolean | undefined; + + /** + * Enables the Android ripple effect and configures its color. + */ + android_ripple?: null | PressableAndroidRippleConfig | undefined; + + /** + * Used only for documentation or testing (e.g. snapshot testing). + */ + testOnly_pressed?: null | boolean | undefined; + + /** + * Either view styles or a function that receives a boolean reflecting whether + * the component is currently pressed and returns view styles. + */ + style?: + | StyleProp + | ((state: PressableStateCallbackType) => StyleProp) + | undefined; + + /** + * Duration (in milliseconds) to wait after press down before calling onPressIn. + */ + unstable_pressDelay?: number | undefined; +} + +// TODO use React.AbstractComponent when available +export const Pressable: React.ForwardRefExoticComponent< + PressableProps & React.RefAttributes +>; diff --git a/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js b/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js index b5aa60f12af..487fa72a60f 100644 --- a/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js +++ b/vnext/src-win/Libraries/Components/Pressable/Pressable.windows.js @@ -71,6 +71,14 @@ type PressableBaseProps = $ReadOnly<{ */ disabled?: ?boolean, + // [Windows + /** + * When the pressable is pressed it will take focus + * Default value: true + */ + focusOnPress?: ?boolean, + // Windows] + /** * Additional distance outside of this view in which a press is detected. */ @@ -238,6 +246,7 @@ function Pressable({ delayLongPress, disabled, focusable, + focusOnPress, // Windows hitSlop, onBlur, onFocus, @@ -309,6 +318,16 @@ function Pressable({ hitSlop, }; + const onPressWithFocus = React.useCallback( + (args: GestureResponderEvent) => { + if (focusable !== false && focusOnPress !== false) { + viewRef?.current?.focus(); + } + onPress?.(args); + }, + [focusOnPress, onPress, focusable], + ); + const config = useMemo( () => ({ cancelable, @@ -325,7 +344,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPress: onPressWithFocus, onPressIn(event: GestureResponderEvent): void { if (android_rippleConfig != null) { android_rippleConfig.onPressIn(event); @@ -369,7 +388,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPressWithFocus, onPressIn, onPressMove, onPressOut, From b561d9fcfd7cbd561ecb1c9ff723f568aa3dfa3c Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:31:26 -0800 Subject: [PATCH 2/3] Change files --- ...ative-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json diff --git a/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json new file mode 100644 index 00000000000..b9a7adab163 --- /dev/null +++ b/change/react-native-windows-e3c55f20-cfa0-4c06-876b-b0d90e525adf.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Pressables should take focus on press", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From a3f289134bc741440b76b3af58f907d1a97b0aa0 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:40:17 -0800 Subject: [PATCH 3/3] fix --- .../PressableComponentTest.test.ts.snap | 23 +++++++++++++++++++ vnext/overrides.json | 6 ----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap index d6c6fc8d640..5c7e39210f7 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap @@ -846,6 +846,13 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Name": "pressOut", "TextRangePattern.GetText": "pressOut", }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "focus", + "TextRangePattern.GetText": "focus", + }, { "AutomationId": "", "ControlType": 50020, @@ -891,6 +898,10 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", "_Props": {}, }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, ], }, "Visual Tree": { @@ -991,6 +1002,18 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] }, ], }, + { + "Offset": "11, 85, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, } diff --git a/vnext/overrides.json b/vnext/overrides.json index 2a7c45d400f..63b78c5e7f0 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -218,12 +218,6 @@ "baseFile": "packages/react-native/index.js.flow", "baseHash": "8ee0df254428851107836dbb7a2fa9033c92216e" }, - { - "type": "derived", - "file": "src-win/index.windows.js.flow", - "baseFile": "packages/react-native/index.js.flow", - "baseHash": "bb6c7873aa350c235aad233dd75656237d16d79c" - }, { "type": "platform", "file": "src-win/IntegrationTests/BlobTest.js"