From 0d75725a2858e67c4929026708ffeb83fba2bcfe Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 13 Jun 2026 19:34:22 -0700 Subject: [PATCH] fix: reliable mobile board horizontal swipe over task cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .changeset/fix-mobile-card-swipe-scroll.md | 5 +++ .../dashboard/app/components/TaskCard.tsx | 8 ++++- .../components/__tests__/TaskCard.test.tsx | 27 ++++++++++++++++ .../dashboard/app/hooks/useCoarsePointer.ts | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-mobile-card-swipe-scroll.md create mode 100644 packages/dashboard/app/hooks/useCoarsePointer.ts diff --git a/.changeset/fix-mobile-card-swipe-scroll.md b/.changeset/fix-mobile-card-swipe-scroll.md new file mode 100644 index 0000000000..7295ef65e6 --- /dev/null +++ b/.changeset/fix-mobile-card-swipe-scroll.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fixed unreliable horizontal scrolling when swiping across task cards on the mobile board. Native HTML5 drag is now disabled on touch-primary devices (where it never worked anyway), so the browser no longer hijacks swipe-to-scroll gestures that start on a card. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index b45013dea9..b90fafcd2d 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -18,6 +18,7 @@ import { PrCreateModal } from "./PrCreateModal"; import { ProviderIcon } from "./ProviderIcon"; import { PluginSlot } from "./PluginSlot"; import { useBadgeWebSocket } from "../hooks/useBadgeWebSocket"; +import { useCoarsePointer } from "../hooks/useCoarsePointer"; import { getFreshBatchData } from "../hooks/useBatchBadgeFetch"; import { useTaskDiffStats } from "../hooks/useTaskDiffStats"; import { useAgentsMapCache } from "../hooks/useAgentsMapCache"; @@ -983,7 +984,12 @@ function TaskCardComponent({ const isAwaitingInput = task.status === "awaiting-user-input"; const isArchived = task.column === "archived"; const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && !isStuck && !isAwaitingApproval && !isAwaitingInput && (task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string)); - const isDraggable = !disableDrag && !queued && !isPaused && !isEditing && !isArchived; // Disable drag during edit/archived or host embedding + // Native HTML5 drag is desktop-mouse only — it doesn't move cards via touch. + // On touch-primary devices the `draggable` attribute still arms the browser's + // touch-drag heuristic, which intermittently hijacks horizontal swipes meant + // to scroll the board. Drop drag on coarse pointers so panning stays reliable. + const isCoarsePointer = useCoarsePointer(); + const isDraggable = !disableDrag && !queued && !isPaused && !isEditing && !isArchived && !isCoarsePointer; // Disable drag during edit/archived, host embedding, or touch // Check if this card can be edited inline const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask; diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 75e346be9b..6f39cd31f4 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -607,6 +607,33 @@ describe("TaskCard", () => { expect(card.getAttribute("draggable")).toBe("false"); }); + // FN-6389 follow-up: native HTML5 drag is desktop-mouse only and doesn't move + // cards via touch, but a `draggable` element still arms the browser's touch-drag + // heuristic, which intermittently hijacks horizontal swipes meant to scroll the + // mobile board. On touch-primary (coarse pointer) devices we drop `draggable`. + it("disables native card dragging on touch-primary (coarse pointer) devices", () => { + const original = window.matchMedia; + window.matchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: query === "(hover: none) and (pointer: coarse)", + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })) as unknown as typeof window.matchMedia; + try { + const { container } = render(); + const card = container.querySelector(".card") as HTMLElement; + expect(card.getAttribute("draggable")).toBe("false"); + // No drag-start handler should be wired on touch (would arm the heuristic). + const dragStart = new Event("dragstart", { bubbles: true, cancelable: true }); + const prevented = !card.dispatchEvent(dragStart); + expect(prevented).toBe(false); + } finally { + window.matchMedia = original; + } + }); + it("renders Nx PR badge label when multiple PRs are linked", () => { render( (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; +}