Files
boocode/apps/web/src/hooks/useLongPress.ts

75 lines
2.1 KiB
TypeScript

import { useCallback, useRef, type TouchEvent } from "react";
interface LongPressHandlers {
onTouchStart: (e: TouchEvent) => void;
onTouchMove: (e: TouchEvent) => void;
onTouchEnd: (e: TouchEvent) => void;
onTouchCancel: (e: TouchEvent) => void;
}
interface Options {
ms?: number;
// Suppress the synthetic click that follows touchend when long-press fired.
suppressClickOnFire?: boolean;
}
// Hand-rolled long-press detector. Starts a timer on touchstart; cancels on
// touchmove or early touchend; fires the callback on timer expiry. Caller is
// expected to suppress text-selection callout via CSS (-webkit-touch-callout).
export function useLongPress(
callback: (touch: { clientX: number; clientY: number; target: EventTarget | null }) => void,
{ ms = 500, suppressClickOnFire = true }: Options = {},
): LongPressHandlers {
const timerRef = useRef<number | null>(null);
const firedRef = useRef(false);
const clear = useCallback(() => {
if (timerRef.current !== null) {
window.clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const onTouchStart = useCallback(
(e: TouchEvent) => {
firedRef.current = false;
const touch = e.touches[0];
if (!touch) return;
const x = touch.clientX;
const y = touch.clientY;
const target = e.target;
clear();
timerRef.current = window.setTimeout(() => {
firedRef.current = true;
callback({ clientX: x, clientY: y, target });
}, ms);
},
[callback, ms, clear],
);
const onTouchMove = useCallback(() => {
clear();
}, [clear]);
const onTouchEnd = useCallback(
(e: TouchEvent) => {
clear();
if (firedRef.current && suppressClickOnFire) {
// Block the synthetic click that follows touchend; the long-press
// already handled the gesture.
e.preventDefault();
}
},
[clear, suppressClickOnFire],
);
const onTouchCancel = useCallback(
(_e: TouchEvent) => {
clear();
},
[clear],
);
return { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel };
}