feat(web/m7-364): drawer focus management + grip Enter/Space handling
This commit is contained in:
@@ -3,12 +3,27 @@
|
|||||||
import { player, closeQueueDrawer } from '$lib/player/store.svelte';
|
import { player, closeQueueDrawer } from '$lib/player/store.svelte';
|
||||||
import QueueTrackRow from './QueueTrackRow.svelte';
|
import QueueTrackRow from './QueueTrackRow.svelte';
|
||||||
|
|
||||||
|
let previouslyFocused: HTMLElement | null = null;
|
||||||
|
let closeButton: HTMLButtonElement | undefined = $state();
|
||||||
|
|
||||||
function totalDurationLabel(tracks: { duration_sec: number }[]): string {
|
function totalDurationLabel(tracks: { duration_sec: number }[]): string {
|
||||||
const totalSec = tracks.reduce((s, tr) => s + (tr.duration_sec ?? 0), 0);
|
const totalSec = tracks.reduce((s, tr) => s + (tr.duration_sec ?? 0), 0);
|
||||||
const m = Math.floor(totalSec / 60);
|
const m = Math.floor(totalSec / 60);
|
||||||
const h = Math.floor(m / 60);
|
const h = Math.floor(m / 60);
|
||||||
return h > 0 ? `${h}h ${m % 60}m` : `${m} min`;
|
return h > 0 ? `${h}h ${m % 60}m` : `${m} min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (player.queueDrawerOpen) {
|
||||||
|
previouslyFocused = document.activeElement as HTMLElement | null;
|
||||||
|
// Defer to next microtask so the close button is rendered + visible
|
||||||
|
// before we focus (the drawer may still be transitioning in).
|
||||||
|
Promise.resolve().then(() => closeButton?.focus());
|
||||||
|
} else if (previouslyFocused) {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
previouslyFocused = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if player.queueDrawerOpen}
|
{#if player.queueDrawerOpen}
|
||||||
@@ -38,6 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
bind:this={closeButton}
|
||||||
aria-label="Close queue"
|
aria-label="Close queue"
|
||||||
onclick={() => closeQueueDrawer()}
|
onclick={() => closeQueueDrawer()}
|
||||||
class="text-text-secondary hover:text-text-primary"
|
class="text-text-secondary hover:text-text-primary"
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
moveQueueItem(index, index + 1);
|
moveQueueItem(index, index + 1);
|
||||||
|
} else if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
// Reorder activates via arrow keys, not Space/Enter. Prevent
|
||||||
|
// default so Space doesn't scroll the queue list and Enter doesn't
|
||||||
|
// fire the button's no-op click.
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -53,6 +58,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Reorder track (use arrow keys)"
|
aria-label="Reorder track (use arrow keys)"
|
||||||
|
aria-keyshortcuts="ArrowUp ArrowDown"
|
||||||
onkeydown={handleHandleKeydown}
|
onkeydown={handleHandleKeydown}
|
||||||
class="cursor-grab text-text-secondary hover:text-text-primary flex-shrink-0"
|
class="cursor-grab text-text-secondary hover:text-text-primary flex-shrink-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -77,4 +77,18 @@ describe('QueueTrackRow', () => {
|
|||||||
await fireEvent.keyDown(handle, { key: 'ArrowUp' });
|
await fireEvent.keyDown(handle, { key: 'ArrowUp' });
|
||||||
expect(moveQueueItem).toHaveBeenCalledWith(3, 2);
|
expect(moveQueueItem).toHaveBeenCalledWith(3, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Enter on the drag handle prevents default but does not call moveQueueItem', async () => {
|
||||||
|
render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
|
||||||
|
const handle = screen.getByLabelText(/reorder track/i);
|
||||||
|
await fireEvent.keyDown(handle, { key: 'Enter' });
|
||||||
|
expect(moveQueueItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Space on the drag handle prevents default but does not call moveQueueItem', async () => {
|
||||||
|
render(QueueTrackRow, { props: { track: sampleTrack, index: 3, isCurrent: false } });
|
||||||
|
const handle = screen.getByLabelText(/reorder track/i);
|
||||||
|
await fireEvent.keyDown(handle, { key: ' ' });
|
||||||
|
expect(moveQueueItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user