@thibault.sh/hooks
Version:
A comprehensive collection of React hooks for browser storage, UI interactions, and more
1 lines • 9.51 kB
Source Map (JSON)
{"version":3,"sources":["../src/hooks/useLongPress.ts"],"names":["useLongPress","options","delay","preventContext","onPress","onLongPress","onLongPressCanceled","timeout","useRef","startTime","frameRef","pressState","setPressState","useState","updateProgress","useCallback","elapsed","newProgress","prev","__spreadProps","__spreadValues","useEffect","start","event","end","cancel"],"mappings":"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","file":"useLongPress.mjs","sourcesContent":["import { useCallback, useRef, useState, useEffect } from \"react\";\n\n/**\n * Configuration options for the useLongPress hook\n */\ninterface LongPressOptions {\n /** Duration in milliseconds before long press is triggered (default: 400) */\n delay?: number;\n /** Whether to disable context menu on long press (default: true) */\n preventContext?: boolean;\n /** Callback fired when a normal press (shorter than delay) is completed */\n onPress?: () => void;\n /** Callback fired when a long press is successfully triggered */\n onLongPress?: () => void;\n /** Callback fired when a long press is canceled before completion */\n onLongPressCanceled?: () => void;\n}\n\n/**\n * Internal state for tracking press status and progress\n */\ninterface PressState {\n /** Whether the element is currently being pressed */\n isPressed: boolean;\n /** Whether the press has exceeded the long press threshold */\n isLongPressed: boolean;\n /** Progress towards completing a long press (0 to 1) */\n progress: number;\n}\n\n/**\n * A hook that handles both normal press and long press interactions\n *\n * @param options - Configuration options for the long press behavior\n * @param options.delay - Duration in milliseconds before a press is considered a long press (default: 400)\n * @param options.preventContext - When true, prevents the default context menu on long press (default: true)\n * @param options.onPress - Callback fired when a normal press (shorter than delay) is completed.\n * Triggers only if the press duration was less than the specified delay\n * @param options.onLongPress - Callback fired when a long press is successfully triggered.\n * Triggers exactly once when the press duration exceeds the delay\n * @param options.onLongPressCanceled - Callback fired when a long press is canceled before completion.\n * Triggers if the press is released or canceled before reaching the delay threshold\n * @returns Object containing event handlers and current press state\n *\n * @example\n * ```tsx\n * const MyComponent = () => {\n * const { handlers, state } = useLongPress({\n * delay: 1000,\n * onPress: () => console.log('Normal press'),\n * onLongPress: () => console.log('Long press completed'),\n * onLongPressCanceled: () => console.log('Long press canceled'),\n * });\n *\n * return (\n * <button {...handlers}>\n * {state.isLongPressed\n * ? 'Long Press!'\n * : `Hold me (${Math.round(state.progress * 100)}%)`}\n * </button>\n * );\n * };\n * ```\n */\nexport function useLongPress(options: LongPressOptions = {}) {\n const { delay = 400, preventContext = true, onPress, onLongPress, onLongPressCanceled } = options;\n\n const timeout = useRef<ReturnType<typeof setTimeout>>();\n const startTime = useRef<number>(0);\n const frameRef = useRef<number>();\n\n const [pressState, setPressState] = useState<PressState>({\n isPressed: false,\n isLongPressed: false,\n progress: 0,\n });\n\n /**\n * Updates the progress of the long press animation\n * Uses requestAnimationFrame for smooth updates\n */\n const updateProgress = useCallback(() => {\n if (!startTime.current || !pressState.isPressed) return;\n\n const elapsed = Date.now() - startTime.current;\n const newProgress = Math.min(elapsed / delay, 1);\n\n setPressState((prev) => ({ ...prev, progress: newProgress }));\n\n if (newProgress < 1) {\n frameRef.current = requestAnimationFrame(updateProgress);\n }\n }, [delay, pressState.isPressed]);\n\n // Cleanup animation frames and timeouts on unmount\n useEffect(() => {\n return () => {\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n if (timeout.current) clearTimeout(timeout.current);\n };\n }, []);\n\n /**\n * Handles the start of a press interaction\n * Initializes timers and starts progress tracking\n */\n const start = useCallback(\n (event: React.TouchEvent | React.MouseEvent) => {\n if (preventContext) {\n event.preventDefault();\n }\n\n startTime.current = Date.now();\n setPressState({ isPressed: true, isLongPressed: false, progress: 0 });\n frameRef.current = requestAnimationFrame(updateProgress);\n\n timeout.current = setTimeout(() => {\n setPressState((prev) => ({ ...prev, isLongPressed: true }));\n onLongPress?.();\n }, delay);\n },\n [delay, onLongPress, preventContext, updateProgress]\n );\n\n /**\n * Handles the end of a press interaction\n * Determines if it was a long press and fires appropriate callbacks\n */\n const end = useCallback(\n (event: React.TouchEvent | React.MouseEvent) => {\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n if (timeout.current) clearTimeout(timeout.current);\n\n const wasLongPress = pressState.isLongPressed;\n\n if (!wasLongPress) {\n if (pressState.progress < 1) {\n onLongPressCanceled?.();\n }\n onPress?.();\n }\n\n setPressState({ isPressed: false, isLongPressed: false, progress: 0 });\n },\n [onLongPressCanceled, onPress, pressState.isLongPressed, pressState.progress]\n );\n\n /**\n * Handles cancellation of a press interaction\n * (e.g., pointer leave or touch cancel events)\n */\n const cancel = useCallback(\n (event: React.TouchEvent | React.MouseEvent) => {\n if (frameRef.current) cancelAnimationFrame(frameRef.current);\n if (timeout.current) clearTimeout(timeout.current);\n\n if (pressState.isPressed && !pressState.isLongPressed) {\n onLongPressCanceled?.();\n }\n\n setPressState({ isPressed: false, isLongPressed: false, progress: 0 });\n },\n [onLongPressCanceled, pressState.isPressed, pressState.isLongPressed]\n );\n\n return {\n /** Event handlers to attach to the target element */\n handlers: {\n onMouseDown: start,\n onMouseUp: end,\n onMouseLeave: cancel,\n onTouchStart: start,\n onTouchEnd: end,\n onTouchCancel: cancel,\n },\n /** Current state of the press interaction */\n state: {\n isPressed: pressState.isPressed,\n isLongPressed: pressState.isLongPressed,\n progress: pressState.progress,\n },\n };\n}\n"]}