52 lines
1.7 KiB
TypeScript
52 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* useTapGuard — tells a genuine tap apart from a swipe on a natively-scrolling
|
|
* strip (CSS scroll-snap carousels).
|
|
*
|
|
* Browsers still fire a `click` after some swipe gestures, so a plain onClick
|
|
* on a slide would trigger mid-swipe. Snapshot the pointer position AND the
|
|
* scroller's offset on pointerdown, then check both when the click arrives:
|
|
*
|
|
* • pointer travel — catches a drag that didn't scroll (e.g. hit the end)
|
|
* • scroll movement — catches an inertial swipe, where the pointer barely
|
|
* moves before `click` fires but the strip is already gliding
|
|
*
|
|
* Either signal alone misses cases, hence both. No staleness risk: a click is
|
|
* always immediately preceded by a pointerdown on the same element.
|
|
*/
|
|
|
|
import { useRef, type RefObject } from "react";
|
|
|
|
/** Pointer travel (px) still considered a tap rather than a drag. */
|
|
const MOVE_TOLERANCE = 8;
|
|
/** Scroll movement (px) still considered "didn't scroll". */
|
|
const SCROLL_TOLERANCE = 4;
|
|
|
|
type PointerLike = { clientX: number; clientY: number };
|
|
|
|
export function useTapGuard(scrollerRef: RefObject<HTMLElement | null>) {
|
|
const start = useRef({ x: 0, y: 0, scrollLeft: 0 });
|
|
|
|
function onPointerDown(e: PointerLike) {
|
|
start.current = {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
scrollLeft: scrollerRef.current?.scrollLeft ?? 0,
|
|
};
|
|
}
|
|
|
|
function wasTap(e: PointerLike) {
|
|
const moved =
|
|
Math.abs(e.clientX - start.current.x) > MOVE_TOLERANCE ||
|
|
Math.abs(e.clientY - start.current.y) > MOVE_TOLERANCE;
|
|
const scrolled =
|
|
Math.abs(
|
|
(scrollerRef.current?.scrollLeft ?? 0) - start.current.scrollLeft,
|
|
) > SCROLL_TOLERANCE;
|
|
return !moved && !scrolled;
|
|
}
|
|
|
|
return { onPointerDown, wasTap };
|
|
}
|