From 59e2f957673c76a6f5ccfd3fb1fce4ba70d4f987 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 16:59:43 +0200 Subject: [PATCH] Drag-reorder works on touch via svelte-dnd-action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTML5 native drag-and-drop doesn't fire on touchscreens — mobile users couldn't reorder the list at all. Swapped the manual DnD wiring (dragstart/dragover/drop) for svelte-dnd-action, which uses Pointer Events and handles mouse, touch, AND keyboard reorder uniformly. Linear-quality reorder UX for ~11 KB gzipped. Replacement details: - bun add svelte-dnd-action (0.9.69) - Home.svelte: ~70 lines of manual handler code deleted, replaced with ~30 lines wiring up `use:dndzone` + `onconsider` + `onfinalize`. The midpoint-position math for sort_position is unchanged (finalize gives us the new neighbour list directly). - ActivityRow.svelte: the drag handle's `onpointerdown` flips a parent-owned dragDisabled flag to false — the library then takes over. Standard "handle-only drag" recipe; clicks on the title/buttons inside the card don't initiate drag because dragDisabled stays true everywhere else. - dndItems is a buffer copy of `filtered` that the library mutates during a drag. An $effect re-syncs it from `filtered` between drags (so new activities still float to the top, etc). - Shadow item (the library's placeholder while dragging) is rendered at 30% opacity so the drop target is visible without flashing. Accessibility wins for free: - Keyboard reorder: focus an item, press Space to "pick up", arrow keys to move, Space to drop. Screen readers get polite-live announcements of each move from the library. - Touch reorder works on iOS Safari and Android Chrome. 96 tests still pass; typecheck clean; build ok. Bundle: 122 KB → 154 KB (gzipped 42 → 53 KB, ~+11 KB). --- bun.lock | 3 + frontend/src/components/ActivityRow.svelte | 22 ++- frontend/src/components/Home.svelte | 155 ++++++++++----------- package.json | 3 +- 4 files changed, 90 insertions(+), 93 deletions(-) diff --git a/bun.lock b/bun.lock index d0fb04e..9a378db 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "hono": "^4.6.0", "libsodium-wrappers-sumo": "^0.7.15", + "svelte-dnd-action": "^0.9.69", }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", @@ -219,6 +220,8 @@ "svelte-check": ["svelte-check@4.4.8", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w=="], + "svelte-dnd-action": ["svelte-dnd-action@0.9.69", "", { "peerDependencies": { "svelte": ">=3.23.0 || ^5.0.0-next.0" } }, "sha512-NAmSOH7htJoYraTQvr+q5whlIuVoq88vEuHr4NcFgscDRUxfWPPxgie2OoxepBCQCikrXZV4pqV86aun60wVyw=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte index b103cdc..480fda5 100644 --- a/frontend/src/components/ActivityRow.svelte +++ b/frontend/src/components/ActivityRow.svelte @@ -18,17 +18,16 @@ /** Called when the row updates its own activity (e.g. after a heart * toggle). The parent should patch its list so the change persists. */ onChanged?: (a: Activity) => void; - /** Whether to render a drag handle that initiates HTML5 DnD on this row. - * Parent owns the actual dragstart/dragend wiring — the handle just - * forwards events upward. */ + /** Render a drag handle on this row. The parent owns the actual drag + * wiring via svelte-dnd-action; the handle's pointerdown/touchstart + * just flips the parent's dragDisabled to false (one drag at a time). */ draggable?: boolean; - onDragHandleStart?: (e: DragEvent) => void; - onDragHandleEnd?: () => void; + onDragHandleStart?: () => void; } let { activity, privateCleartext = null, onDeleted, onEdit, onChanged, - draggable = false, onDragHandleStart, onDragHandleEnd, + draggable = false, onDragHandleStart, }: Props = $props(); // Fallback decrypt for the case where Home hasn't pre-computed yet (e.g. @@ -205,15 +204,14 @@
{#if draggable} - + diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte index 2882101..d9ce8a0 100644 --- a/frontend/src/components/Home.svelte +++ b/frontend/src/components/Home.svelte @@ -1,5 +1,6 @@