feat(web): keyboard reorder on playlist tracks (Tier C10)
test-web / test (push) Successful in 31s

Drag was already wired via @neodrag/svelte; this adds the missing
keyboard path so reorder is accessible without a pointer device.

- The drag handle becomes a real <button> (focusable). Disabled
  when canDrag is false so non-owners + unavailable tracks can't
  attempt a move.
- ArrowUp / ArrowDown on the focused handle call
  onMove(row.position, row.position +/- 1). Bounds clamping
  already lives in the page-level handler (playlists/[id]/+page.svelte:39),
  so out-of-range targets resolve as no-ops.
- Matches QueueTrackRow's pattern: aria-label "Reorder track …",
  aria-keyshortcuts, swallow Space/Enter to avoid scrolling.

Test updates: drag-handle assertions retargeted to /reorder track/
+ two new ArrowUp/ArrowDown handler tests.
This commit is contained in:
2026-06-01 14:16:27 -04:00
parent cc81e0f183
commit 9fc21d217a
2 changed files with 49 additions and 8 deletions
+25 -4
View File
@@ -41,6 +41,21 @@
onMove?.(row.position, row.position + delta);
}
function handleHandleKeydown(e: KeyboardEvent) {
if (e.key === 'ArrowUp') {
e.preventDefault();
onMove?.(row.position, row.position - 1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
onMove?.(row.position, row.position + 1);
} else if (e.key === ' ' || e.key === 'Enter') {
// Reorder activates via arrow keys, not Space/Enter. Prevent
// default so Space doesn't scroll the list and Enter doesn't
// fire the button's no-op click.
e.preventDefault();
}
}
function format(sec: number): string {
const m = Math.floor(sec / 60);
const s = sec % 60;
@@ -74,12 +89,18 @@
}}
>
{#if isOwner}
<span
class="flex-shrink-0 cursor-grab text-text-muted active:cursor-grabbing"
aria-label="Drag handle"
<button
type="button"
disabled={!canDrag}
aria-label="Reorder track (drag or use arrow keys)"
aria-keyshortcuts="ArrowUp ArrowDown"
onkeydown={handleHandleKeydown}
class="flex-shrink-0 cursor-grab text-text-muted hover:text-text-primary
active:cursor-grabbing focus-visible:outline focus-visible:outline-2
focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50"
>
<GripVertical size={14} strokeWidth={1} />
</span>
</button>
{/if}
<button