Files
fusion/packages/dashboard/app/hooks/useCoarsePointer.ts
gsxdsm 0d75725a28 fix: reliable mobile board horizontal swipe over task cards
Swiping across the board to scroll horizontally was intermittent when the
gesture started on a task card. Cards render with native HTML5 `draggable`
for desktop drag-to-move, but native DnD does not function via touch — the
attribute only arms the browser's touch-drag heuristic, which non-
deterministically hijacks horizontal swipes meant to pan the board.

Disable native drag on touch-primary devices (`(hover: none) and
(pointer: coarse)`) via a new `useCoarsePointer` hook, folded into the
card's `isDraggable`. Mouse/desktop and hybrid laptops keep drag-to-move;
touch loses nothing (DnD never worked there) and panning is now reliable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:40:15 -07:00

33 lines
1.3 KiB
TypeScript

import { useEffect, useState } from "react";
// Touch-primary devices (phones/tablets, Android WebViews). A hybrid laptop
// with a trackpad/mouse reports `(hover: hover)` and is intentionally excluded
// so mouse drag-to-move keeps working there.
export const COARSE_POINTER_MEDIA_QUERY = "(hover: none) and (pointer: coarse)";
export function isCoarsePointer(): boolean {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
return window.matchMedia(COARSE_POINTER_MEDIA_QUERY).matches;
}
// Whether the primary pointer is coarse (touch). Native HTML5 drag-and-drop is
// non-functional via touch, yet a `draggable` element still arms the browser's
// touch-drag heuristic and can hijack a horizontal swipe meant to scroll the
// board. Components use this to drop `draggable` on touch so panning is reliable.
export function useCoarsePointer(): boolean {
const [coarse, setCoarse] = useState<boolean>(isCoarsePointer);
useEffect(() => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
const query = window.matchMedia(COARSE_POINTER_MEDIA_QUERY);
const update = () => setCoarse(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return coarse;
}