|
5 | 5 | import type { Component } from 'svelte'; |
6 | 6 | import IconChevronLeft from 'virtual:icons/material-symbols/chevron-left'; |
7 | 7 | import IconChevronRight from 'virtual:icons/material-symbols/chevron-right'; |
| 8 | + import { MediaQuery } from 'svelte/reactivity'; |
8 | 9 | import type { NavItem } from '$lib/types/navigation.js'; |
9 | 10 | import { getNavSections } from './nav-items.js'; |
10 | 11 |
|
|
20 | 21 |
|
21 | 22 | const sections = $derived(getNavSections({ storageBrowserEnabled })); |
22 | 23 |
|
| 24 | + // make mobile navbar not focusable if its hidden |
| 25 | + const isDesktop = new MediaQuery('min-width: 64rem', true); |
| 26 | + const hiddenFromUser = $derived(!isDesktop.current && !mobileOpen); |
| 27 | +
|
23 | 28 | function isActive(href: string): boolean { |
24 | 29 | if (href === '/') return page.url.pathname === '/'; |
25 | 30 | return page.url.pathname.startsWith(href); |
|
40 | 45 | } |
41 | 46 |
|
42 | 47 | let sidebarEl: HTMLElement | undefined = $state(); |
| 48 | +
|
| 49 | + $effect(() => { |
| 50 | + if (isDesktop.current || !mobileOpen || !sidebarEl) return; |
| 51 | + sidebarEl.focus(); |
| 52 | + }); |
| 53 | +
|
| 54 | + function handleWindowKeydown(event: KeyboardEvent) { |
| 55 | + if (isDesktop.current || !mobileOpen) return; |
| 56 | +
|
| 57 | + if (event.key === 'Escape') { |
| 58 | + event.preventDefault(); |
| 59 | + mobileOpen = false; |
| 60 | + } |
| 61 | + } |
| 62 | +
|
| 63 | + // Tabbing off either end of the open panel would land on the page behind the |
| 64 | + // backdrop, so send focus back to the top of the panel instead. |
| 65 | + function handleFocusOut(event: FocusEvent) { |
| 66 | + if (isDesktop.current || !mobileOpen) return; |
| 67 | +
|
| 68 | + const next = event.relatedTarget as Node | null; |
| 69 | + if (next && sidebarEl?.contains(next)) return; |
| 70 | +
|
| 71 | + sidebarEl?.focus(); |
| 72 | + } |
43 | 73 | </script> |
44 | 74 |
|
| 75 | +<svelte:window onkeydown={handleWindowKeydown} /> |
| 76 | + |
45 | 77 | {#snippet navIcon(IconComponent: Component)} |
46 | 78 | <IconComponent class="h-5 w-5 shrink-0" aria-hidden="true" /> |
47 | 79 | {/snippet} |
|
59 | 91 | <!-- Sidebar --> |
60 | 92 | <aside |
61 | 93 | bind:this={sidebarEl} |
| 94 | + onfocusout={handleFocusOut} |
62 | 95 | id="sidebar" |
63 | 96 | aria-label={m.sidebar_label()} |
| 97 | + tabindex="-1" |
| 98 | + inert={hiddenFromUser} |
64 | 99 | class="border-base-300 bg-base-200 fixed inset-y-0 left-0 z-50 flex flex-col border-r |
65 | 100 | transition-[transform,width] duration-200 ease-out |
66 | 101 | lg:relative lg:inset-auto lg:z-auto lg:translate-x-0 |
|
0 commit comments