UNPKG

react-native-gesture-handler

Version:

Declarative API exposing native platform touch and gesture system to React Native

267 lines (256 loc) 8.69 kB
"use strict"; import React, { useEffect, useRef, useState } from 'react'; import { addInsets, isTouchWithinInset, numberAsInset } from '../../components/Pressable/utils'; import { PressabilityDebugView } from '../../handlers/PressabilityDebugView'; import { isTestEnv } from '../../utils'; import { pointerStyle } from './pointerStyle'; import { Touchable } from './Touchable/Touchable'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const IS_TEST_ENV = isTestEnv(); // RN's Pressable default. Touchable's own default is 600ms const DEFAULT_LONG_PRESS_DURATION = 500; function normalizeInset(value) { return typeof value === 'number' ? numberAsInset(value) : value ?? numberAsInset(0); } function buttonToInner(event) { return { identifier: 0, locationX: event.x, locationY: event.y, pageX: event.absoluteX, pageY: event.absoluteY, target: 0, timestamp: Date.now(), touches: [], changedTouches: [], force: undefined }; } function buttonToPressableEvent(event) { const inner = buttonToInner(event); return { nativeEvent: { ...inner, touches: [inner], changedTouches: [inner] } }; } const PressableWithTouchable = props => { 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, ref, ...rest } = props; // Drop props Touchable doesn't take: `cancelable`/`dimensionsAfterResize` are // unsupported; the relation props are handled by the wrapper. // /* eslint-disable @typescript-eslint/no-unused-vars */ const { cancelable, dimensionsAfterResize, simultaneousWith, requireToFail, block, ...remainingProps } = rest; /* eslint-enable @typescript-eslint/no-unused-vars */ const [pressed, setPressed] = useState(false); const timers = useRef({ press: null, hoverIn: null, hoverOut: null }); const dimensions = useRef({ width: 0, height: 0 }); // Whether the in-progress press activated within hitSlop (see handlePressIn). const isActive = useRef(false); useEffect(() => () => { const pending = timers.current; if (pending.press) { clearTimeout(pending.press); } if (pending.hoverIn) { clearTimeout(pending.hoverIn); } if (pending.hoverOut) { clearTimeout(pending.hoverOut); } }, []); // RN's ripple config allows `null` on every field; Touchable's excludes it. // Normalize `null` → `undefined` so the types line up. const androidRipple = android_ripple ? { color: android_ripple.color ?? undefined, borderless: android_ripple.borderless ?? undefined, radius: android_ripple.radius ?? undefined, foreground: android_ripple.foreground ?? undefined } : undefined; // Activation is gated to `normalizedHitSlop` (see handlePressIn); the native // button gets the wider `appliedHitSlop` so an active press is retained out // to hitSlop + pressRetentionOffset before it cancels. const normalizedHitSlop = normalizeInset(hitSlop); const appliedHitSlop = addInsets(normalizedHitSlop, normalizeInset(pressRetentionOffset)); // Long press is measured from onPressIn, which `unstable_pressDelay` defers, // so fold it in to match RN / StatefulPressable. const resolvedDelayLongPress = (delayLongPress ?? DEFAULT_LONG_PRESS_DURATION) + (unstable_pressDelay ?? 0); const handleLayout = event => { onLayout?.(event); dimensions.current = event.nativeEvent.layout; }; const firePressIn = event => { setPressed(true); onPressIn?.(buttonToPressableEvent(event)); }; const handlePressIn = event => { // A down in the retention-only zone is held by the button but isn't a press. isActive.current = isTouchWithinInset(dimensions.current, normalizedHitSlop, buttonToInner(event)); if (!isActive.current) { return; } if (unstable_pressDelay) { // Drop a still-pending timer so a re-entrant press can't double-fire. if (timers.current.press) { clearTimeout(timers.current.press); } timers.current.press = setTimeout(() => { timers.current.press = null; firePressIn(event); }, unstable_pressDelay); return; } firePressIn(event); }; const handlePressOut = event => { // Not cleared here: onPress fires after onPressOut and must stay suppressed // too; isActive resets on the next press-in. if (!isActive.current) { return; } // If the touch is released before `unstable_pressDelay` elapses, RN still // emits the deferred `onPressIn` before `onPressOut` — flush it now. if (timers.current.press) { clearTimeout(timers.current.press); timers.current.press = null; firePressIn(event); } setPressed(false); onPressOut?.(buttonToPressableEvent(event)); }; const handlePress = onPress ? event => { if (isActive.current) { onPress(buttonToPressableEvent(event)); } } : undefined; const handleLongPress = onLongPress ? event => { if (!isActive.current) { return; } // Flush the deferred onPressIn so it can't arrive after onLongPress // (e.g. delayLongPress={0} makes the two timers coincide). if (timers.current.press) { clearTimeout(timers.current.press); timers.current.press = null; firePressIn(event); } onLongPress(buttonToPressableEvent(event)); } : undefined; // Wire each handler whenever the opposite side can leave a pending timer, so a // delayed hover callback is cancelled once the pointer leaves/re-enters. const needsHoverIn = onHoverIn != null || onHoverOut != null && !!delayHoverOut; const needsHoverOut = onHoverOut != null || onHoverIn != null && !!delayHoverIn; const handleHoverIn = needsHoverIn ? event => { if (timers.current.hoverOut) { clearTimeout(timers.current.hoverOut); timers.current.hoverOut = null; } if (!onHoverIn) { return; } if (delayHoverIn) { timers.current.hoverIn = setTimeout(() => { timers.current.hoverIn = null; onHoverIn(buttonToPressableEvent(event)); }, delayHoverIn); return; } onHoverIn(buttonToPressableEvent(event)); } : undefined; const handleHoverOut = needsHoverOut ? event => { if (timers.current.hoverIn) { clearTimeout(timers.current.hoverIn); timers.current.hoverIn = null; } if (!onHoverOut) { return; } if (delayHoverOut) { timers.current.hoverOut = setTimeout(() => { timers.current.hoverOut = null; onHoverOut(buttonToPressableEvent(event)); }, delayHoverOut); return; } onHoverOut(buttonToPressableEvent(event)); } : undefined; // `testOnly_pressed` forces the pressed state for snapshots/tests. Derive the // displayed value from it each render, keeping the interactive `pressed` state // independent (seeded to false) so clearing the prop doesn't leave it stuck. const displayPressed = testOnly_pressed ?? pressed; const resolvedStyle = typeof style === 'function' ? style({ pressed: displayPressed }) : style; const resolvedChildren = typeof children === 'function' ? children({ pressed: displayPressed }) : children; return /*#__PURE__*/_jsxs(Touchable, { ...remainingProps, ref: ref, accessible: accessible !== false, disabled: disabled === true, hitSlop: appliedHitSlop, onLayout: handleLayout, androidRipple: androidRipple, touchSoundDisabled: android_disableSound ?? undefined, delayLongPress: resolvedDelayLongPress, style: [pointerStyle, resolvedStyle], onPressIn: handlePressIn, onPressOut: handlePressOut, onPress: handlePress, onLongPress: handleLongPress, onHoverIn: handleHoverIn, onHoverOut: handleHoverOut, 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, testOnly_onHoverIn: IS_TEST_ENV ? onHoverIn : undefined, testOnly_onHoverOut: IS_TEST_ENV ? onHoverOut : undefined, children: [resolvedChildren, __DEV__ ? /*#__PURE__*/_jsx(PressabilityDebugView, { color: "red", hitSlop: normalizedHitSlop }) : null] }); }; export default PressableWithTouchable; //# sourceMappingURL=PressableWithTouchable.js.map