UNPKG

react-native-gesture-handler

Version:

Experimental implementation of a new declarative API for gesture handling in react-native

334 lines (260 loc) 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = Pressable; 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"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const DEFAULT_LONG_PRESS_DURATION = 500; function Pressable(props) { var _props$testOnly_press, _props$delayLongPress, _props$unstable_press, _props$android_disabl, _props$android_ripple, _props$android_ripple2, _props$android_ripple3, _props$android_ripple4; const [pressedState, setPressedState] = (0, _react.useState)((_props$testOnly_press = props.testOnly_pressed) !== null && _props$testOnly_press !== void 0 ? _props$testOnly_press : false); const pressableRef = (0, _react.useRef)(null); // 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)(() => { var _props$hitSlop; return typeof props.hitSlop === 'number' ? (0, _utils.numberAsInset)(props.hitSlop) : (_props$hitSlop = props.hitSlop) !== null && _props$hitSlop !== void 0 ? _props$hitSlop : {}; }, [props.hitSlop]); const normalizedPressRetentionOffset = (0, _react.useMemo)(() => { var _props$pressRetention; return typeof props.pressRetentionOffset === 'number' ? (0, _utils.numberAsInset)(props.pressRetentionOffset) : (_props$pressRetention = props.pressRetentionOffset) !== null && _props$pressRetention !== void 0 ? _props$pressRetention : {}; }, [props.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 => { var _props$onHoverIn2; if (hoverOutTimeout.current) { clearTimeout(hoverOutTimeout.current); } if (props.delayHoverIn) { hoverInTimeout.current = setTimeout(() => { var _props$onHoverIn; return (_props$onHoverIn = props.onHoverIn) === null || _props$onHoverIn === void 0 ? void 0 : _props$onHoverIn.call(props, (0, _utils.gestureToPressableEvent)(event)); }, props.delayHoverIn); return; } (_props$onHoverIn2 = props.onHoverIn) === null || _props$onHoverIn2 === void 0 ? void 0 : _props$onHoverIn2.call(props, (0, _utils.gestureToPressableEvent)(event)); }).onFinalize(event => { var _props$onHoverOut2; if (hoverInTimeout.current) { clearTimeout(hoverInTimeout.current); } if (props.delayHoverOut) { hoverOutTimeout.current = setTimeout(() => { var _props$onHoverOut; return (_props$onHoverOut = props.onHoverOut) === null || _props$onHoverOut === void 0 ? void 0 : _props$onHoverOut.call(props, (0, _utils.gestureToPressableEvent)(event)); }, props.delayHoverOut); return; } (_props$onHoverOut2 = props.onHoverOut) === null || _props$onHoverOut2 === void 0 ? void 0 : _props$onHoverOut2.call(props, (0, _utils.gestureToPressableEvent)(event)); }), [props]); 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 => { var _props$onPressIn; if (handlingOnTouchesDown.current) { deferredEventPayload.current = event; } if (!isTouchPropagationAllowed.current) { return; } deferredEventPayload.current = null; (_props$onPressIn = props.onPressIn) === null || _props$onPressIn === void 0 ? void 0 : _props$onPressIn.call(props, event); isPressCallbackEnabled.current = true; pressDelayTimeoutRef.current = null; setPressedState(true); }, [props]); const pressOutHandler = (0, _react.useCallback)(event => { var _props$onPressOut; if (!hasPassedBoundsChecks.current || event.nativeEvent.touches.length > event.nativeEvent.changedTouches.length) { return; } if (props.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) { var _props$onPressIn2; (_props$onPressIn2 = props.onPressIn) === null || _props$onPressIn2 === void 0 ? void 0 : _props$onPressIn2.call(props, deferredEventPayload.current); deferredEventPayload.current = null; } (_props$onPressOut = props.onPressOut) === null || _props$onPressOut === void 0 ? void 0 : _props$onPressOut.call(props, event); if (isPressCallbackEnabled.current) { var _props$onPress; (_props$onPress = props.onPress) === null || _props$onPress === void 0 ? void 0 : _props$onPress.call(props, event); } if (longPressTimeoutRef.current) { clearTimeout(longPressTimeoutRef.current); longPressTimeoutRef.current = null; } isTouchPropagationAllowed.current = false; hasPassedBoundsChecks.current = false; isPressCallbackEnabled.current = true; setPressedState(false); }, [pressInHandler, props]); 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) { var _props$onLongPress; (_props$onLongPress = props.onLongPress) === null || _props$onLongPress === void 0 ? void 0 : _props$onLongPress.call(props, (0, _utils.gestureTouchToPressableEvent)(event)); isPressCallbackEnabled.current = false; } if (longPressTimeoutRef.current) { clearTimeout(longPressTimeoutRef.current); longPressTimeoutRef.current = null; } }, [props]); const longPressTimeoutRef = (0, _react.useRef)(null); const longPressMinDuration = ((_props$delayLongPress = props.delayLongPress) !== null && _props$delayLongPress !== void 0 ? _props$delayLongPress : DEFAULT_LONG_PRESS_DURATION) + ((_props$unstable_press = props.unstable_pressDelay) !== null && _props$unstable_press !== void 0 ? _props$unstable_press : 0); const pressAndTouchGesture = (0, _react.useMemo)(() => _gestureObjects.GestureObjects.LongPress().minDuration(Number.MAX_SAFE_INTEGER) // Stops long press from blocking native gesture .maxDistance(Number.MAX_SAFE_INTEGER) // Stops long press from cancelling after set distance .cancelsTouchesInView(false).onTouchesDown(event => { var _pressableRef$current; handlingOnTouchesDown.current = true; (_pressableRef$current = pressableRef.current) === null || _pressableRef$current === void 0 ? void 0 : _pressableRef$current.measure((_x, _y, width, height) => { var _onEndHandlingTouches; 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 (props.unstable_pressDelay) { pressDelayTimeoutRef.current = setTimeout(() => { pressInHandler((0, _utils.gestureTouchToPressableEvent)(event)); }, props.unstable_pressDelay); } else { pressInHandler((0, _utils.gestureTouchToPressableEvent)(event)); } (_onEndHandlingTouches = onEndHandlingTouchesDown.current) === null || _onEndHandlingTouches === void 0 ? void 0 : _onEndHandlingTouches.call(onEndHandlingTouchesDown); onEndHandlingTouchesDown.current = null; handlingOnTouchesDown.current = false; }); }).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)); }), [activateLongPress, longPressMinDuration, normalizedHitSlop, pressInHandler, pressOutHandler, props.unstable_pressDelay]); // 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') { 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; return; } isTouchPropagationAllowed.current = true; }), [pressInHandler, pressOutHandler]); const appliedHitSlop = (0, _utils.addInsets)(normalizedHitSlop, normalizedPressRetentionOffset); const isPressableEnabled = props.disabled !== true; const gestures = [pressAndTouchGesture, hoverGesture, buttonGesture]; for (const gesture of gestures) { gesture.enabled(isPressableEnabled); gesture.runOnJS(true); gesture.hitSlop(appliedHitSlop); gesture.shouldCancelWhenOutside(false); if (_reactNative.Platform.OS !== 'web') { gesture.shouldCancelWhenOutside(true); } } // Uses different hitSlop, to activate on hitSlop area instead of pressRetentionOffset area buttonGesture.hitSlop(normalizedHitSlop); const gesture = _gestureObjects.GestureObjects.Simultaneous(...gestures); const defaultRippleColor = props.android_ripple ? undefined : 'transparent'; // `cursor: 'pointer'` on `RNButton` crashes iOS const pointerStyle = _reactNative.Platform.OS === 'web' ? { cursor: 'pointer' } : {}; const styleProp = typeof props.style === 'function' ? props.style({ pressed: pressedState }) : props.style; const childrenProp = typeof props.children === 'function' ? props.children({ pressed: pressedState }) : props.children; const flattenedStyles = _reactNative.StyleSheet.flatten(styleProp !== null && styleProp !== void 0 ? styleProp : {}); const [innerStyles, outerStyles] = (0, _utils.splitStyles)(flattenedStyles); return /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: outerStyles }, /*#__PURE__*/_react.default.createElement(_GestureDetector.GestureDetector, { gesture: gesture }, /*#__PURE__*/_react.default.createElement(_GestureHandlerButton.default, { ref: pressableRef, testID: props.testID, hitSlop: appliedHitSlop, enabled: isPressableEnabled, touchSoundDisabled: (_props$android_disabl = props.android_disableSound) !== null && _props$android_disabl !== void 0 ? _props$android_disabl : undefined, rippleColor: (0, _reactNative.processColor)((_props$android_ripple = (_props$android_ripple2 = props.android_ripple) === null || _props$android_ripple2 === void 0 ? void 0 : _props$android_ripple2.color) !== null && _props$android_ripple !== void 0 ? _props$android_ripple : defaultRippleColor), rippleRadius: (_props$android_ripple3 = (_props$android_ripple4 = props.android_ripple) === null || _props$android_ripple4 === void 0 ? void 0 : _props$android_ripple4.radius) !== null && _props$android_ripple3 !== void 0 ? _props$android_ripple3 : undefined, style: [_reactNative.StyleSheet.absoluteFill, pointerStyle, innerStyles] }, childrenProp, __DEV__ ? /*#__PURE__*/_react.default.createElement(_PressabilityDebugView.PressabilityDebugView, { color: "red", hitSlop: normalizedHitSlop }) : null))); } //# sourceMappingURL=Pressable.js.map