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" +} 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 efa658f0b0b..63b78c5e7f0 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -358,6 +358,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 5ef36e3cfbb..1fef2318463 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. */ @@ -245,6 +253,7 @@ function Pressable({ delayLongPress, disabled, focusable, + focusOnPress, // Windows hitSlop, onBlur, onFocus, @@ -316,6 +325,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, @@ -332,7 +351,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPress: onPressWithFocus, onPressIn(event: GestureResponderEvent): void { if (android_rippleConfig != null) { android_rippleConfig.onPressIn(event); @@ -378,7 +397,7 @@ function Pressable({ onHoverIn, onHoverOut, onLongPress, - onPress, + onPressWithFocus, onPressIn, onPressMove, onPressOut,