react-native-gesture-handler
Version:
Experimental implementation of a new declarative API for gesture handling in react-native
309 lines (252 loc) • 13.5 kB
JavaScript
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { GestureObjects as Gesture } from '../../handlers/gestures/gestureObjects';
import { GestureDetector } from '../../handlers/gestures/GestureDetector';
import { Platform, View, processColor, StyleSheet } from 'react-native';
import NativeButton from '../GestureHandlerButton';
import { numberAsInset, gestureToPressableEvent, isTouchWithinInset, gestureTouchToPressableEvent, addInsets, splitStyles } from './utils';
import { PressabilityDebugView } from '../../handlers/PressabilityDebugView';
const DEFAULT_LONG_PRESS_DURATION = 500;
export default 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] = useState((_props$testOnly_press = props.testOnly_pressed) !== null && _props$testOnly_press !== void 0 ? _props$testOnly_press : false);
const pressableRef = useRef(null); // Disabled when onLongPress has been called
const isPressCallbackEnabled = useRef(true);
const hasPassedBoundsChecks = useRef(false);
const shouldPreventNativeEffects = useRef(false);
const normalizedHitSlop = useMemo(() => {
var _props$hitSlop;
return typeof props.hitSlop === 'number' ? numberAsInset(props.hitSlop) : (_props$hitSlop = props.hitSlop) !== null && _props$hitSlop !== void 0 ? _props$hitSlop : {};
}, [props.hitSlop]);
const normalizedPressRetentionOffset = useMemo(() => {
var _props$pressRetention;
return typeof props.pressRetentionOffset === 'number' ? numberAsInset(props.pressRetentionOffset) : (_props$pressRetention = props.pressRetentionOffset) !== null && _props$pressRetention !== void 0 ? _props$pressRetention : {};
}, [props.pressRetentionOffset]);
const hoverInTimeout = useRef(null);
const hoverOutTimeout = useRef(null);
const hoverGesture = useMemo(() => Gesture.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, gestureToPressableEvent(event));
}, props.delayHoverIn);
return;
}
(_props$onHoverIn2 = props.onHoverIn) === null || _props$onHoverIn2 === void 0 ? void 0 : _props$onHoverIn2.call(props, 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, gestureToPressableEvent(event));
}, props.delayHoverOut);
return;
}
(_props$onHoverOut2 = props.onHoverOut) === null || _props$onHoverOut2 === void 0 ? void 0 : _props$onHoverOut2.call(props, gestureToPressableEvent(event));
}), [props]);
const pressDelayTimeoutRef = useRef(null);
const isTouchPropagationAllowed = useRef(false); // iOS only: due to varying flow of gestures, events sometimes have to be saved for later use
const deferredEventPayload = useRef(null);
const pressInHandler = 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 = 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 = useRef(false);
const onEndHandlingTouchesDown = useRef(null);
const cancelledMidPress = useRef(false);
const activateLongPress = 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, gestureTouchToPressableEvent(event));
isPressCallbackEnabled.current = false;
}
if (longPressTimeoutRef.current) {
clearTimeout(longPressTimeoutRef.current);
longPressTimeoutRef.current = null;
}
}, [props]);
const longPressTimeoutRef = 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 = useMemo(() => Gesture.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 (!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(gestureTouchToPressableEvent(event));
}, props.unstable_pressDelay);
} else {
pressInHandler(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(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(gestureTouchToPressableEvent(event));
}).onTouchesCancelled(event => {
isPressCallbackEnabled.current = false;
if (handlingOnTouchesDown.current) {
cancelledMidPress.current = true;
onEndHandlingTouchesDown.current = () => pressOutHandler(gestureTouchToPressableEvent(event));
return;
}
if (!hasPassedBoundsChecks.current || event.allTouches.length > event.changedTouches.length) {
return;
}
pressOutHandler(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 = useMemo(() => Gesture.Native().onBegin(() => {
// Android sets BEGAN state on press down
if (Platform.OS === 'android') {
isTouchPropagationAllowed.current = true;
}
}).onStart(() => {
if (Platform.OS === 'web') {
isTouchPropagationAllowed.current = true;
} // iOS sets ACTIVE state on press down
if (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 = 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 (Platform.OS !== 'web') {
gesture.shouldCancelWhenOutside(true);
}
} // Uses different hitSlop, to activate on hitSlop area instead of pressRetentionOffset area
buttonGesture.hitSlop(normalizedHitSlop);
const gesture = Gesture.Simultaneous(...gestures);
const defaultRippleColor = props.android_ripple ? undefined : 'transparent'; // `cursor: 'pointer'` on `RNButton` crashes iOS
const pointerStyle = 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 = StyleSheet.flatten(styleProp !== null && styleProp !== void 0 ? styleProp : {});
const [innerStyles, outerStyles] = splitStyles(flattenedStyles);
return /*#__PURE__*/React.createElement(View, {
style: outerStyles
}, /*#__PURE__*/React.createElement(GestureDetector, {
gesture: gesture
}, /*#__PURE__*/React.createElement(NativeButton, {
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: 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: [StyleSheet.absoluteFill, pointerStyle, innerStyles]
}, childrenProp, __DEV__ ? /*#__PURE__*/React.createElement(PressabilityDebugView, {
color: "red",
hitSlop: normalizedHitSlop
}) : null)));
}
//# sourceMappingURL=Pressable.js.map