diff --git a/apps/web/src/components/swipeable-row.tsx b/apps/web/src/components/swipeable-row.tsx index cebe10d..076f2b7 100644 --- a/apps/web/src/components/swipeable-row.tsx +++ b/apps/web/src/components/swipeable-row.tsx @@ -67,6 +67,11 @@ export function SwipeableRow({ const [dragging, setDragging] = useState(false); const containerRef = useRef(null); const dragStart = useRef<{ x: number; baseOffset: number } | null>(null); + // Tracks whether the pointer crossed the click-vs-drag threshold during + // the current gesture. If it did, we swallow the synthetic click that + // browsers fire on pointerup — otherwise a swipe on a Link-wrapped row + // both swipes the shelf open AND navigates to the link target. + const dragMoved = useRef(false); // Close the shelf when the user taps anywhere outside an open row. useEffect(() => { @@ -92,12 +97,17 @@ export function SwipeableRow({ function handlePointerDown(e: React.PointerEvent) { if (e.button !== 0 && e.pointerType === "mouse") return; dragStart.current = { x: e.clientX, baseOffset: offset }; + dragMoved.current = false; setDragging(true); } function handlePointerMove(e: React.PointerEvent) { if (!dragging || !dragStart.current) return; const dx = e.clientX - dragStart.current.x; + // 6 px is the standard threshold below which a touch counts as a tap + // rather than a drag. Cross it once and the gesture commits to drag + // for the rest of the pointer's lifetime. + if (Math.abs(dx) > 6) dragMoved.current = true; setOffset(clamp(dragStart.current.baseOffset + dx)); } @@ -113,6 +123,28 @@ export function SwipeableRow({ rightWidth, }), ); + if (dragMoved.current) { + // The browser fires a synthetic `click` on the element under the + // pointer right after pointerup. If our row body wraps a , + // that click navigates away. Add a one-shot capture-phase handler + // that swallows the next click ANYWHERE in the row container + // before it can reach the anchor's onClick. + const swallow = (ev: Event) => { + ev.preventDefault(); + ev.stopPropagation(); + }; + const node = containerRef.current; + if (node) { + node.addEventListener("click", swallow, { capture: true, once: true }); + // Defensive: if for some reason no click fires (e.g. pointerup + // outside the element), strip the listener after a tick so it + // doesn't accidentally eat a future legitimate click. + window.setTimeout(() => { + node.removeEventListener("click", swallow, { capture: true }); + }, 350); + } + } + dragMoved.current = false; } return (