UNPKG

react-native-gesture-handler

Version:

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

324 lines (316 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _gestureObjects = require("../../handlers/gestures/gestureObjects"); var _GestureDetector = require("../../handlers/gestures/GestureDetector"); var _reactNative = require("react-native"); var _GestureHandlerButton = _interopRequireDefault(require("../GestureHandlerButton")); var _utils = require("./utils"); var _PressabilityDebugView = require("../../handlers/PressabilityDebugView"); var _utils2 = require("../../utils"); var _utils3 = require("../utils"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } const DEFAULT_LONG_PRESS_DURATION = 500; const IS_TEST_ENV = (0, _utils2.isTestEnv)(); let IS_FABRIC = null; const Pressable = /*#__PURE__*/(0, _react.forwardRef)((props, pressableRef) => { const { testOnly_pressed, hitSlop, pressRetentionOffset, delayHoverIn, onHoverIn, delayHoverOut, onHoverOut, delayLongPress, unstable_pressDelay, onPress, onPressIn, onPressOut, onLongPress, style, children, android_disableSound, android_ripple, disabled, accessible, simultaneousWithExternalGesture, requireExternalGestureToFail, blocksExternalGesture, ...remainingProps } = props; const relationProps = { simultaneousWithExternalGesture, requireExternalGestureToFail, blocksExternalGesture }; const [pressedState, setPressedState] = (0, _react.useState)(testOnly_pressed ?? false); // Disabled when onLongPress has been called const isPressCallbackEnabled = (0, _react.useRef)(true); const hasPassedBoundsChecks = (0, _react.useRef)(false); const shouldPreventNativeEffects = (0, _react.useRef)(false); const normalizedHitSlop = (0, _react.useMemo)(() => typeof hitSlop === 'number' ? (0, _utils.numberAsInset)(hitSlop) : hitSlop ?? {}, [hitSlop]); const normalizedPressRetentionOffset = (0, _react.useMemo)(() => typeof pressRetentionOffset === 'number' ? (0, _utils.numberAsInset)(pressRetentionOffset) : pressRetentionOffset ?? {}, [pressRetentionOffset]); const hoverInTimeout = (0, _react.useRef)(null); const hoverOutTimeout = (0, _react.useRef)(null); const hoverGesture = (0, _react.useMemo)(() => _gestureObjects.GestureObjects.Hover().manualActivation(true) // Stops Hover from blocking Native gesture activation on web .cancelsTouchesInView(false).onBegin(event => { if (hoverOutTimeout.current) { clearTimeout(hoverOutTimeout.current); } if (delayHoverIn) { hoverInTimeout.current = setTimeout(() => onHoverIn?.((0, _utils.gestureToPressableEvent)(event)), delayHoverIn); return; } onHoverIn?.((0, _utils.gestureToPressableEvent)(event)); }).onFinalize(event => { if (hoverInTimeout.current) { clearTimeout(hoverInTimeout.current); } if (delayHoverOut) { hoverOutTimeout.current = setTimeout(() => onHoverOut?.((0, _utils.gestureToPressableEvent)(event)), delayHoverOut); return; } onHoverOut?.((0, _utils.gestureToPressableEvent)(event)); }), [delayHoverIn, delayHoverOut, onHoverIn, onHoverOut]); const pressDelayTimeoutRef = (0, _react.useRef)(null); const isTouchPropagationAllowed = (0, _react.useRef)(false); // iOS only: due to varying flow of gestures, events sometimes have to be saved for later use const deferredEventPayload = (0, _react.useRef)(null); const pressInHandler = (0, _react.useCallback)(event => { if (handlingOnTouchesDown.current) { deferredEventPayload.current = event; } if (!isTouchPropagationAllowed.current) { return; } deferredEventPayload.current = null; onPressIn?.(event); isPressCallbackEnabled.current = true; pressDelayTimeoutRef.current = null; setPressedState(true); }, [onPressIn]); const pressOutHandler = (0, _react.useCallback)(event => { if (!isTouchPropagationAllowed.current) { hasPassedBoundsChecks.current = false; isPressCallbackEnabled.current = true; deferredEventPayload.current = null; if (longPressTimeoutRef.current) { clearTimeout(longPressTimeoutRef.current); longPressTimeoutRef.current = null; } if (pressDelayTimeoutRef.current) { clearTimeout(pressDelayTimeoutRef.current); pressDelayTimeoutRef.current = null; } return; } if (!hasPassedBoundsChecks.current || event.nativeEvent.touches.length > event.nativeEvent.changedTouches.length) { return; } if (unstable_pressDelay && pressDelayTimeoutRef.current !== null) { // When delay is preemptively finished by lifting touches, // we want to immediately activate it's effects - pressInHandler, // even though we are located at the pressOutHandler clearTimeout(pressDelayTimeoutRef.current); pressInHandler(event); } if (deferredEventPayload.current) { onPressIn?.(deferredEventPayload.current); deferredEventPayload.current = null; } onPressOut?.(event); if (isPressCallbackEnabled.current) { onPress?.(event); } if (longPressTimeoutRef.current) { clearTimeout(longPressTimeoutRef.current); longPressTimeoutRef.current = null; } isTouchPropagationAllowed.current = false; hasPassedBoundsChecks.current = false; isPressCallbackEnabled.current = true; setPressedState(false); }, [onPress, onPressIn, onPressOut, pressInHandler, unstable_pressDelay]); const handlingOnTouchesDown = (0, _react.useRef)(false); const onEndHandlingTouchesDown = (0, _react.useRef)(null); const cancelledMidPress = (0, _react.useRef)(false); const activateLongPress = (0, _react.useCallback)(event => { if (!isTouchPropagationAllowed.current) { return; } if (hasPassedBoundsChecks.current && onLongPress) { onLongPress((0, _utils.gestureTouchToPressableEvent)(event)); isPressCallbackEnabled.current = false; } if (longPressTimeoutRef.current) { clearTimeout(longPressTimeoutRef.current); longPressTimeoutRef.current = null; } }, [onLongPress]); const longPressTimeoutRef = (0, _react.useRef)(null); const longPressMinDuration = (delayLongPress ?? DEFAULT_LONG_PRESS_DURATION) + (unstable_pressDelay ?? 0); const innerPressableRef = (0, _react.useRef)(null); const measureCallback = (0, _react.useCallback)((width, height, event) => { if (!(0, _utils.isTouchWithinInset)({ width, height }, normalizedHitSlop, event.changedTouches.at(-1)) || hasPassedBoundsChecks.current || cancelledMidPress.current) { cancelledMidPress.current = false; onEndHandlingTouchesDown.current = null; handlingOnTouchesDown.current = false; return; } hasPassedBoundsChecks.current = true; // In case of multiple touches, the first one starts long press gesture if (longPressTimeoutRef.current === null) { // Start long press gesture timer longPressTimeoutRef.current = setTimeout(() => activateLongPress(event), longPressMinDuration); } if (unstable_pressDelay) { pressDelayTimeoutRef.current = setTimeout(() => { pressInHandler((0, _utils.gestureTouchToPressableEvent)(event)); }, unstable_pressDelay); } else { pressInHandler((0, _utils.gestureTouchToPressableEvent)(event)); } onEndHandlingTouchesDown.current?.(); onEndHandlingTouchesDown.current = null; handlingOnTouchesDown.current = false; }, [activateLongPress, longPressMinDuration, normalizedHitSlop, pressInHandler, unstable_pressDelay]); const pressAndTouchGesture = (0, _react.useMemo)(() => _gestureObjects.GestureObjects.LongPress().minDuration(_utils2.INT32_MAX) // Stops long press from blocking native gesture .maxDistance(_utils2.INT32_MAX) // Stops long press from cancelling after set distance .cancelsTouchesInView(false).onTouchesDown(event => { handlingOnTouchesDown.current = true; if (pressableRef) { pressableRef.current?.measure((_x, _y, width, height) => { measureCallback(width, height, event); }); } else { innerPressableRef.current?.measure((_x, _y, width, height) => { measureCallback(width, height, event); }); } }).onTouchesUp(event => { if (handlingOnTouchesDown.current) { onEndHandlingTouchesDown.current = () => pressOutHandler((0, _utils.gestureTouchToPressableEvent)(event)); return; } // On iOS, short taps will make LongPress gesture call onTouchesUp before Native gesture calls onStart // This variable ensures that onStart isn't detected as the first gesture since Pressable is pressed. if (deferredEventPayload.current !== null) { shouldPreventNativeEffects.current = true; } pressOutHandler((0, _utils.gestureTouchToPressableEvent)(event)); }).onTouchesCancelled(event => { isPressCallbackEnabled.current = false; if (handlingOnTouchesDown.current) { cancelledMidPress.current = true; onEndHandlingTouchesDown.current = () => pressOutHandler((0, _utils.gestureTouchToPressableEvent)(event)); return; } if (!hasPassedBoundsChecks.current || event.allTouches.length > event.changedTouches.length) { return; } pressOutHandler((0, _utils.gestureTouchToPressableEvent)(event)); }), [pressableRef, measureCallback, pressOutHandler]); // RNButton is placed inside ButtonGesture to enable Android's ripple and to capture non-propagating events const buttonGesture = (0, _react.useMemo)(() => _gestureObjects.GestureObjects.Native().onBegin(() => { // Android sets BEGAN state on press down if (_reactNative.Platform.OS === 'android' || _reactNative.Platform.OS === 'macos') { isTouchPropagationAllowed.current = true; } }).onStart(() => { if (_reactNative.Platform.OS === 'web') { isTouchPropagationAllowed.current = true; } // iOS sets ACTIVE state on press down if (_reactNative.Platform.OS !== 'ios') { return; } if (deferredEventPayload.current) { isTouchPropagationAllowed.current = true; if (hasPassedBoundsChecks.current) { pressInHandler(deferredEventPayload.current); deferredEventPayload.current = null; } else { pressOutHandler(deferredEventPayload.current); isTouchPropagationAllowed.current = false; } return; } if (hasPassedBoundsChecks.current) { isTouchPropagationAllowed.current = true; return; } if (shouldPreventNativeEffects.current) { shouldPreventNativeEffects.current = false; if (!handlingOnTouchesDown.current) { return; } } isTouchPropagationAllowed.current = true; }), [pressInHandler, pressOutHandler]); const appliedHitSlop = (0, _utils.addInsets)(normalizedHitSlop, normalizedPressRetentionOffset); const isPressableEnabled = disabled !== true; const gestures = [buttonGesture, pressAndTouchGesture, hoverGesture]; for (const gesture of gestures) { gesture.enabled(isPressableEnabled); gesture.runOnJS(true); gesture.hitSlop(appliedHitSlop); gesture.shouldCancelWhenOutside(_reactNative.Platform.OS === 'web' ? false : true); Object.entries(relationProps).forEach(([relationName, relation]) => { (0, _utils3.applyRelationProp)(gesture, relationName, relation); }); } // Uses different hitSlop, to activate on hitSlop area instead of pressRetentionOffset area buttonGesture.hitSlop(normalizedHitSlop); const gesture = _gestureObjects.GestureObjects.Simultaneous(...gestures); // `cursor: 'pointer'` on `RNButton` crashes iOS const pointerStyle = _reactNative.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 = (0, _react.useMemo)(() => { if (IS_FABRIC === null) { IS_FABRIC = (0, _utils2.isFabric)(); } const defaultRippleColor = android_ripple ? undefined : 'transparent'; const unprocessedRippleColor = android_ripple?.color ?? defaultRippleColor; return IS_FABRIC ? unprocessedRippleColor : (0, _reactNative.processColor)(unprocessedRippleColor); }, [android_ripple]); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_GestureDetector.GestureDetector, { gesture: gesture, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_GestureHandlerButton.default, { ...remainingProps, ref: pressableRef ?? innerPressableRef, accessible: accessible !== false, hitSlop: appliedHitSlop, enabled: isPressableEnabled, touchSoundDisabled: android_disableSound ?? undefined, rippleColor: rippleColor, rippleRadius: android_ripple?.radius ?? undefined, style: [pointerStyle, styleProp], 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, children: [childrenProp, __DEV__ ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_PressabilityDebugView.PressabilityDebugView, { color: "red", hitSlop: normalizedHitSlop }) : null] }) }); }); var _default = exports.default = Pressable; //# sourceMappingURL=Pressable.js.map