From 57c1e67e4c5eef86dc122d966e7b85dfa804550b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Wed, 7 Jan 2026 14:43:06 +0100 Subject: [PATCH 01/22] stepping stone --- .../react-native-gesture-handler/src/index.ts | 4 +- .../src/v3/components/Pressable/Pressable.tsx | 393 ++++++++++++++++++ .../components/Pressable/PressableProps.tsx | 180 ++++++++ .../v3/components/Pressable/StateMachine.tsx | 54 +++ .../src/v3/components/Pressable/index.ts | 5 + .../components/Pressable/stateDefinitions.ts | 126 ++++++ .../src/v3/components/Pressable/utils.ts | 135 ++++++ 7 files changed, 895 insertions(+), 2 deletions(-) create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts create mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 1e657867dd..962700f5a0 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -149,8 +149,8 @@ export type { export type { PressableProps, PressableStateCallbackType, -} from './components/Pressable'; -export { default as Pressable } from './components/Pressable'; +} from './v3/components/Pressable'; +export { default as Pressable } from './v3/components/Pressable'; export type { GestureTouchEvent as SingleGestureTouchEvent } from './handlers/gestureHandlerCommon'; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx new file mode 100644 index 0000000000..213cae2bfd --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -0,0 +1,393 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { + PressableEvent, + PressableProps, + FullPressableDimensions, +} from './PressableProps'; +import { + Insets, + LayoutChangeEvent, + Platform, + StyleProp, + ViewStyle, +} from 'react-native'; +import { + addInsets, + numberAsInset, + gestureTouchToPressableEvent, + isTouchWithinInset, + gestureToPressableEvent, +} from './utils'; + +import { getStatesConfig, StateMachineEvent } from './stateDefinitions'; +import { PressableStateMachine } from './StateMachine'; +import { + useHoverGesture, + useLongPressGesture, + useNativeGesture, + useSimultaneousGestures, +} from '../../hooks'; +import { GestureDetector } from '../../detectors'; +import { PureNativeButton } from '../GestureButtons'; + +import { PressabilityDebugView } from '../../../handlers/PressabilityDebugView'; +import { INT32_MAX } from '../../../utils'; +const DEFAULT_LONG_PRESS_DURATION = 500; +// const IS_TEST_ENV = isTestEnv(); + +const Pressable = (props: PressableProps) => { + const { + testOnly_pressed, + hitSlop, + pressRetentionOffset, + delayHoverIn, + delayHoverOut, + delayLongPress, + unstable_pressDelay, + onHoverIn, + onHoverOut, + onPress, + onPressIn, + onPressOut, + onLongPress, + onLayout, + style, + children, + android_disableSound, + android_ripple, + disabled, + accessible, + simultaneousWith, + requireToFail, + block, + ...remainingProps + } = props; + + const [pressedState, setPressedState] = useState(testOnly_pressed ?? false); + + const longPressTimeoutRef = useRef(null); + const pressDelayTimeoutRef = useRef(null); + const isOnPressAllowed = useRef(true); + const isCurrentlyPressed = useRef(false); + const dimensions = useRef({ + width: 0, + height: 0, + x: 0, + y: 0, + }); + + const normalizedHitSlop: Insets = useMemo( + () => + typeof hitSlop === 'number' + ? numberAsInset(hitSlop) + : (hitSlop ?? numberAsInset(0)), + [hitSlop] + ); + const normalizedPressRetentionOffset: Insets = useMemo( + () => + typeof pressRetentionOffset === 'number' + ? numberAsInset(pressRetentionOffset) + : (pressRetentionOffset ?? {}), + [pressRetentionOffset] + ); + const appliedHitSlop = addInsets( + normalizedHitSlop, + normalizedPressRetentionOffset + ); + + const cancelLongPress = useCallback(() => { + if (longPressTimeoutRef.current) { + clearTimeout(longPressTimeoutRef.current); + longPressTimeoutRef.current = null; + isOnPressAllowed.current = true; + } + }, []); + + const cancelDelayedPress = useCallback(() => { + if (pressDelayTimeoutRef.current) { + clearTimeout(pressDelayTimeoutRef.current); + pressDelayTimeoutRef.current = null; + } + }, []); + + const startLongPress = useCallback( + (event: PressableEvent) => { + if (onLongPress) { + cancelLongPress(); + longPressTimeoutRef.current = setTimeout(() => { + isOnPressAllowed.current = false; + onLongPress(event); + }, delayLongPress ?? DEFAULT_LONG_PRESS_DURATION); + } + }, + [onLongPress, cancelLongPress, delayLongPress] + ); + const innerHandlePressIn = useCallback( + (event: PressableEvent) => { + onPressIn?.(event); + startLongPress(event); + setPressedState(true); + if (pressDelayTimeoutRef.current) { + clearTimeout(pressDelayTimeoutRef.current); + pressDelayTimeoutRef.current = null; + } + }, + [onPressIn, startLongPress] + ); + + const handleFinalize = useCallback(() => { + isCurrentlyPressed.current = false; + cancelLongPress(); + cancelDelayedPress(); + setPressedState(false); + }, [cancelDelayedPress, cancelLongPress]); + + const handlePressIn = useCallback( + (event: PressableEvent) => { + if ( + !isTouchWithinInset( + dimensions.current, + normalizedHitSlop, + event.nativeEvent.changedTouches.at(-1) + ) + ) { + // Ignoring pressIn within pressRetentionOffset + return; + } + + isCurrentlyPressed.current = true; + if (unstable_pressDelay) { + pressDelayTimeoutRef.current = setTimeout(() => { + innerHandlePressIn(event); + }, unstable_pressDelay); + } else { + innerHandlePressIn(event); + } + }, + [innerHandlePressIn, normalizedHitSlop, unstable_pressDelay] + ); + + const handlePressOut = useCallback( + (event: PressableEvent, success: boolean = true) => { + if (!isCurrentlyPressed.current) { + // Some prop configurations may lead to handlePressOut being called mutliple times. + return; + } + + isCurrentlyPressed.current = false; + + if (pressDelayTimeoutRef.current) { + innerHandlePressIn(event); + } + + onPressOut?.(event); + + if (isOnPressAllowed.current && success) { + onPress?.(event); + } + + handleFinalize(); + }, + [handleFinalize, innerHandlePressIn, onPress, onPressOut] + ); + + const stateMachine = useMemo(() => new PressableStateMachine(), []); + + useEffect(() => { + const configuration = getStatesConfig(handlePressIn, handlePressOut); + stateMachine.setStates(configuration); + }, [handlePressIn, handlePressOut, stateMachine]); + + const hoverInTimeout = useRef(null); + const hoverOutTimeout = useRef(null); + + const hoverGesture = useHoverGesture({ + // manualActivation: true, // Prevents Hover blocking Gesture.Native() on web + cancelsTouchesInView: false, + onBegin: (event) => { + if (hoverOutTimeout.current) { + clearTimeout(hoverOutTimeout.current); + } + if (delayHoverIn) { + hoverInTimeout.current = setTimeout( + () => onHoverIn?.(gestureToPressableEvent(event)), + delayHoverIn + ); + return; + } + onHoverIn?.(gestureToPressableEvent(event)); + }, + onFinalize: (event) => { + if (hoverInTimeout.current) { + clearTimeout(hoverInTimeout.current); + } + if (delayHoverOut) { + hoverOutTimeout.current = setTimeout( + () => onHoverOut?.(gestureToPressableEvent(event)), + delayHoverOut + ); + return; + } + onHoverOut?.(gestureToPressableEvent(event)); + }, + disableReanimated: true, + simultaneousWith: simultaneousWith, + block: block, + requireToFail: requireToFail, + }); + + const pressAndTouchGesture = useLongPressGesture({ + minDuration: Platform.OS === 'web' ? 0 : INT32_MAX, // Long press handles finalize on web, thus it must activate right away + maxDistance: INT32_MAX, // Stops long press from cancelling on touch move + cancelsTouchesInView: false, + onTouchesDown: (event) => { + const pressableEvent = gestureTouchToPressableEvent(event); + stateMachine.handleEvent( + StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + pressableEvent + ); + }, + onTouchesUp: () => { + if (Platform.OS === 'android') { + // Prevents potential soft-locks + stateMachine.reset(); + handleFinalize(); + } + }, + onTouchesCancel: (event) => { + const pressableEvent = gestureTouchToPressableEvent(event); + stateMachine.reset(); + handlePressOut(pressableEvent, false); + }, + onFinalize: (_event, success) => { + if (Platform.OS === 'web') { + if (success) { + stateMachine.handleEvent(StateMachineEvent.FINALIZE); + } else { + stateMachine.handleEvent(StateMachineEvent.CANCEL); + } + handleFinalize(); + } + }, + disableReanimated: true, + simultaneousWith: simultaneousWith, + block: block, + requireToFail: requireToFail, + }); + + // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events + const buttonGesture = useNativeGesture({ + onTouchesCancel: (event) => { + if (Platform.OS !== 'macos' && Platform.OS !== 'web') { + // On MacOS cancel occurs in middle of gesture + // On Web cancel occurs on mouse move, which is unwanted + const pressableEvent = gestureTouchToPressableEvent(event); + stateMachine.reset(); + handlePressOut(pressableEvent, false); + } + }, + onBegin: () => { + stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN); + }, + onActivate: () => { + if (Platform.OS !== 'android') { + // Gesture.Native().onStart() is broken with Android + hitSlop + stateMachine.handleEvent(StateMachineEvent.NATIVE_START); + } + }, + onFinalize: (_event, success) => { + if (Platform.OS !== 'web') { + // On Web we use LongPress().onFinalize() instead of Native().onFinalize(), + // as Native cancels on mouse move, and LongPress does not. + if (success) { + stateMachine.handleEvent(StateMachineEvent.FINALIZE); + } else { + stateMachine.handleEvent(StateMachineEvent.CANCEL); + } + + if (Platform.OS !== 'ios') { + handleFinalize(); + } + } + }, + disableReanimated: true, + simultaneousWith: simultaneousWith, + block: block, + requireToFail: requireToFail, + }); + + const isPressableEnabled = disabled !== true; + + // for (const gesture of gestures) { + // gesture.enabled(isPressableEnabled); + // gesture.runOnJS(true); + // gesture.hitSlop(appliedHitSlop); + // + // Object.entries(relationProps).forEach(([relationName, relation]) => { + // applyRelationProp( + // gesture, + // relationName as RelationPropName, + // relation as RelationPropType + // ); + // }); + // } + + const gesture = useSimultaneousGestures( + buttonGesture, + pressAndTouchGesture, + hoverGesture + ); + + // `cursor: 'pointer'` on `RNButton` crashes iOS + const pointerStyle: StyleProp = + Platform.OS === 'web' ? { cursor: 'pointer' } : {}; + + const styleProp = + typeof style === 'function' ? style({ pressed: pressedState }) : style; + + const childrenProp = + typeof children === 'function' + ? children({ pressed: pressedState }) + : children; + + const rippleColor = useMemo(() => { + const defaultRippleColor = android_ripple ? undefined : 'transparent'; + return android_ripple?.color ?? defaultRippleColor; + }, [android_ripple]); + + const setDimensions = useCallback( + (event: LayoutChangeEvent) => { + onLayout?.(event); + dimensions.current = event.nativeEvent.layout; + }, + [onLayout] + ); + + return ( + + + {childrenProp} + {__DEV__ ? ( + + ) : null} + + + ); +}; + +export default Pressable; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx new file mode 100644 index 0000000000..21e44b06fa --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx @@ -0,0 +1,180 @@ +import { + AccessibilityProps, + ViewProps, + Insets, + StyleProp, + ViewStyle, + PressableStateCallbackType as RNPressableStateCallbackType, + PressableAndroidRippleConfig as RNPressableAndroidRippleConfig, + View, +} from 'react-native'; +import { AnyGesture } from '../../types'; + +export type PressableDimensions = { width: number; height: number }; +export type FullPressableDimensions = { + width: number; + height: number; + x: number; + y: number; +}; + +export type PressableStateCallbackType = RNPressableStateCallbackType; +export type PressableAndroidRippleConfig = RNPressableAndroidRippleConfig; + +export type InnerPressableEvent = { + changedTouches: InnerPressableEvent[]; + identifier: number; + locationX: number; + locationY: number; + pageX: number; + pageY: number; + target: number; + timestamp: number; + touches: InnerPressableEvent[]; + force?: number; +}; + +export type PressableEvent = { nativeEvent: InnerPressableEvent }; + +export interface PressableProps + extends AccessibilityProps, + Omit { + /** + * Called when the hover is activated to provide visual feedback. + */ + onHoverIn?: null | ((event: PressableEvent) => void); + + /** + * Called when the hover is deactivated to undo visual feedback. + */ + onHoverOut?: null | ((event: PressableEvent) => void); + + /** + * Called when a single tap gesture is detected. + */ + onPress?: null | ((event: PressableEvent) => void); + + /** + * Called when a touch is engaged before `onPress`. + */ + onPressIn?: null | ((event: PressableEvent) => void); + + /** + * Called when a touch is released before `onPress`. + */ + onPressOut?: null | ((event: PressableEvent) => void); + + /** + * Called when a long-tap gesture is detected. + */ + onLongPress?: null | ((event: PressableEvent) => void); + + /** + * A reference to the pressable element. + */ + ref?: React.Ref; + + /** + * Either children or a render prop that receives a boolean reflecting whether + * the component is currently pressed. + */ + children?: + | React.ReactNode + | ((state: PressableStateCallbackType) => React.ReactNode); + + /** + * Whether a press gesture can be interrupted by a parent gesture such as a + * scroll event. Defaults to true. + */ + cancelable?: null | boolean; + + /** + * Duration to wait after hover in before calling `onHoverIn`. + * @platform web macos + * + * NOTE: not present in RN docs + */ + delayHoverIn?: number | null; + + /** + * Duration to wait after hover out before calling `onHoverOut`. + * @platform web macos + * + * NOTE: not present in RN docs + */ + delayHoverOut?: number | null; + + /** + * Duration (in milliseconds) from `onPressIn` before `onLongPress` is called. + */ + delayLongPress?: null | number; + + /** + * Whether the press behavior is disabled. + */ + disabled?: null | boolean; + + /** + * Additional distance outside of this view in which a press is detected. + */ + hitSlop?: null | Insets | number; + + /** + * Additional distance outside of this view in which a touch is considered a + * press before `onPressOut` is triggered. + */ + pressRetentionOffset?: null | Insets | number; + + /** + * If true, doesn't play system sound on touch. + * @platform android + */ + android_disableSound?: null | boolean; + + /** + * Enables the Android ripple effect and configures its color. + * @platform android + */ + android_ripple?: null | PressableAndroidRippleConfig; + + /** + * Used only for documentation or testing (e.g. snapshot testing). + */ + testOnly_pressed?: null | boolean; + + /** + * 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); + + /** + * Duration (in milliseconds) to wait after press down before calling onPressIn. + */ + unstable_pressDelay?: number; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + simultaneousWith?: AnyGesture; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + requireToFail?: AnyGesture; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + block?: AnyGesture; + + /** + * @deprecated This property is no longer used, and will be removed in the future. + */ + dimensionsAfterResize?: PressableDimensions; +} diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx new file mode 100644 index 0000000000..b6d38ac828 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx @@ -0,0 +1,54 @@ +import { PressableEvent } from './PressableProps'; + +export interface StateDefinition { + eventName: string; + callback?: (event: PressableEvent) => void; +} + +class PressableStateMachine { + private states: StateDefinition[] | null; + private currentStepIndex: number; + private eventPayload: PressableEvent | null; + + constructor() { + this.states = null; + this.currentStepIndex = 0; + this.eventPayload = null; + } + + public setStates(states: StateDefinition[]) { + this.states = states; + } + + public reset() { + this.currentStepIndex = 0; + this.eventPayload = null; + } + + public handleEvent(eventName: string, eventPayload?: PressableEvent) { + if (!this.states) { + return; + } + const step = this.states[this.currentStepIndex]; + this.eventPayload = eventPayload || this.eventPayload; + + if (step.eventName !== eventName) { + if (this.currentStepIndex > 0) { + // retry with position at index 0 + this.reset(); + this.handleEvent(eventName, eventPayload); + } + return; + } + if (this.eventPayload && step.callback) { + step.callback(this.eventPayload); + } + this.currentStepIndex++; + + if (this.currentStepIndex === this.states.length) { + this.reset(); + } + } +} + +export { PressableStateMachine }; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts new file mode 100644 index 0000000000..79ce14a8f0 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts @@ -0,0 +1,5 @@ +export type { + PressableProps, + PressableStateCallbackType, +} from './PressableProps'; +export { default } from './Pressable'; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts new file mode 100644 index 0000000000..55279211cd --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts @@ -0,0 +1,126 @@ +import { Platform } from 'react-native'; +import { PressableEvent } from './PressableProps'; +import { StateDefinition } from './StateMachine'; + +export enum StateMachineEvent { + NATIVE_BEGIN = 'nativeBegin', + NATIVE_START = 'nativeStart', + FINALIZE = 'finalize', + LONG_PRESS_TOUCHES_DOWN = 'longPressTouchesDown', + CANCEL = 'cancel', +} + +function getAndroidStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.NATIVE_BEGIN, + }, + { + eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + callback: handlePressIn, + }, + { + eventName: StateMachineEvent.FINALIZE, + callback: handlePressOut, + }, + ]; +} + +function getIosStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + }, + { + eventName: StateMachineEvent.NATIVE_START, + callback: handlePressIn, + }, + { + eventName: StateMachineEvent.FINALIZE, + callback: handlePressOut, + }, + ]; +} + +function getWebStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.NATIVE_BEGIN, + }, + { + eventName: StateMachineEvent.NATIVE_START, + }, + { + eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + callback: handlePressIn, + }, + { + eventName: StateMachineEvent.FINALIZE, + callback: handlePressOut, + }, + ]; +} + +function getMacosStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, + }, + { + eventName: StateMachineEvent.NATIVE_BEGIN, + callback: handlePressIn, + }, + { + eventName: StateMachineEvent.NATIVE_START, + }, + { + eventName: StateMachineEvent.FINALIZE, + callback: handlePressOut, + }, + ]; +} + +function getUniversalStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +) { + return [ + { + eventName: StateMachineEvent.FINALIZE, + callback: (event: PressableEvent) => { + handlePressIn(event); + handlePressOut(event); + }, + }, + ]; +} + +export function getStatesConfig( + handlePressIn: (event: PressableEvent) => void, + handlePressOut: (event: PressableEvent) => void +): StateDefinition[] { + if (Platform.OS === 'android') { + return getAndroidStatesConfig(handlePressIn, handlePressOut); + } else if (Platform.OS === 'ios') { + return getIosStatesConfig(handlePressIn, handlePressOut); + } else if (Platform.OS === 'web') { + return getWebStatesConfig(handlePressIn, handlePressOut); + } else if (Platform.OS === 'macos') { + return getMacosStatesConfig(handlePressIn, handlePressOut); + } else { + // Unknown platform - using minimal universal setup. + return getUniversalStatesConfig(handlePressIn, handlePressOut); + } +} diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts new file mode 100644 index 0000000000..4e0368b274 --- /dev/null +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts @@ -0,0 +1,135 @@ +import { Insets } from 'react-native'; +import { + FullPressableDimensions, + InnerPressableEvent, + PressableEvent, +} from './PressableProps'; +import { + GestureTouchEvent, + TouchData, +} from '../../../handlers/gestureHandlerCommon'; +import { HoverGestureEvent } from '../../hooks/gestures/hover/useHoverGesture'; +import { LongPressGestureEvent } from '../../hooks'; + +const numberAsInset = (value: number): Insets => ({ + left: value, + right: value, + top: value, + bottom: value, +}); + +const addInsets = (a: Insets, b: Insets): Insets => ({ + left: (a.left ?? 0) + (b.left ?? 0), + right: (a.right ?? 0) + (b.right ?? 0), + top: (a.top ?? 0) + (b.top ?? 0), + bottom: (a.bottom ?? 0) + (b.bottom ?? 0), +}); + +const touchDataToPressEvent = ( + data: TouchData, + timestamp: number, + targetId: number +): InnerPressableEvent => ({ + identifier: data.id, + locationX: data.x, + locationY: data.y, + pageX: data.absoluteX, + pageY: data.absoluteY, + target: targetId, + timestamp: timestamp, + touches: [], // Always empty - legacy compatibility + changedTouches: [], // Always empty - legacy compatibility +}); + +const gestureToPressEvent = ( + event: HoverGestureEvent | LongPressGestureEvent, + timestamp: number, + targetId: number +): InnerPressableEvent => ({ + identifier: event.handlerTag, + locationX: event.x, + locationY: event.y, + pageX: event.absoluteX, + pageY: event.absoluteY, + target: targetId, + timestamp: timestamp, + touches: [], // Always empty - legacy compatibility + changedTouches: [], // Always empty - legacy compatibility +}); + +const isTouchWithinInset = ( + dimensions: FullPressableDimensions, + inset: Insets, + touch?: InnerPressableEvent +) => + (touch?.locationX ?? 0) < + (inset.right ?? 0) + dimensions.width + dimensions.x && + (touch?.locationY ?? 0) < + (inset.bottom ?? 0) + dimensions.height + dimensions.y && + (touch?.locationX ?? 0) > -(inset.left ?? 0) + dimensions.x && + (touch?.locationY ?? 0) > -(inset.top ?? 0) + dimensions.y; + +const gestureToPressableEvent = ( + event: HoverGestureEvent | LongPressGestureEvent +): PressableEvent => { + const timestamp = Date.now(); + + // As far as I can see, there isn't a conventional way of getting targetId with the data we get + const targetId = 0; + + const pressEvent = gestureToPressEvent(event, timestamp, targetId); + + return { + nativeEvent: { + touches: [pressEvent], + changedTouches: [pressEvent], + identifier: pressEvent.identifier, + locationX: event.x, + locationY: event.y, + pageX: event.absoluteX, + pageY: event.absoluteY, + target: targetId, + timestamp: timestamp, + force: undefined, + }, + }; +}; + +const gestureTouchToPressableEvent = ( + event: GestureTouchEvent +): PressableEvent => { + const timestamp = Date.now(); + + // As far as I can see, there isn't a conventional way of getting targetId with the data we get + const targetId = 0; + + const touchesList = event.allTouches.map((touch: TouchData) => + touchDataToPressEvent(touch, timestamp, targetId) + ); + const changedTouchesList = event.changedTouches.map((touch: TouchData) => + touchDataToPressEvent(touch, timestamp, targetId) + ); + + return { + nativeEvent: { + touches: touchesList, + changedTouches: changedTouchesList, + identifier: event.handlerTag, + locationX: event.allTouches.at(0)?.x ?? -1, + locationY: event.allTouches.at(0)?.y ?? -1, + pageX: event.allTouches.at(0)?.absoluteX ?? -1, + pageY: event.allTouches.at(0)?.absoluteY ?? -1, + target: targetId, + timestamp: timestamp, + force: undefined, + }, + }; +}; + +export { + numberAsInset, + addInsets, + isTouchWithinInset, + gestureToPressableEvent, + gestureTouchToPressableEvent, +}; From 342c5f8018b810dbc2fbead1718c7aee58e65bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Wed, 7 Jan 2026 18:05:13 +0100 Subject: [PATCH 02/22] update whitelist --- .../src/v3/hooks/utils/propsWhiteList.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts index 943adfc1ce..d577b4d413 100644 --- a/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts +++ b/packages/react-native-gesture-handler/src/v3/hooks/utils/propsWhiteList.ts @@ -22,6 +22,7 @@ const CommonConfig = new Set([ 'activeCursor', 'mouseButton', 'testID', + 'cancelsTouchesInView', ]); const ExternalRelationsConfig = new Set([ From 917513ebd04c9bfb4639fd55e057f2eef7ae21a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Thu, 8 Jan 2026 15:13:32 +0100 Subject: [PATCH 03/22] fix ios hitFrame --- .../apple/RNGestureHandler.h | 1 + .../apple/RNGestureHandler.mm | 12 ++++- .../src/v3/components/Pressable/Pressable.tsx | 53 +++++++------------ 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 52a2ea5a66..966630332a 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -94,6 +94,7 @@ - (void)updateRelations:(nonnull NSDictionary *)relations; - (void)handleGesture:(nonnull id)recognizer fromReset:(BOOL)fromReset; - (void)handleGesture:(nonnull id)recognizer inState:(RNGestureHandlerState)state; +- (CGRect)getViewBounds; - (BOOL)containsPointInView; - (RNGestureHandlerState)state; - (nullable RNGestureHandlerEventExtraData *)eventExtraData:(nonnull id)recognizer; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index f1ce6f1c9d..a738e09c79 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -666,10 +666,20 @@ - (void)reset } } +- (CGRect)getViewBounds +{ + CGRect bounds = _recognizer.view.bounds; + if (bounds.size.width == 0 and bounds.size.height == 0 and bounds.origin.x == 0 and bounds.origin.y == 0 and + _recognizer.view.subviews.count) { + bounds = _recognizer.view.subviews[0].bounds; + } + return bounds; +} + - (BOOL)containsPointInView { CGPoint pt = [_recognizer locationInView:_recognizer.view]; - CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); + CGRect hitFrame = RNGHHitSlopInsetRect([self getViewBounds], _hitSlop); return CGRectContainsPoint(hitFrame, pt); } diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 213cae2bfd..c387cfd96c 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -15,6 +15,7 @@ import { LayoutChangeEvent, Platform, StyleProp, + View, ViewStyle, } from 'react-native'; import { @@ -322,22 +323,6 @@ const Pressable = (props: PressableProps) => { requireToFail: requireToFail, }); - const isPressableEnabled = disabled !== true; - - // for (const gesture of gestures) { - // gesture.enabled(isPressableEnabled); - // gesture.runOnJS(true); - // gesture.hitSlop(appliedHitSlop); - // - // Object.entries(relationProps).forEach(([relationName, relation]) => { - // applyRelationProp( - // gesture, - // relationName as RelationPropName, - // relation as RelationPropType - // ); - // }); - // } - const gesture = useSimultaneousGestures( buttonGesture, pressAndTouchGesture, @@ -370,23 +355,25 @@ const Pressable = (props: PressableProps) => { ); return ( - - - {childrenProp} - {__DEV__ ? ( - - ) : null} - - + + + + {childrenProp} + {__DEV__ ? ( + + ) : null} + + + ); }; From b501a7a4e0b2206b65647b07143cb24cea0338c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Thu, 8 Jan 2026 15:42:15 +0100 Subject: [PATCH 04/22] fix ios hitFrame --- .../apple/RNGestureHandler.mm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index f1ce6f1c9d..f151dde2ec 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -668,8 +668,15 @@ - (void)reset - (BOOL)containsPointInView { + CGRect bounds = _recognizer.view.bounds; CGPoint pt = [_recognizer locationInView:_recognizer.view]; - CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); + + if (bounds.size.width == 0 and bounds.size.height == 0 and bounds.origin.x == 0 and bounds.origin.y == 0 and + _recognizer.view.subviews.count) { + bounds = _recognizer.view.subviews[0].bounds; + pt = [_recognizer locationInView:_recognizer.view.subviews[0]]; + } + CGRect hitFrame = RNGHHitSlopInsetRect(bounds, _hitSlop); return CGRectContainsPoint(hitFrame, pt); } From 0b0b482f70023a23f411030a2460fb3ca04ff084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 13 Jan 2026 15:09:38 +0100 Subject: [PATCH 05/22] fix web!!!! --- .../src/v3/components/Pressable/utils.ts | 21 ++++++++++++------- .../v3/detectors/HostGestureDetector.web.tsx | 20 ++++++++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts index 4e0368b274..05b1c597b6 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts @@ -1,8 +1,8 @@ -import { Insets } from 'react-native'; +import { Insets, Platform } from 'react-native'; import { - FullPressableDimensions, InnerPressableEvent, PressableEvent, + FullPressableDimensions, } from './PressableProps'; import { GestureTouchEvent, @@ -62,12 +62,17 @@ const isTouchWithinInset = ( inset: Insets, touch?: InnerPressableEvent ) => - (touch?.locationX ?? 0) < - (inset.right ?? 0) + dimensions.width + dimensions.x && - (touch?.locationY ?? 0) < - (inset.bottom ?? 0) + dimensions.height + dimensions.y && - (touch?.locationX ?? 0) > -(inset.left ?? 0) + dimensions.x && - (touch?.locationY ?? 0) > -(inset.top ?? 0) + dimensions.y; + Platform.OS === 'ios' + ? (touch?.locationX ?? 0) < + (inset.right ?? 0) + dimensions.width + dimensions.x && + (touch?.locationY ?? 0) < + (inset.bottom ?? 0) + dimensions.height + dimensions.y && + (touch?.locationX ?? 0) > -(inset.left ?? 0) + dimensions.x && + (touch?.locationY ?? 0) > -(inset.top ?? 0) + dimensions.y + : (touch?.locationX ?? 0) < (inset.right ?? 0) + dimensions.width && + (touch?.locationY ?? 0) < (inset.bottom ?? 0) + dimensions.height && + (touch?.locationX ?? 0) > -(inset.left ?? 0) && + (touch?.locationY ?? 0) > -(inset.top ?? 0); const gestureToPressableEvent = ( event: HoverGestureEvent | LongPressGestureEvent diff --git a/packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx b/packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx index d118d1abb9..f822fee97a 100644 --- a/packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx +++ b/packages/react-native-gesture-handler/src/v3/detectors/HostGestureDetector.web.tsx @@ -1,4 +1,4 @@ -import React, { Ref, RefObject, useEffect, useRef } from 'react'; +import React, { Ref, RefObject, useEffect, useMemo, useRef } from 'react'; import RNGestureHandlerModule from '../../RNGestureHandlerModule.web'; import { ActionType } from '../../ActionType'; import { PropsRef } from '../../web/interfaces'; @@ -27,6 +27,8 @@ const EMPTY_HANDLERS = new Set(); const HostGestureDetector = (props: GestureHandlerDetectorProps) => { const { handlerTags, children } = props; + const handlerTagsSet = useMemo(() => new Set(handlerTags), [...handlerTags]); + const viewRef = useRef(null); const propsRef = useRef(props); const attachedHandlers = useRef>(new Set()); @@ -110,25 +112,31 @@ const HostGestureDetector = (props: GestureHandlerDetectorProps) => { tagMessage('Detector expected to have exactly one child element') ); } + }, [children]); - const currentHandlerTags = new Set(handlerTags); - detachHandlers(currentHandlerTags, attachedHandlers.current); + useEffect(() => { + if (React.Children.count(children) !== 1) { + throw new Error( + tagMessage('Detector expected to have exactly one child element') + ); + } + + detachHandlers(handlerTagsSet, attachedHandlers.current); attachHandlers( viewRef, propsRef, - currentHandlerTags, + handlerTagsSet, attachedHandlers.current, ActionType.NATIVE_DETECTOR ); - return () => { detachHandlers(EMPTY_HANDLERS, attachedHandlers.current); attachedVirtualHandlers?.current.forEach((childHandlerTags) => { detachHandlers(EMPTY_HANDLERS, childHandlerTags); }); }; - }, [handlerTags, children]); + }, [handlerTagsSet, viewRef]); useEffect(() => { const virtualChildrenToDetach: Set = new Set( From ed3b7a41ea395b0fb474c4de4c8bda17a2481723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Thu, 15 Jan 2026 22:02:19 +0100 Subject: [PATCH 06/22] Revert "merge" This reverts commit 6bebb33dab3f605b9530ce7895fea901601049e4, reversing changes made to 917513ebd04c9bfb4639fd55e057f2eef7ae21a3. --- .../apple/RNGestureHandler.h | 1 + .../apple/RNGestureHandler.mm | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 52a2ea5a66..966630332a 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -94,6 +94,7 @@ - (void)updateRelations:(nonnull NSDictionary *)relations; - (void)handleGesture:(nonnull id)recognizer fromReset:(BOOL)fromReset; - (void)handleGesture:(nonnull id)recognizer inState:(RNGestureHandlerState)state; +- (CGRect)getViewBounds; - (BOOL)containsPointInView; - (RNGestureHandlerState)state; - (nullable RNGestureHandlerEventExtraData *)eventExtraData:(nonnull id)recognizer; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index f151dde2ec..a738e09c79 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -666,17 +666,20 @@ - (void)reset } } -- (BOOL)containsPointInView +- (CGRect)getViewBounds { CGRect bounds = _recognizer.view.bounds; - CGPoint pt = [_recognizer locationInView:_recognizer.view]; - if (bounds.size.width == 0 and bounds.size.height == 0 and bounds.origin.x == 0 and bounds.origin.y == 0 and _recognizer.view.subviews.count) { bounds = _recognizer.view.subviews[0].bounds; - pt = [_recognizer locationInView:_recognizer.view.subviews[0]]; } - CGRect hitFrame = RNGHHitSlopInsetRect(bounds, _hitSlop); + return bounds; +} + +- (BOOL)containsPointInView +{ + CGPoint pt = [_recognizer locationInView:_recognizer.view]; + CGRect hitFrame = RNGHHitSlopInsetRect([self getViewBounds], _hitSlop); return CGRectContainsPoint(hitFrame, pt); } From b11757f570a8109805081b63bcdec9d6e4df389a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Thu, 15 Jan 2026 22:17:41 +0100 Subject: [PATCH 07/22] temporary commit --- .../RNGestureHandlerDetectorShadowNode.cpp | 5 +---- .../src/v3/components/Pressable/utils.ts | 17 +++++------------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp index 7abe62f4b3..7fef34cefd 100644 --- a/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp +++ b/packages/react-native-gesture-handler/shared/shadowNodes/react/renderer/components/rngesturehandler_codegen/RNGestureHandlerDetectorShadowNode.cpp @@ -38,10 +38,6 @@ void RNGestureHandlerDetectorShadowNode::layout(LayoutContext layoutContext) { // TODO: consider allowing more than one child and doing bounding box react_native_assert(getChildren().size() == 1); - if (!this->yogaNode_.getHasNewLayout()) { - return; - } - auto child = std::static_pointer_cast( getChildren()[0]); @@ -51,6 +47,7 @@ void RNGestureHandlerDetectorShadowNode::layout(LayoutContext layoutContext) { // TODO: figure out the correct way to setup metrics between detector and // the child auto metrics = child->getLayoutMetrics(); + metrics.frame = child->getLayoutMetrics().frame; setLayoutMetrics(metrics); auto childmetrics = child->getLayoutMetrics(); diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts index 05b1c597b6..cd99adaaf7 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts @@ -1,4 +1,4 @@ -import { Insets, Platform } from 'react-native'; +import { Insets } from 'react-native'; import { InnerPressableEvent, PressableEvent, @@ -62,17 +62,10 @@ const isTouchWithinInset = ( inset: Insets, touch?: InnerPressableEvent ) => - Platform.OS === 'ios' - ? (touch?.locationX ?? 0) < - (inset.right ?? 0) + dimensions.width + dimensions.x && - (touch?.locationY ?? 0) < - (inset.bottom ?? 0) + dimensions.height + dimensions.y && - (touch?.locationX ?? 0) > -(inset.left ?? 0) + dimensions.x && - (touch?.locationY ?? 0) > -(inset.top ?? 0) + dimensions.y - : (touch?.locationX ?? 0) < (inset.right ?? 0) + dimensions.width && - (touch?.locationY ?? 0) < (inset.bottom ?? 0) + dimensions.height && - (touch?.locationX ?? 0) > -(inset.left ?? 0) && - (touch?.locationY ?? 0) > -(inset.top ?? 0); + (touch?.locationX ?? 0) < (inset.right ?? 0) + dimensions.width && + (touch?.locationY ?? 0) < (inset.bottom ?? 0) + dimensions.height && + (touch?.locationX ?? 0) > -(inset.left ?? 0) && + (touch?.locationY ?? 0) > -(inset.top ?? 0); const gestureToPressableEvent = ( event: HoverGestureEvent | LongPressGestureEvent From dae4dd9bc05e015ecf667b98ff53ff3023df8f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Thu, 15 Jan 2026 22:27:59 +0100 Subject: [PATCH 08/22] legacy --- packages/react-native-gesture-handler/src/index.ts | 6 +++--- .../react-native-gesture-handler/src/v3/components/index.ts | 3 +++ packages/react-native-gesture-handler/src/v3/index.ts | 6 ++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index 962700f5a0..ad67b66fb4 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -147,10 +147,10 @@ export type { } from './handlers/gestureHandlerTypesCompat'; export type { - PressableProps, - PressableStateCallbackType, + PressableProps as LegacyPressableProps, + PressableStateCallbackType as LegacyPressableCallbackType, } from './v3/components/Pressable'; -export { default as Pressable } from './v3/components/Pressable'; +export { default as LegacyPressable } from './v3/components/Pressable'; export type { GestureTouchEvent as SingleGestureTouchEvent } from './handlers/gestureHandlerCommon'; diff --git a/packages/react-native-gesture-handler/src/v3/components/index.ts b/packages/react-native-gesture-handler/src/v3/components/index.ts index 6412249068..a551f25f66 100644 --- a/packages/react-native-gesture-handler/src/v3/components/index.ts +++ b/packages/react-native-gesture-handler/src/v3/components/index.ts @@ -20,3 +20,6 @@ export { FlatList, RefreshControl, } from './GestureComponents'; + +export type { PressableProps, PressableStateCallbackType } from './Pressable'; +export { default as Pressable } from './Pressable'; diff --git a/packages/react-native-gesture-handler/src/v3/index.ts b/packages/react-native-gesture-handler/src/v3/index.ts index 4902aae885..ad50434663 100644 --- a/packages/react-native-gesture-handler/src/v3/index.ts +++ b/packages/react-native-gesture-handler/src/v3/index.ts @@ -71,6 +71,12 @@ export { RefreshControl, } from './components'; +export { + PressableProps, + PressableStateCallbackType, + Pressable, +} from './components'; + export type { ComposedGesture } from './types'; export { GestureStateManager } from './gestureStateManager'; From ae6e532b2b1b001830f4ed63e0b564ef4f08a7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Fri, 16 Jan 2026 13:00:00 +0100 Subject: [PATCH 09/22] fix export --- packages/react-native-gesture-handler/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index ad67b66fb4..ebdf90182a 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -149,8 +149,8 @@ export type { export type { PressableProps as LegacyPressableProps, PressableStateCallbackType as LegacyPressableCallbackType, -} from './v3/components/Pressable'; -export { default as LegacyPressable } from './v3/components/Pressable'; +} from './components/Pressable'; +export { default as LegacyPressable } from './components/Pressable'; export type { GestureTouchEvent as SingleGestureTouchEvent } from './handlers/gestureHandlerCommon'; From f7498bbc91e4d02800ecf38d614ecae3c5414e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Fri, 16 Jan 2026 13:19:35 +0100 Subject: [PATCH 10/22] remove no longer necessary --- .../apple/RNGestureHandler.h | 1 - .../apple/RNGestureHandler.mm | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.h b/packages/react-native-gesture-handler/apple/RNGestureHandler.h index 64ac95ab62..597d061a31 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.h +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.h @@ -95,7 +95,6 @@ - (void)updateRelations:(nonnull NSDictionary *)relations; - (void)handleGesture:(nonnull id)recognizer fromReset:(BOOL)fromReset; - (void)handleGesture:(nonnull id)recognizer inState:(RNGestureHandlerState)state; -- (CGRect)getViewBounds; - (BOOL)containsPointInView; - (RNGestureHandlerState)state; - (nullable RNGestureHandlerEventExtraData *)eventExtraData:(nonnull id)recognizer; diff --git a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm index 231cefd745..f24b923edc 100644 --- a/packages/react-native-gesture-handler/apple/RNGestureHandler.mm +++ b/packages/react-native-gesture-handler/apple/RNGestureHandler.mm @@ -677,20 +677,10 @@ - (void)reset } } -- (CGRect)getViewBounds -{ - CGRect bounds = _recognizer.view.bounds; - if (bounds.size.width == 0 and bounds.size.height == 0 and bounds.origin.x == 0 and bounds.origin.y == 0 and - _recognizer.view.subviews.count) { - bounds = _recognizer.view.subviews[0].bounds; - } - return bounds; -} - - (BOOL)containsPointInView { CGPoint pt = [_recognizer locationInView:_recognizer.view]; - CGRect hitFrame = RNGHHitSlopInsetRect([self getViewBounds], _hitSlop); + CGRect hitFrame = RNGHHitSlopInsetRect(_recognizer.view.bounds, _hitSlop); return CGRectContainsPoint(hitFrame, pt); } From a8788a110bc3b0b977b0ff6b070a65370498fb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:08:35 +0100 Subject: [PATCH 11/22] Update packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com> --- .../src/v3/components/Pressable/Pressable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index c387cfd96c..f9bf000c87 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -238,9 +238,9 @@ const Pressable = (props: PressableProps) => { onHoverOut?.(gestureToPressableEvent(event)); }, disableReanimated: true, - simultaneousWith: simultaneousWith, - block: block, - requireToFail: requireToFail, + simultaneousWith, + block, + requireToFail, }); const pressAndTouchGesture = useLongPressGesture({ From d4b3e0a442442e3d71fba2b51b99802f01ea83f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:15:56 +0100 Subject: [PATCH 12/22] simplify if 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com> --- .../src/v3/components/Pressable/Pressable.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index f9bf000c87..048729bb5b 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -267,14 +267,15 @@ const Pressable = (props: PressableProps) => { handlePressOut(pressableEvent, false); }, onFinalize: (_event, success) => { - if (Platform.OS === 'web') { - if (success) { - stateMachine.handleEvent(StateMachineEvent.FINALIZE); - } else { - stateMachine.handleEvent(StateMachineEvent.CANCEL); - } - handleFinalize(); + if (Platform.OS !== 'web') { + return; } + + stateMachine.handleEvent( + success ? StateMachineEvent.FINALIZE : StateMachineEvent.CANCEL + ); + + handleFinalize(); }, disableReanimated: true, simultaneousWith: simultaneousWith, From 5d268281a79f191c73c72dbf23c6d6445f1f6028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:16:22 +0100 Subject: [PATCH 13/22] Update packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com> --- .../src/v3/components/Pressable/Pressable.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 048729bb5b..5d324b4c70 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -304,18 +304,17 @@ const Pressable = (props: PressableProps) => { } }, onFinalize: (_event, success) => { - if (Platform.OS !== 'web') { - // On Web we use LongPress().onFinalize() instead of Native().onFinalize(), - // as Native cancels on mouse move, and LongPress does not. - if (success) { - stateMachine.handleEvent(StateMachineEvent.FINALIZE); - } else { - stateMachine.handleEvent(StateMachineEvent.CANCEL); - } - - if (Platform.OS !== 'ios') { - handleFinalize(); - } + // On Web we use LongPress.onFinalize instead of Native.onFinalize, + // as Native cancels on mouse move, and LongPress does not. + if (Platform.OS === 'web') { + return; + } + stateMachine.handleEvent( + success ? StateMachineEvent.FINALIZE : StateMachineEvent.CANCEL + ); + + if (Platform.OS !== 'ios') { + handleFinalize(); } }, disableReanimated: true, From 6c6875dee2769ac370bbc5c239c5083e1dbd0bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 11:13:57 +0100 Subject: [PATCH 14/22] manual activation and comment --- .../src/v3/components/Pressable/Pressable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 5d324b4c70..90de3f35e3 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -209,7 +209,7 @@ const Pressable = (props: PressableProps) => { const hoverOutTimeout = useRef(null); const hoverGesture = useHoverGesture({ - // manualActivation: true, // Prevents Hover blocking Gesture.Native() on web + manualActivation: true, // Prevents Hover blocking native gesture on web cancelsTouchesInView: false, onBegin: (event) => { if (hoverOutTimeout.current) { From ac68a74537de472319a0749521f314e4efc91282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= <81448793+akwasniewski@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:17:22 +0100 Subject: [PATCH 15/22] Update packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com> --- .../src/v3/components/Pressable/Pressable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 90de3f35e3..8c40f1002b 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -318,9 +318,9 @@ const Pressable = (props: PressableProps) => { } }, disableReanimated: true, - simultaneousWith: simultaneousWith, - block: block, - requireToFail: requireToFail, + simultaneousWith, + block, + requireToFail, }); const gesture = useSimultaneousGestures( From c85289cef811329a5ee78ef253f40b6a5aabad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 11:20:57 +0100 Subject: [PATCH 16/22] update comment --- .../src/v3/components/Pressable/Pressable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 8c40f1002b..995a1d2a15 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -209,7 +209,7 @@ const Pressable = (props: PressableProps) => { const hoverOutTimeout = useRef(null); const hoverGesture = useHoverGesture({ - manualActivation: true, // Prevents Hover blocking native gesture on web + manualActivation: true, // Prevents Hover blocking Native gesture on web cancelsTouchesInView: false, onBegin: (event) => { if (hoverOutTimeout.current) { @@ -299,7 +299,7 @@ const Pressable = (props: PressableProps) => { }, onActivate: () => { if (Platform.OS !== 'android') { - // Gesture.Native().onStart() is broken with Android + hitSlop + // Native.onActivate is broken with Android + hitSlop stateMachine.handleEvent(StateMachineEvent.NATIVE_START); } }, From e5415a24986a44215d9158853ad446badce91b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 11:50:43 +0100 Subject: [PATCH 17/22] remove extra view --- .../src/v3/components/Pressable/Pressable.tsx | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index 995a1d2a15..cbdc6d7e9f 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -15,7 +15,6 @@ import { LayoutChangeEvent, Platform, StyleProp, - View, ViewStyle, } from 'react-native'; import { @@ -355,25 +354,23 @@ const Pressable = (props: PressableProps) => { ); return ( - - - - {childrenProp} - {__DEV__ ? ( - - ) : null} - - - + + + {childrenProp} + {__DEV__ ? ( + + ) : null} + + ); }; From 07ee909bd9abe2e8d6b059ec078790952af51e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 11:54:15 +0100 Subject: [PATCH 18/22] readd enabled and hitslop --- .../src/v3/components/Pressable/Pressable.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx index cbdc6d7e9f..4df81e1fa6 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx @@ -236,10 +236,12 @@ const Pressable = (props: PressableProps) => { } onHoverOut?.(gestureToPressableEvent(event)); }, + enabled: disabled !== true, disableReanimated: true, simultaneousWith, block, requireToFail, + hitSlop: appliedHitSlop, }); const pressAndTouchGesture = useLongPressGesture({ @@ -276,10 +278,12 @@ const Pressable = (props: PressableProps) => { handleFinalize(); }, + enabled: disabled !== true, disableReanimated: true, simultaneousWith: simultaneousWith, block: block, requireToFail: requireToFail, + hitSlop: appliedHitSlop, }); // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events @@ -316,10 +320,12 @@ const Pressable = (props: PressableProps) => { handleFinalize(); } }, + enabled: disabled !== true, disableReanimated: true, simultaneousWith, block, requireToFail, + hitSlop: appliedHitSlop, }); const gesture = useSimultaneousGestures( From d94ab44f6150751d7592faca7327a89bf98ebcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 12:13:41 +0100 Subject: [PATCH 19/22] reduce redundancy --- .../src/components/Pressable/Pressable.tsx | 4 +- .../components/Pressable/PressableProps.tsx | 61 ++++-- .../src/components/Pressable/index.ts | 1 + .../src/components/Pressable/utils.ts | 19 +- .../react-native-gesture-handler/src/index.ts | 5 +- .../components/{Pressable => }/Pressable.tsx | 28 +-- .../components/Pressable/PressableProps.tsx | 180 ------------------ .../v3/components/Pressable/StateMachine.tsx | 54 ------ .../src/v3/components/Pressable/index.ts | 5 - .../components/Pressable/stateDefinitions.ts | 126 ------------ .../src/v3/components/Pressable/utils.ts | 133 ------------- .../src/v3/components/index.ts | 1 - .../src/v3/index.ts | 7 +- 13 files changed, 76 insertions(+), 548 deletions(-) rename packages/react-native-gesture-handler/src/v3/components/{Pressable => }/Pressable.tsx (94%) delete mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx delete mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx delete mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts delete mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts delete mode 100644 packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts diff --git a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx index c5f8f6d60e..8d8bf3eee7 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/components/Pressable/Pressable.tsx @@ -9,8 +9,8 @@ import { GestureObjects as Gesture } from '../../handlers/gestures/gestureObject import { GestureDetector } from '../../handlers/gestures/GestureDetector'; import { PressableEvent, - PressableProps, PressableDimensions, + LegacyPressableProps, } from './PressableProps'; import { Insets, @@ -40,7 +40,7 @@ import { PressableStateMachine } from './StateMachine'; const DEFAULT_LONG_PRESS_DURATION = 500; const IS_TEST_ENV = isTestEnv(); -const Pressable = (props: PressableProps) => { +const Pressable = (props: LegacyPressableProps) => { const { testOnly_pressed, hitSlop, diff --git a/packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx b/packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx index 0a18f75d4c..716d6bd5e3 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx +++ b/packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx @@ -9,6 +9,7 @@ import { View, } from 'react-native'; import { RelationPropType } from '../utils'; +import { AnyGesture } from '../../v3/types'; export type PressableDimensions = { width: number; height: number }; @@ -30,7 +31,47 @@ export type InnerPressableEvent = { export type PressableEvent = { nativeEvent: InnerPressableEvent }; -export interface PressableProps +export interface LegacyPressableProps extends CommonPressableProps { + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + simultaneousWithExternalGesture?: RelationPropType; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + requireExternalGestureToFail?: RelationPropType; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + blocksExternalGesture?: RelationPropType; +} + +export interface PressableProps extends CommonPressableProps { + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + simultaneousWith?: AnyGesture; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + requireToFail?: AnyGesture; + + /** + * A gesture object or an array of gesture objects containing the configuration and callbacks to be + * used with the Pressable's gesture handlers. + */ + block?: AnyGesture; +} + +interface CommonPressableProps extends AccessibilityProps, Omit { /** @@ -149,24 +190,6 @@ export interface PressableProps */ unstable_pressDelay?: number; - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - simultaneousWithExternalGesture?: RelationPropType; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - requireExternalGestureToFail?: RelationPropType; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - blocksExternalGesture?: RelationPropType; - /** * @deprecated This property is no longer used, and will be removed in the future. */ diff --git a/packages/react-native-gesture-handler/src/components/Pressable/index.ts b/packages/react-native-gesture-handler/src/components/Pressable/index.ts index 79ce14a8f0..806a3719da 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/index.ts +++ b/packages/react-native-gesture-handler/src/components/Pressable/index.ts @@ -1,4 +1,5 @@ export type { + LegacyPressableProps, PressableProps, PressableStateCallbackType, } from './PressableProps'; diff --git a/packages/react-native-gesture-handler/src/components/Pressable/utils.ts b/packages/react-native-gesture-handler/src/components/Pressable/utils.ts index 89235e46d9..058a4c25ce 100644 --- a/packages/react-native-gesture-handler/src/components/Pressable/utils.ts +++ b/packages/react-native-gesture-handler/src/components/Pressable/utils.ts @@ -13,6 +13,7 @@ import { InnerPressableEvent, PressableEvent, } from './PressableProps'; +import { HoverGestureEvent, LongPressGestureEvent } from '../../v3'; const numberAsInset = (value: number): Insets => ({ left: value, @@ -45,9 +46,12 @@ const touchDataToPressEvent = ( }); const gestureToPressEvent = ( - event: GestureStateChangeEvent< - HoverGestureHandlerEventPayload | LongPressGestureHandlerEventPayload - >, + event: + | GestureStateChangeEvent< + HoverGestureHandlerEventPayload | LongPressGestureHandlerEventPayload + > + | HoverGestureEvent + | LongPressGestureEvent, timestamp: number, targetId: number ): InnerPressableEvent => ({ @@ -73,9 +77,12 @@ const isTouchWithinInset = ( (touch?.locationY ?? 0) > -(inset.top ?? 0); const gestureToPressableEvent = ( - event: GestureStateChangeEvent< - HoverGestureHandlerEventPayload | LongPressGestureHandlerEventPayload - > + event: + | GestureStateChangeEvent< + HoverGestureHandlerEventPayload | LongPressGestureHandlerEventPayload + > + | HoverGestureEvent + | LongPressGestureEvent ): PressableEvent => { const timestamp = Date.now(); diff --git a/packages/react-native-gesture-handler/src/index.ts b/packages/react-native-gesture-handler/src/index.ts index ebdf90182a..e6b13ee297 100644 --- a/packages/react-native-gesture-handler/src/index.ts +++ b/packages/react-native-gesture-handler/src/index.ts @@ -147,8 +147,9 @@ export type { } from './handlers/gestureHandlerTypesCompat'; export type { - PressableProps as LegacyPressableProps, - PressableStateCallbackType as LegacyPressableCallbackType, + PressableProps, + LegacyPressableProps, + PressableStateCallbackType, } from './components/Pressable'; export { default as LegacyPressable } from './components/Pressable'; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx similarity index 94% rename from packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx rename to packages/react-native-gesture-handler/src/v3/components/Pressable.tsx index 4df81e1fa6..5c27c5cd28 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx @@ -6,10 +6,10 @@ import React, { useState, } from 'react'; import { + PressableDimensions, PressableEvent, PressableProps, - FullPressableDimensions, -} from './PressableProps'; +} from '../../components/Pressable/PressableProps'; import { Insets, LayoutChangeEvent, @@ -23,21 +23,23 @@ import { gestureTouchToPressableEvent, isTouchWithinInset, gestureToPressableEvent, -} from './utils'; - -import { getStatesConfig, StateMachineEvent } from './stateDefinitions'; -import { PressableStateMachine } from './StateMachine'; +} from '../../components/Pressable/utils'; +import { + getStatesConfig, + StateMachineEvent, +} from '../../components/Pressable/stateDefinitions'; +import { PressableStateMachine } from '../../components/Pressable/StateMachine'; import { useHoverGesture, useLongPressGesture, useNativeGesture, useSimultaneousGestures, -} from '../../hooks'; -import { GestureDetector } from '../../detectors'; -import { PureNativeButton } from '../GestureButtons'; +} from '../hooks'; +import { GestureDetector } from '../detectors'; +import { PureNativeButton } from './GestureButtons'; -import { PressabilityDebugView } from '../../../handlers/PressabilityDebugView'; -import { INT32_MAX } from '../../../utils'; +import { PressabilityDebugView } from '../../handlers/PressabilityDebugView'; +import { INT32_MAX } from '../../utils'; const DEFAULT_LONG_PRESS_DURATION = 500; // const IS_TEST_ENV = isTestEnv(); @@ -75,11 +77,9 @@ const Pressable = (props: PressableProps) => { const pressDelayTimeoutRef = useRef(null); const isOnPressAllowed = useRef(true); const isCurrentlyPressed = useRef(false); - const dimensions = useRef({ + const dimensions = useRef({ width: 0, height: 0, - x: 0, - y: 0, }); const normalizedHitSlop: Insets = useMemo( diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx deleted file mode 100644 index 21e44b06fa..0000000000 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/PressableProps.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { - AccessibilityProps, - ViewProps, - Insets, - StyleProp, - ViewStyle, - PressableStateCallbackType as RNPressableStateCallbackType, - PressableAndroidRippleConfig as RNPressableAndroidRippleConfig, - View, -} from 'react-native'; -import { AnyGesture } from '../../types'; - -export type PressableDimensions = { width: number; height: number }; -export type FullPressableDimensions = { - width: number; - height: number; - x: number; - y: number; -}; - -export type PressableStateCallbackType = RNPressableStateCallbackType; -export type PressableAndroidRippleConfig = RNPressableAndroidRippleConfig; - -export type InnerPressableEvent = { - changedTouches: InnerPressableEvent[]; - identifier: number; - locationX: number; - locationY: number; - pageX: number; - pageY: number; - target: number; - timestamp: number; - touches: InnerPressableEvent[]; - force?: number; -}; - -export type PressableEvent = { nativeEvent: InnerPressableEvent }; - -export interface PressableProps - extends AccessibilityProps, - Omit { - /** - * Called when the hover is activated to provide visual feedback. - */ - onHoverIn?: null | ((event: PressableEvent) => void); - - /** - * Called when the hover is deactivated to undo visual feedback. - */ - onHoverOut?: null | ((event: PressableEvent) => void); - - /** - * Called when a single tap gesture is detected. - */ - onPress?: null | ((event: PressableEvent) => void); - - /** - * Called when a touch is engaged before `onPress`. - */ - onPressIn?: null | ((event: PressableEvent) => void); - - /** - * Called when a touch is released before `onPress`. - */ - onPressOut?: null | ((event: PressableEvent) => void); - - /** - * Called when a long-tap gesture is detected. - */ - onLongPress?: null | ((event: PressableEvent) => void); - - /** - * A reference to the pressable element. - */ - ref?: React.Ref; - - /** - * Either children or a render prop that receives a boolean reflecting whether - * the component is currently pressed. - */ - children?: - | React.ReactNode - | ((state: PressableStateCallbackType) => React.ReactNode); - - /** - * Whether a press gesture can be interrupted by a parent gesture such as a - * scroll event. Defaults to true. - */ - cancelable?: null | boolean; - - /** - * Duration to wait after hover in before calling `onHoverIn`. - * @platform web macos - * - * NOTE: not present in RN docs - */ - delayHoverIn?: number | null; - - /** - * Duration to wait after hover out before calling `onHoverOut`. - * @platform web macos - * - * NOTE: not present in RN docs - */ - delayHoverOut?: number | null; - - /** - * Duration (in milliseconds) from `onPressIn` before `onLongPress` is called. - */ - delayLongPress?: null | number; - - /** - * Whether the press behavior is disabled. - */ - disabled?: null | boolean; - - /** - * Additional distance outside of this view in which a press is detected. - */ - hitSlop?: null | Insets | number; - - /** - * Additional distance outside of this view in which a touch is considered a - * press before `onPressOut` is triggered. - */ - pressRetentionOffset?: null | Insets | number; - - /** - * If true, doesn't play system sound on touch. - * @platform android - */ - android_disableSound?: null | boolean; - - /** - * Enables the Android ripple effect and configures its color. - * @platform android - */ - android_ripple?: null | PressableAndroidRippleConfig; - - /** - * Used only for documentation or testing (e.g. snapshot testing). - */ - testOnly_pressed?: null | boolean; - - /** - * 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); - - /** - * Duration (in milliseconds) to wait after press down before calling onPressIn. - */ - unstable_pressDelay?: number; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - simultaneousWith?: AnyGesture; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - requireToFail?: AnyGesture; - - /** - * A gesture object or an array of gesture objects containing the configuration and callbacks to be - * used with the Pressable's gesture handlers. - */ - block?: AnyGesture; - - /** - * @deprecated This property is no longer used, and will be removed in the future. - */ - dimensionsAfterResize?: PressableDimensions; -} diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx deleted file mode 100644 index b6d38ac828..0000000000 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/StateMachine.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { PressableEvent } from './PressableProps'; - -export interface StateDefinition { - eventName: string; - callback?: (event: PressableEvent) => void; -} - -class PressableStateMachine { - private states: StateDefinition[] | null; - private currentStepIndex: number; - private eventPayload: PressableEvent | null; - - constructor() { - this.states = null; - this.currentStepIndex = 0; - this.eventPayload = null; - } - - public setStates(states: StateDefinition[]) { - this.states = states; - } - - public reset() { - this.currentStepIndex = 0; - this.eventPayload = null; - } - - public handleEvent(eventName: string, eventPayload?: PressableEvent) { - if (!this.states) { - return; - } - const step = this.states[this.currentStepIndex]; - this.eventPayload = eventPayload || this.eventPayload; - - if (step.eventName !== eventName) { - if (this.currentStepIndex > 0) { - // retry with position at index 0 - this.reset(); - this.handleEvent(eventName, eventPayload); - } - return; - } - if (this.eventPayload && step.callback) { - step.callback(this.eventPayload); - } - this.currentStepIndex++; - - if (this.currentStepIndex === this.states.length) { - this.reset(); - } - } -} - -export { PressableStateMachine }; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts deleted file mode 100644 index 79ce14a8f0..0000000000 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { - PressableProps, - PressableStateCallbackType, -} from './PressableProps'; -export { default } from './Pressable'; diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts deleted file mode 100644 index 55279211cd..0000000000 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/stateDefinitions.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Platform } from 'react-native'; -import { PressableEvent } from './PressableProps'; -import { StateDefinition } from './StateMachine'; - -export enum StateMachineEvent { - NATIVE_BEGIN = 'nativeBegin', - NATIVE_START = 'nativeStart', - FINALIZE = 'finalize', - LONG_PRESS_TOUCHES_DOWN = 'longPressTouchesDown', - CANCEL = 'cancel', -} - -function getAndroidStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -) { - return [ - { - eventName: StateMachineEvent.NATIVE_BEGIN, - }, - { - eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, - callback: handlePressIn, - }, - { - eventName: StateMachineEvent.FINALIZE, - callback: handlePressOut, - }, - ]; -} - -function getIosStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -) { - return [ - { - eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, - }, - { - eventName: StateMachineEvent.NATIVE_START, - callback: handlePressIn, - }, - { - eventName: StateMachineEvent.FINALIZE, - callback: handlePressOut, - }, - ]; -} - -function getWebStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -) { - return [ - { - eventName: StateMachineEvent.NATIVE_BEGIN, - }, - { - eventName: StateMachineEvent.NATIVE_START, - }, - { - eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, - callback: handlePressIn, - }, - { - eventName: StateMachineEvent.FINALIZE, - callback: handlePressOut, - }, - ]; -} - -function getMacosStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -) { - return [ - { - eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN, - }, - { - eventName: StateMachineEvent.NATIVE_BEGIN, - callback: handlePressIn, - }, - { - eventName: StateMachineEvent.NATIVE_START, - }, - { - eventName: StateMachineEvent.FINALIZE, - callback: handlePressOut, - }, - ]; -} - -function getUniversalStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -) { - return [ - { - eventName: StateMachineEvent.FINALIZE, - callback: (event: PressableEvent) => { - handlePressIn(event); - handlePressOut(event); - }, - }, - ]; -} - -export function getStatesConfig( - handlePressIn: (event: PressableEvent) => void, - handlePressOut: (event: PressableEvent) => void -): StateDefinition[] { - if (Platform.OS === 'android') { - return getAndroidStatesConfig(handlePressIn, handlePressOut); - } else if (Platform.OS === 'ios') { - return getIosStatesConfig(handlePressIn, handlePressOut); - } else if (Platform.OS === 'web') { - return getWebStatesConfig(handlePressIn, handlePressOut); - } else if (Platform.OS === 'macos') { - return getMacosStatesConfig(handlePressIn, handlePressOut); - } else { - // Unknown platform - using minimal universal setup. - return getUniversalStatesConfig(handlePressIn, handlePressOut); - } -} diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts b/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts deleted file mode 100644 index cd99adaaf7..0000000000 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable/utils.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Insets } from 'react-native'; -import { - InnerPressableEvent, - PressableEvent, - FullPressableDimensions, -} from './PressableProps'; -import { - GestureTouchEvent, - TouchData, -} from '../../../handlers/gestureHandlerCommon'; -import { HoverGestureEvent } from '../../hooks/gestures/hover/useHoverGesture'; -import { LongPressGestureEvent } from '../../hooks'; - -const numberAsInset = (value: number): Insets => ({ - left: value, - right: value, - top: value, - bottom: value, -}); - -const addInsets = (a: Insets, b: Insets): Insets => ({ - left: (a.left ?? 0) + (b.left ?? 0), - right: (a.right ?? 0) + (b.right ?? 0), - top: (a.top ?? 0) + (b.top ?? 0), - bottom: (a.bottom ?? 0) + (b.bottom ?? 0), -}); - -const touchDataToPressEvent = ( - data: TouchData, - timestamp: number, - targetId: number -): InnerPressableEvent => ({ - identifier: data.id, - locationX: data.x, - locationY: data.y, - pageX: data.absoluteX, - pageY: data.absoluteY, - target: targetId, - timestamp: timestamp, - touches: [], // Always empty - legacy compatibility - changedTouches: [], // Always empty - legacy compatibility -}); - -const gestureToPressEvent = ( - event: HoverGestureEvent | LongPressGestureEvent, - timestamp: number, - targetId: number -): InnerPressableEvent => ({ - identifier: event.handlerTag, - locationX: event.x, - locationY: event.y, - pageX: event.absoluteX, - pageY: event.absoluteY, - target: targetId, - timestamp: timestamp, - touches: [], // Always empty - legacy compatibility - changedTouches: [], // Always empty - legacy compatibility -}); - -const isTouchWithinInset = ( - dimensions: FullPressableDimensions, - inset: Insets, - touch?: InnerPressableEvent -) => - (touch?.locationX ?? 0) < (inset.right ?? 0) + dimensions.width && - (touch?.locationY ?? 0) < (inset.bottom ?? 0) + dimensions.height && - (touch?.locationX ?? 0) > -(inset.left ?? 0) && - (touch?.locationY ?? 0) > -(inset.top ?? 0); - -const gestureToPressableEvent = ( - event: HoverGestureEvent | LongPressGestureEvent -): PressableEvent => { - const timestamp = Date.now(); - - // As far as I can see, there isn't a conventional way of getting targetId with the data we get - const targetId = 0; - - const pressEvent = gestureToPressEvent(event, timestamp, targetId); - - return { - nativeEvent: { - touches: [pressEvent], - changedTouches: [pressEvent], - identifier: pressEvent.identifier, - locationX: event.x, - locationY: event.y, - pageX: event.absoluteX, - pageY: event.absoluteY, - target: targetId, - timestamp: timestamp, - force: undefined, - }, - }; -}; - -const gestureTouchToPressableEvent = ( - event: GestureTouchEvent -): PressableEvent => { - const timestamp = Date.now(); - - // As far as I can see, there isn't a conventional way of getting targetId with the data we get - const targetId = 0; - - const touchesList = event.allTouches.map((touch: TouchData) => - touchDataToPressEvent(touch, timestamp, targetId) - ); - const changedTouchesList = event.changedTouches.map((touch: TouchData) => - touchDataToPressEvent(touch, timestamp, targetId) - ); - - return { - nativeEvent: { - touches: touchesList, - changedTouches: changedTouchesList, - identifier: event.handlerTag, - locationX: event.allTouches.at(0)?.x ?? -1, - locationY: event.allTouches.at(0)?.y ?? -1, - pageX: event.allTouches.at(0)?.absoluteX ?? -1, - pageY: event.allTouches.at(0)?.absoluteY ?? -1, - target: targetId, - timestamp: timestamp, - force: undefined, - }, - }; -}; - -export { - numberAsInset, - addInsets, - isTouchWithinInset, - gestureToPressableEvent, - gestureTouchToPressableEvent, -}; diff --git a/packages/react-native-gesture-handler/src/v3/components/index.ts b/packages/react-native-gesture-handler/src/v3/components/index.ts index a551f25f66..bf4bbc5526 100644 --- a/packages/react-native-gesture-handler/src/v3/components/index.ts +++ b/packages/react-native-gesture-handler/src/v3/components/index.ts @@ -21,5 +21,4 @@ export { RefreshControl, } from './GestureComponents'; -export type { PressableProps, PressableStateCallbackType } from './Pressable'; export { default as Pressable } from './Pressable'; diff --git a/packages/react-native-gesture-handler/src/v3/index.ts b/packages/react-native-gesture-handler/src/v3/index.ts index ad50434663..835113c66d 100644 --- a/packages/react-native-gesture-handler/src/v3/index.ts +++ b/packages/react-native-gesture-handler/src/v3/index.ts @@ -64,6 +64,7 @@ export { RectButton, BorderlessButton, PureNativeButton, + Pressable, ScrollView, Switch, TextInput, @@ -71,12 +72,6 @@ export { RefreshControl, } from './components'; -export { - PressableProps, - PressableStateCallbackType, - Pressable, -} from './components'; - export type { ComposedGesture } from './types'; export { GestureStateManager } from './gestureStateManager'; From c5475ab252f826056b45856627e00ac4aa47c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 12:25:45 +0100 Subject: [PATCH 20/22] delete depreacated --- .../src/v3/components/Pressable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx index 5c27c5cd28..e130e66a8c 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx @@ -40,8 +40,8 @@ import { PureNativeButton } from './GestureButtons'; import { PressabilityDebugView } from '../../handlers/PressabilityDebugView'; import { INT32_MAX } from '../../utils'; + const DEFAULT_LONG_PRESS_DURATION = 500; -// const IS_TEST_ENV = isTestEnv(); const Pressable = (props: PressableProps) => { const { From c7ff5432a1eb11ba1d71b05cee19d0af6a0bc5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Tue, 20 Jan 2026 13:41:51 +0100 Subject: [PATCH 21/22] readded test props --- .../src/v3/components/Pressable.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx index e130e66a8c..66a842292b 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx @@ -39,9 +39,10 @@ import { GestureDetector } from '../detectors'; import { PureNativeButton } from './GestureButtons'; import { PressabilityDebugView } from '../../handlers/PressabilityDebugView'; -import { INT32_MAX } from '../../utils'; +import { INT32_MAX, isTestEnv } from '../../utils'; const DEFAULT_LONG_PRESS_DURATION = 500; +const IS_TEST_ENV = isTestEnv(); const Pressable = (props: PressableProps) => { const { @@ -370,7 +371,11 @@ const Pressable = (props: PressableProps) => { touchSoundDisabled={android_disableSound ?? undefined} rippleColor={rippleColor} rippleRadius={android_ripple?.radius ?? undefined} - style={[pointerStyle, styleProp, { minWidth: 44, minHeight: 44 }]}> + style={[pointerStyle, styleProp, { minWidth: 44, minHeight: 44 }]} + testOnly_onPress={IS_TEST_ENV ? onPress : undefined} + testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined} + testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined} + testOnly_onLongPress={IS_TEST_ENV ? onLongPress : undefined}> {childrenProp} {__DEV__ ? ( From 8eabd7250a5a2698ed33fa3680172c9975481c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Antoni=20Kwa=C5=9Bniewski?= Date: Wed, 21 Jan 2026 20:08:53 +0100 Subject: [PATCH 22/22] remove 44 --- .../src/v3/components/Pressable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx index 66a842292b..50d3ce15e5 100644 --- a/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx +++ b/packages/react-native-gesture-handler/src/v3/components/Pressable.tsx @@ -371,7 +371,7 @@ const Pressable = (props: PressableProps) => { touchSoundDisabled={android_disableSound ?? undefined} rippleColor={rippleColor} rippleRadius={android_ripple?.radius ?? undefined} - style={[pointerStyle, styleProp, { minWidth: 44, minHeight: 44 }]} + style={[pointerStyle, styleProp]} testOnly_onPress={IS_TEST_ENV ? onPress : undefined} testOnly_onPressIn={IS_TEST_ENV ? onPressIn : undefined} testOnly_onPressOut={IS_TEST_ENV ? onPressOut : undefined}