first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
/**
* 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<ViewProps, 'children' | 'style' | 'hitSlop'> {
/**
* 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<TargetedEvent>) => void)
| undefined;
/**
* Called after the element is focused.
* @platform macos windows
*/
onFocus?:
| null
| ((event: NativeSyntheticEvent<TargetedEvent>) => 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;
/**
* 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<ViewStyle>
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
| 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<View>
>;

View File

@@ -0,0 +1,358 @@
/**
* 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
*/
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {
GestureResponderEvent,
LayoutChangeEvent,
MouseEvent,
} from '../../Types/CoreEventTypes';
import type {ViewProps} from '../View/ViewPropTypes';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import usePressability from '../../Pressability/usePressability';
import {type RectOrSize} from '../../StyleSheet/Rect';
import useMergeRefs from '../../Utilities/useMergeRefs';
import View from '../View/View';
import useAndroidRippleForView, {
type PressableAndroidRippleConfig,
} from './useAndroidRippleForView';
import * as React from 'react';
import {memo, useMemo, useRef, useState} from 'react';
export type {PressableAndroidRippleConfig};
export type PressableStateCallbackType = $ReadOnly<{
pressed: boolean,
}>;
type PressableBaseProps = $ReadOnly<{
/**
* Whether a press gesture can be interrupted by a parent gesture such as a
* scroll event. Defaults to true.
*/
cancelable?: ?boolean,
/**
* Either children or a render prop that receives a boolean reflecting whether
* the component is currently pressed.
*/
children?: React.Node | ((state: PressableStateCallbackType) => React.Node),
/**
* Duration to wait after hover in before calling `onHoverIn`.
*/
delayHoverIn?: ?number,
/**
* Duration to wait after hover out before calling `onHoverOut`.
*/
delayHoverOut?: ?number,
/**
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
*/
delayLongPress?: ?number,
/**
* Whether the press behavior is disabled.
*/
disabled?: ?boolean,
/**
* Additional distance outside of this view in which a press is detected.
*/
hitSlop?: ?RectOrSize,
/**
* Additional distance outside of this view in which a touch is considered a
* press before `onPressOut` is triggered.
*/
pressRetentionOffset?: ?RectOrSize,
/**
* Called when this view's layout changes.
*/
onLayout?: ?(event: LayoutChangeEvent) => mixed,
/**
* Called when the hover is activated to provide visual feedback.
*/
onHoverIn?: ?(event: MouseEvent) => mixed,
/**
* Called when the hover is deactivated to undo visual feedback.
*/
onHoverOut?: ?(event: MouseEvent) => mixed,
/**
* Called when a long-tap gesture is detected.
*/
onLongPress?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a single tap gesture is detected.
*/
onPress?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a touch is engaged before `onPress`.
*/
onPressIn?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when the press location moves.
*/
onPressMove?: ?(event: GestureResponderEvent) => mixed,
/**
* Called when a touch is released before `onPress`.
*/
onPressOut?: ?(event: GestureResponderEvent) => mixed,
/**
* Whether to prevent any other native components from becoming responder
* while this pressable is responder.
*/
blockNativeResponder?: ?boolean,
/**
* Either view styles or a function that receives a boolean reflecting whether
* the component is currently pressed and returns view styles.
*/
style?:
| ViewStyleProp
| ((state: PressableStateCallbackType) => ViewStyleProp),
/**
* Identifier used to find this view in tests.
*/
testID?: ?string,
/**
* If true, doesn't play system sound on touch.
*/
android_disableSound?: ?boolean,
/**
* Enables the Android ripple effect and configures its color.
*/
android_ripple?: ?PressableAndroidRippleConfig,
/**
* Used only for documentation or testing (e.g. snapshot testing).
*/
testOnly_pressed?: ?boolean,
/**
* Duration to wait after press down before calling `onPressIn`.
*/
unstable_pressDelay?: ?number,
}>;
export type PressableProps = $ReadOnly<{
// Pressability may override `onMouseEnter` and `onMouseLeave` to
// implement `onHoverIn` and `onHoverOut` in a platform-agnostic way.
// Hover events should be used instead of mouse events.
...Omit<ViewProps, 'onMouseEnter' | 'onMouseLeave'>,
...PressableBaseProps,
}>;
type Instance = React.ElementRef<typeof View>;
/**
* Component used to build display components that should respond to whether the
* component is currently pressed or not.
*/
function Pressable({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<Instance>,
...PressableProps,
}): React.Node {
const {
accessible,
accessibilityState,
'aria-live': ariaLive,
android_disableSound,
android_ripple,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
blockNativeResponder,
cancelable,
children,
delayHoverIn,
delayHoverOut,
delayLongPress,
disabled,
focusable,
hitSlop,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn,
onPressMove,
onPressOut,
pressRetentionOffset,
style,
testOnly_pressed,
unstable_pressDelay,
...restProps
} = props;
const viewRef = useRef<Instance | null>(null);
const mergedRef = useMergeRefs(forwardedRef, viewRef);
const android_rippleConfig = useAndroidRippleForView(android_ripple, viewRef);
const [pressed, setPressed] = usePressState(testOnly_pressed === true);
const shouldUpdatePressed =
typeof children === 'function' || typeof style === 'function';
let _accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
_accessibilityState =
disabled != null ? {..._accessibilityState, disabled} : _accessibilityState;
const accessibilityValue = {
max: props['aria-valuemax'] ?? props.accessibilityValue?.max,
min: props['aria-valuemin'] ?? props.accessibilityValue?.min,
now: props['aria-valuenow'] ?? props.accessibilityValue?.now,
text: props['aria-valuetext'] ?? props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
ariaLive === 'off' ? 'none' : (ariaLive ?? props.accessibilityLiveRegion);
const accessibilityLabel = ariaLabel ?? props.accessibilityLabel;
const restPropsWithDefaults: React.ElementConfig<typeof View> = {
...restProps,
...android_rippleConfig?.viewProps,
accessible: accessible !== false,
accessibilityViewIsModal:
restProps['aria-modal'] ?? restProps.accessibilityViewIsModal,
accessibilityLiveRegion,
accessibilityLabel,
accessibilityState: _accessibilityState,
focusable: focusable !== false,
accessibilityValue,
hitSlop,
};
const config = useMemo(
() => ({
cancelable,
disabled,
hitSlop,
pressRectOffset: pressRetentionOffset,
android_disableSound,
delayHoverIn,
delayHoverOut,
delayLongPress,
delayPressIn: unstable_pressDelay,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn(event: GestureResponderEvent): void {
if (android_rippleConfig != null) {
android_rippleConfig.onPressIn(event);
}
shouldUpdatePressed && setPressed(true);
if (onPressIn != null) {
onPressIn(event);
}
},
onPressMove(event: GestureResponderEvent): void {
android_rippleConfig?.onPressMove(event);
if (onPressMove != null) {
onPressMove(event);
}
},
onPressOut(event: GestureResponderEvent): void {
if (android_rippleConfig != null) {
android_rippleConfig.onPressOut(event);
}
shouldUpdatePressed && setPressed(false);
if (onPressOut != null) {
onPressOut(event);
}
},
blockNativeResponder,
}),
[
android_disableSound,
android_rippleConfig,
blockNativeResponder,
cancelable,
delayHoverIn,
delayHoverOut,
delayLongPress,
disabled,
hitSlop,
onBlur,
onFocus,
onHoverIn,
onHoverOut,
onLongPress,
onPress,
onPressIn,
onPressMove,
onPressOut,
pressRetentionOffset,
setPressed,
shouldUpdatePressed,
unstable_pressDelay,
],
);
const eventHandlers = usePressability(config);
return (
<View
{...restPropsWithDefaults}
{...eventHandlers}
ref={mergedRef}
style={typeof style === 'function' ? style({pressed}) : style}
collapsable={false}>
{typeof children === 'function' ? children({pressed}) : children}
{__DEV__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
</View>
);
}
function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
const [pressed, setPressed] = useState(false);
return [pressed || forcePressed, setPressed];
}
const MemoedPressable = memo(Pressable);
MemoedPressable.displayName = 'Pressable';
export default (MemoedPressable: component(
ref?: React.RefSetter<React.ElementRef<typeof View>>,
...props: PressableProps
));

View File

@@ -0,0 +1,109 @@
/**
* 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
*/
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import processColor from '../../StyleSheet/processColor';
import Platform from '../../Utilities/Platform';
import View from '../View/View';
import {Commands} from '../View/ViewNativeComponent';
import invariant from 'invariant';
import * as React from 'react';
import {useMemo} from 'react';
type NativeBackgroundProp = $ReadOnly<{
type: 'RippleAndroid',
color: ?number,
borderless: boolean,
rippleRadius: ?number,
}>;
export type PressableAndroidRippleConfig = {
color?: ColorValue,
borderless?: boolean,
radius?: number,
foreground?: boolean,
};
/**
* Provides the event handlers and props for configuring the ripple effect on
* supported versions of Android.
*/
export default function useAndroidRippleForView(
rippleConfig: ?PressableAndroidRippleConfig,
viewRef: {current: null | React.ElementRef<typeof View>},
): ?$ReadOnly<{
onPressIn: (event: GestureResponderEvent) => void,
onPressMove: (event: GestureResponderEvent) => void,
onPressOut: (event: GestureResponderEvent) => void,
viewProps:
| $ReadOnly<{nativeBackgroundAndroid: NativeBackgroundProp}>
| $ReadOnly<{nativeForegroundAndroid: NativeBackgroundProp}>,
}> {
const {color, borderless, radius, foreground} = rippleConfig ?? {};
return useMemo(() => {
if (
Platform.OS === 'android' &&
(color != null || borderless != null || radius != null)
) {
const processedColor = processColor(color);
invariant(
processedColor == null || typeof processedColor === 'number',
'Unexpected color given for Ripple color',
);
const nativeRippleValue = {
type: 'RippleAndroid',
color: processedColor,
borderless: borderless === true,
rippleRadius: radius,
};
return {
viewProps:
foreground === true
? // $FlowFixMe[incompatible-type]
{nativeForegroundAndroid: nativeRippleValue}
: // $FlowFixMe[incompatible-type]
{nativeBackgroundAndroid: nativeRippleValue},
onPressIn(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.hotspotUpdate(
view,
event.nativeEvent.locationX ?? 0,
event.nativeEvent.locationY ?? 0,
);
Commands.setPressed(view, true);
}
},
onPressMove(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.hotspotUpdate(
view,
event.nativeEvent.locationX ?? 0,
event.nativeEvent.locationY ?? 0,
);
}
},
onPressOut(event: GestureResponderEvent): void {
const view = viewRef.current;
if (view != null) {
Commands.setPressed(view, false);
}
},
};
}
return null;
}, [borderless, color, foreground, radius, viewRef]);
}