react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
178 lines (172 loc) • 6.89 kB
JavaScript
;
import React, { use, useCallback, useMemo, useRef, useState } from 'react';
import { Platform } from 'react-native';
import GestureHandlerButton from '../../../components/GestureHandlerButton';
import { getTVProps } from '../../../components/utils';
import { getNextHandlerTag } from '../../../handlers/getNextHandlerTag';
import { isKeyboardDismissingTap, JSResponderContext, updateResponderEventValue } from '../../scrollViewInterop';
import { jsx as _jsx } from "react/jsx-runtime";
const isAndroid = Platform.OS === 'android';
const TRANSPARENT_RIPPLE = {
rippleColor: 'transparent'
};
const DEFAULT_IN_DURATION_MS = 50;
const DEFAULT_OUT_DURATION_MS = 100;
// Clamp user-supplied durations to finite, non-negative milliseconds.
// Negative, NaN, or Infinity values would produce invalid CSS transitions
// on web and negative setTimeout delays in branch 3 of the press-out path.
function sanitizeDuration(value) {
return Number.isFinite(value) && value >= 0 ? value : 0;
}
function resolveAnimationDuration(value) {
if (value === undefined) {
return {
tapAnimationInDuration: DEFAULT_IN_DURATION_MS,
tapAnimationOutDuration: DEFAULT_OUT_DURATION_MS,
longPressAnimationOutDuration: DEFAULT_OUT_DURATION_MS,
hoverAnimationInDuration: DEFAULT_IN_DURATION_MS,
hoverAnimationOutDuration: DEFAULT_OUT_DURATION_MS
};
}
if (typeof value === 'number') {
const sanitized = sanitizeDuration(value);
return {
tapAnimationInDuration: sanitized,
tapAnimationOutDuration: sanitized,
longPressAnimationOutDuration: sanitized,
hoverAnimationInDuration: sanitized,
hoverAnimationOutDuration: sanitized
};
}
// The union guarantees variant 2 supplies top-level `in`/`out`, variant 3
// supplies both category objects — so per-category fallback to base is
// always defined for well-typed input; the 0 fallbacks here are unreachable.
const baseIn = 'in' in value ? value.in : 0;
const baseOut = 'out' in value ? value.out : 0;
const tapOut = value.tap?.out ?? baseOut;
return {
tapAnimationInDuration: sanitizeDuration(value.tap?.in ?? baseIn),
tapAnimationOutDuration: sanitizeDuration(tapOut),
longPressAnimationOutDuration: sanitizeDuration(value.longPress?.out ?? tapOut),
hoverAnimationInDuration: sanitizeDuration(value.hover?.in ?? baseIn),
hoverAnimationOutDuration: sanitizeDuration(value.hover?.out ?? baseOut)
};
}
export const Touchable = props => {
const {
underlayColor = 'transparent',
defaultUnderlayOpacity = 0,
activeUnderlayOpacity = 0.105,
defaultOpacity = 1,
animationDuration,
androidRipple,
delayLongPress = 600,
onLongPress,
onPress,
onPressIn,
onPressOut,
onHoverIn,
onHoverOut,
children,
disabled = false,
cancelOnLeave = true,
ref,
...rest
} = props;
const [handlerTag] = useState(() => getNextHandlerTag());
const resolvedDurations = resolveAnimationDuration(animationDuration);
const resolvedDelayLongPress = sanitizeDuration(delayLongPress);
const shouldUseNativeRipple = isAndroid && androidRipple !== undefined;
const jsResponderContext = use(JSResponderContext);
const dropKeyboardTapRef = useRef(null);
// The button handles presses natively, without a NativeDetector, so it has
// to mark its responder events as RNGH-handled itself — otherwise a tap on
// it looks unhandled to an enclosing ScrollView in
// keyboardShouldPersistTaps='handled' mode and dismisses the keyboard.
const handleStartShouldSetResponderCapture = useCallback(() => {
if (!disabled) {
updateResponderEventValue(jsResponderContext, true);
}
return false;
}, [disabled, jsResponderContext]);
const internalOnPress = useCallback(e => {
if (dropKeyboardTapRef.current) {
return;
}
onPress?.(e.nativeEvent);
}, [onPress]);
const internalOnPressIn = useCallback(e => {
// PressIn opens every press sequence; capture the verdict once per
// sequence so a re-entry PressIn (with cancelOnLeave={false}) doesn't
// overwrite it after the keyboard is already dismissed.
dropKeyboardTapRef.current ??= isKeyboardDismissingTap(jsResponderContext);
if (dropKeyboardTapRef.current) {
return;
}
onPressIn?.(e.nativeEvent);
}, [jsResponderContext, onPressIn]);
const internalOnPressOut = useCallback(e => {
if (dropKeyboardTapRef.current) {
return;
}
onPressOut?.(e.nativeEvent);
}, [onPressOut]);
const internalOnLongPress = useCallback(e => {
if (dropKeyboardTapRef.current) {
return;
}
onLongPress?.(e.nativeEvent);
}, [onLongPress]);
// Left undefined when the corresponding prop is absent so web can skip
// building a hover payload nobody consumes — it costs a synchronous layout
// read per pointer enter/leave. The native platforms emit either way.
const internalOnHoverIn = useMemo(() => onHoverIn ? e => onHoverIn(e.nativeEvent) : undefined, [onHoverIn]);
const internalOnHoverOut = useMemo(() => onHoverOut ? e => onHoverOut(e.nativeEvent) : undefined, [onHoverOut]);
// InteractionFinished is dispatched after the terminal PressOut/Press
// events, so resetting synchronously here is safe.
const internalOnInteractionFinished = useCallback(() => {
dropKeyboardTapRef.current = null;
}, []);
const rippleProps = shouldUseNativeRipple ? {
rippleColor: androidRipple?.color,
rippleRadius: androidRipple?.radius,
borderless: androidRipple?.borderless,
foreground: androidRipple?.foreground
} : TRANSPARENT_RIPPLE;
const tvProps = getTVProps(rest);
const hitSlop = typeof props.hitSlop === 'number' ? {
top: props.hitSlop,
left: props.hitSlop,
bottom: props.hitSlop,
right: props.hitSlop
} : props.hitSlop;
return /*#__PURE__*/_jsx(GestureHandlerButton, {
...rest,
...tvProps,
...rippleProps,
...resolvedDurations,
ref: ref ?? null,
enabled: !disabled,
onStartShouldSetResponderCapture: handleStartShouldSetResponderCapture,
moduleId: globalThis._RNGH_MODULE_ID,
handlerTag: handlerTag,
cancelOnLeave: cancelOnLeave,
gestureTestID: props.testID,
gestureHitSlop: hitSlop,
defaultOpacity: defaultOpacity,
defaultUnderlayOpacity: defaultUnderlayOpacity,
activeUnderlayOpacity: activeUnderlayOpacity,
underlayColor: underlayColor,
longPressDuration: resolvedDelayLongPress,
hasLongPressHandler: onLongPress !== undefined,
onButtonPress: internalOnPress,
onButtonPressIn: internalOnPressIn,
onButtonPressOut: internalOnPressOut,
onButtonLongPress: internalOnLongPress,
onButtonHoverIn: internalOnHoverIn,
onButtonHoverOut: internalOnHoverOut,
onButtonInteractionFinished: internalOnInteractionFinished,
children: children
});
};
//# sourceMappingURL=Touchable.js.map