Files
minstrel/web/src/lib/components/MobileNavDrawer.svelte
T
bvandeusen 3f8a2c511a fix(web): MobileNavDrawer focus uses querySelector, not conditional bind:this
bind:this requires an Identifier or MemberExpression — a ternary
like {i === 0 ? firstNavLink : undefined} is invalid. Replaced
with a single bind on the <aside> root, then querySelector('nav a')
to locate the first nav link at focus time. Same behaviour, valid
Svelte.

Caught by CI svelte-check on 268e12a (failed before the @const
fix landed in 1536860):
  src/lib/components/MobileNavDrawer.svelte:91:13
  https://svelte.dev/e/bind_invalid_expression

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:26:44 -04:00

142 lines
3.8 KiB
Svelte

<script lang="ts">
import { X } from 'lucide-svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { user, logout } from '$lib/auth/store.svelte';
import { mobileNav, closeMobileNav } from '$lib/stores/mobileNav.svelte';
let previouslyFocused: HTMLElement | null = null;
let drawerEl: HTMLElement | undefined = $state();
const navItems = [
{ href: '/', label: 'Home' },
{ href: '/library/artists', label: 'Artists' },
{ href: '/library/albums', label: 'Albums' },
{ href: '/library/liked', label: 'Liked' },
{ href: '/library/history', label: 'History' },
{ href: '/discover', label: 'Discover' },
{ href: '/playlists', label: 'Playlists' }
];
function isActive(href: string): boolean {
if (href === '/') return page.url.pathname === '/';
return page.url.pathname.startsWith(href);
}
async function handleLogout() {
closeMobileNav();
await logout();
goto('/login', { replaceState: true });
}
function onLinkClick() {
closeMobileNav();
}
$effect(() => {
if (mobileNav.value) {
previouslyFocused = document.activeElement as HTMLElement | null;
Promise.resolve().then(() => {
const first = drawerEl?.querySelector<HTMLAnchorElement>('nav a');
first?.focus();
});
} else if (previouslyFocused) {
previouslyFocused.focus();
previouslyFocused = null;
}
});
function onKeydown(e: KeyboardEvent) {
if (mobileNav.value && e.key === 'Escape') {
closeMobileNav();
}
}
</script>
<svelte:window onkeydown={onKeydown} />
{#if mobileNav.value}
<button
type="button"
aria-label="Close navigation backdrop"
class="fixed inset-0 bg-obsidian/40 z-40 md:hidden"
onclick={() => closeMobileNav()}
></button>
{/if}
<aside
bind:this={drawerEl}
aria-label="Main navigation"
aria-hidden={!mobileNav.value}
inert={!mobileNav.value}
class="fixed top-0 left-0 h-full w-[min(280px,80vw)] bg-surface z-50 md:hidden
transition-transform duration-200 flex flex-col
motion-reduce:transition-none
{mobileNav.value ? 'translate-x-0' : '-translate-x-full'}"
>
<div class="flex items-center justify-between border-b border-border px-4 py-3">
<h2 class="text-lg font-semibold">Navigate</h2>
<button
type="button"
aria-label="Close navigation"
onclick={() => closeMobileNav()}
class="text-text-secondary hover:text-text-primary min-h-[44px] min-w-[44px]
flex items-center justify-center"
>
<X size={20} />
</button>
</div>
<nav class="flex-1 overflow-y-auto p-2">
<ul>
{#each navItems as item (item.href)}
<li>
<a
href={item.href}
onclick={onLinkClick}
class="flex items-center min-h-[44px] rounded px-3 py-2 hover:bg-surface-hover"
aria-current={isActive(item.href) ? 'page' : undefined}
>
{item.label}
</a>
</li>
{/each}
</ul>
<hr class="my-2 border-border" />
<ul>
<li>
<a
href="/settings"
onclick={onLinkClick}
class="flex items-center min-h-[44px] rounded px-3 py-2 hover:bg-surface-hover"
>
Settings
</a>
</li>
{#if user.value?.is_admin}
<li>
<a
href="/admin"
onclick={onLinkClick}
class="flex items-center min-h-[44px] rounded px-3 py-2 hover:bg-surface-hover"
>
Admin
</a>
</li>
{/if}
<li>
<button
type="button"
onclick={handleLogout}
class="flex w-full items-center min-h-[44px] rounded px-3 py-2 text-left
hover:bg-surface-hover"
>
Log out
</button>
</li>
</ul>
</nav>
</aside>