import { useCallback, useRef } from 'react'; import { useMediaQuery } from './useMediaQuery'; const LONG_PRESS_MS = 450; const MOVE_TOLERANCE_PX = 10; /** * [Gitea #166] Long-press detection for touch screens. Fires `onLongPress` * with the finger's viewport position after 450 ms of holding still (< 10 px * of movement); a scroll or a lift cancels it. Android also fires * `contextmenu` for a long-press — the returned `suppressContextMenu` ref is * true briefly after we fired so the caller can drop that duplicate. Handlers * are no-ops when the device has no coarse pointer. */ export function useLongPress(onLongPress: (x: number, y: number) => void) { const coarse = useMediaQuery('(pointer: coarse)'); const timer = useRef(undefined); const start = useRef<{ x: number; y: number } | null>(null); const fired = useRef(false); const clear = useCallback(() => { if (timer.current !== undefined) window.clearTimeout(timer.current); timer.current = undefined; start.current = null; }, []); const onTouchStart = useCallback( (evt: React.TouchEvent) => { if (!coarse || evt.touches.length !== 1) return; const t = evt.touches[0]; start.current = { x: t.clientX, y: t.clientY }; fired.current = false; timer.current = window.setTimeout(() => { const s = start.current; clear(); if (!s) return; fired.current = true; window.setTimeout(() => { fired.current = false; }, 1000); onLongPress(s.x, s.y); }, LONG_PRESS_MS); }, [coarse, clear, onLongPress], ); const onTouchMove = useCallback( (evt: React.TouchEvent) => { const s = start.current; if (!s) return; const t = evt.touches[0]; if ( !t || Math.abs(t.clientX - s.x) > MOVE_TOLERANCE_PX || Math.abs(t.clientY - s.y) > MOVE_TOLERANCE_PX ) { clear(); } }, [clear], ); // Lifting the finger after a long-press would synthesise mousedown/click at // that point — on whatever the sheet just put there. Swallow it. const onTouchEnd = useCallback( (evt: React.TouchEvent) => { if (fired.current && evt.cancelable) evt.preventDefault(); clear(); }, [clear], ); return { coarse, handlers: coarse ? { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: clear } : {}, /** True for ~1 s after a long-press fired (drop the Android contextmenu echo). */ suppressContextMenu: fired, }; }