fix(dnd): single-gesture drag + working keyboard, drop the handle
Two bugs in the previous DnD wiring, both stemming from the
handle-gates-dragDisabled pattern:
1. **Two-step desktop drag.** The handle's pointerdown flipped
dragDisabled to false, but by then the gesture was already in
progress and the library wasn't watching. Users had to click
the handle, release, then mousedown+drag the card — two
gestures for one action.
2. **Keyboard reorder didn't work.** svelte-dnd-action's Space-to-
lift + arrow-keys-to-move handling is gated by dragDisabled. By
keeping it true except during a handle-press, the library never
processed keyboard interactions.
Fix: dragDisabled becomes a pure function of auth state. The
library's built-in distance threshold prevents accidental drags
from clicks; form controls and links inside cards aren't draggable
targets. Result:
- Desktop: mousedown on the card, move → drag starts.
- Touch: tap-and-drag on the card → drag starts (after threshold).
- Keyboard: Tab to a row, Space to lift, arrows to move, Space
to drop. Screen-reader announcements come from the library.
Plus: the drag-handle ⋮⋮ icon is gone entirely. The library
listens on the whole card, so the handle was only ever a visual
hint. `cursor: grab` on the card carries that affordance for the
~5px of weight the handle was eating.
Net diff: −12 / +18, including a small CSS adjustment to scope
`cursor: grab` to dndzone children only (so the same card style
on permalink / public-list / tag pages stays with default cursor).
Also: the public landing ("/") now sorts strictly by created_at
DESC regardless of viewer. Personal sort applies on /home but not
on the public root — the landing is the canonical newest-first
view, not the viewer's curated one.
96 tests still pass; typecheck clean; build ok.
This commit is contained in:
parent
59e2f95767
commit
51f2dcd104
3 changed files with 41 additions and 74 deletions
|
|
@ -18,16 +18,10 @@
|
|||
/** 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;
|
||||
/** 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?: () => void;
|
||||
}
|
||||
let {
|
||||
activity, privateCleartext = null,
|
||||
onDeleted, onEdit, onChanged,
|
||||
draggable = false, onDragHandleStart,
|
||||
}: Props = $props();
|
||||
|
||||
// Fallback decrypt for the case where Home hasn't pre-computed yet (e.g.
|
||||
|
|
@ -201,22 +195,7 @@
|
|||
</p>
|
||||
{/snippet}
|
||||
|
||||
<article class="card" aria-labelledby={`act-${activity.id}-h`}
|
||||
style={draggable ? 'display: flex; align-items: flex-start; gap: 0.75rem;' : ''}>
|
||||
{#if draggable}
|
||||
<!-- Drag handle. The parent uses svelte-dnd-action and keeps
|
||||
dragDisabled=true by default; pressing the handle flips it false
|
||||
for this gesture. Works for mouse, touch, and pen via Pointer
|
||||
Events. Clicks elsewhere on the card don't initiate drag. -->
|
||||
<button
|
||||
type="button"
|
||||
class="drag-handle"
|
||||
onpointerdown={onDragHandleStart}
|
||||
aria-label="Dra for å endre rekkefølge"
|
||||
title="Dra for å endre rekkefølge"
|
||||
>⋮⋮</button>
|
||||
{/if}
|
||||
<div style={draggable ? 'flex: 1; min-width: 0;' : ''}>
|
||||
<article class="card" aria-labelledby={`act-${activity.id}-h`}>
|
||||
{#if activity.visibility === 'private'}
|
||||
{#if decrypted}
|
||||
<h3 id={`act-${activity.id}-h`} style="display: flex; align-items: center;">
|
||||
|
|
@ -331,5 +310,4 @@
|
|||
{copiedAt ? 'Kopiert!' : 'Del'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -89,16 +89,22 @@
|
|||
].some((s) => s.toLowerCase().includes(needle));
|
||||
}
|
||||
|
||||
// The list is now unified — one column, sorted by sort_position. The
|
||||
// server's COALESCE(custom, -created_at) handles both manually-positioned
|
||||
// and not-yet-touched rows. Bookmark/friends/private/semi/public are all
|
||||
// intermingled; their vis-badges tell the viewer which is which.
|
||||
// The list is unified — one column. On the public landing ("/", which
|
||||
// anonymous and logged-in users both see), the order is strictly
|
||||
// newest-first regardless of any personal sort the viewer may have
|
||||
// applied on /home. On the authenticated dashboard, sort by the
|
||||
// viewer's effective sort_position (custom if dragged, else
|
||||
// -created_at, both surfaced by the server).
|
||||
const filtered = $derived(
|
||||
activities
|
||||
.filter((a) => !publicOnly || a.visibility !== 'private')
|
||||
.filter((a) => matchesQuery(a, query))
|
||||
.slice()
|
||||
.sort((a, b) => a.sort_position - b.sort_position),
|
||||
.sort((a, b) =>
|
||||
publicOnly
|
||||
? b.created_at - a.created_at
|
||||
: a.sort_position - b.sort_position,
|
||||
),
|
||||
);
|
||||
|
||||
// --- Drag-and-drop reordering --------------------------------------------
|
||||
|
|
@ -106,33 +112,34 @@
|
|||
// We hand it the `filtered` array and let it dispatch `consider`/`finalize`
|
||||
// CustomEvents as the user drags.
|
||||
//
|
||||
// Drag-handle-only behaviour: the library doesn't have a built-in "handle"
|
||||
// option, so we use the standard recipe — keep dragDisabled=true by
|
||||
// default, flip to false on pointerdown/touchstart on the handle, flip
|
||||
// back after finalize. Clicks on the title/buttons inside a card don't
|
||||
// initiate drag because dragDisabled is true everywhere else.
|
||||
// dragDisabled is a pure function of auth state — NOT a handle toggle.
|
||||
// An earlier version gated it on handle pointerdown, which had two bugs:
|
||||
// 1. Desktop required two gestures (handle to "enable", then card to
|
||||
// drag) because pointerdown flipped the flag too late for the
|
||||
// already-in-progress mouse gesture.
|
||||
// 2. Keyboard reorder relies on dragDisabled being false at focus time
|
||||
// — gating it broke Space/arrow-key lifting.
|
||||
// The library's built-in distance threshold prevents accidental drags
|
||||
// from clicks. Form controls and links inside cards aren't draggable.
|
||||
// The drag-handle icon stays as a visual affordance only.
|
||||
|
||||
/** Items the library is currently working with — a copy of `filtered` it
|
||||
* can mutate during the drag for the visual transition. Re-derived from
|
||||
* `filtered` between drags. */
|
||||
/** Items the library mutates during the drag. Re-synced from `filtered`
|
||||
* between drags via the $effect below. */
|
||||
let dndItems: Activity[] = $state([]);
|
||||
/** Disable drag globally unless the user has just tapped a drag handle. */
|
||||
let dragDisabled = $state(true);
|
||||
/** Drag is disabled for anonymous viewers and on the public landing. */
|
||||
const dragDisabled = $derived(!session.user || publicOnly);
|
||||
|
||||
/** Set to true while a drag is in progress, so the resync effect doesn't
|
||||
* fight the library by overwriting its in-flight item ordering. */
|
||||
let dragActive = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
// Keep dndItems in sync with the source-of-truth `filtered` array
|
||||
// whenever it changes outside of an active drag (e.g. after a new
|
||||
// activity is created, or the search filter shrinks the list).
|
||||
if (dragDisabled) dndItems = filtered.slice();
|
||||
if (!dragActive) dndItems = filtered.slice();
|
||||
});
|
||||
|
||||
function activateDrag() {
|
||||
if (!session.user) return;
|
||||
dragDisabled = false;
|
||||
}
|
||||
|
||||
function handleConsider(e: CustomEvent<{ items: Activity[] }>) {
|
||||
// Library asks us to render this transient ordering during the drag.
|
||||
dragActive = true;
|
||||
dndItems = e.detail.items;
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +147,7 @@
|
|||
const next = e.detail.items;
|
||||
const movedId = e.detail.info.id;
|
||||
dndItems = next;
|
||||
dragDisabled = true;
|
||||
dragActive = false;
|
||||
|
||||
// Find where the moved item landed and compute a midpoint position
|
||||
// between its (new) neighbours' sort_positions.
|
||||
|
|
@ -267,8 +274,6 @@
|
|||
onDeleted={onDeleted}
|
||||
onEdit={(act) => (editing = act)}
|
||||
onChanged={onChanged}
|
||||
draggable={!!session.user && !publicOnly}
|
||||
onDragHandleStart={activateDrag}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -294,30 +294,14 @@ nav.top h1::after {
|
|||
override the ::before-emoji default that .vis-badge applies otherwise. */
|
||||
.vis-badge.bookmarked::before { content: none; }
|
||||
|
||||
/* Drag handle on each activity row in the unified list view. The button
|
||||
itself is what `draggable="true"` is bound to so users have a clear
|
||||
target; the rest of the card stays selectable. */
|
||||
.drag-handle {
|
||||
min-height: 24px !important;
|
||||
min-width: 28px !important;
|
||||
padding: 0 0.3rem !important;
|
||||
cursor: grab;
|
||||
background: transparent !important;
|
||||
border: 1px solid transparent !important;
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
background: var(--accent-soft) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
/* Reorder affordance for the unified activity list. svelte-dnd-action
|
||||
wraps each item in a div with role="listitem"; the card becomes
|
||||
grabbable on the empty regions while buttons and links inside keep
|
||||
their native cursor. */
|
||||
[role="listitem"] article.card { cursor: grab; }
|
||||
[role="listitem"] article.card:active { cursor: grabbing; }
|
||||
[role="listitem"] article.card button,
|
||||
[role="listitem"] article.card a { cursor: pointer; }
|
||||
|
||||
.error { color: var(--danger); margin-top: 0.5rem; }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue