diff --git a/messages/de.json b/messages/de.json index 6173dea8..203b7f11 100644 --- a/messages/de.json +++ b/messages/de.json @@ -25,6 +25,7 @@ "sidebar_expand": "Seitenleiste ausklappen", "sidebar_collapse": "Seitenleiste einklappen", "sidebar_collapse_label": "Einklappen", + "skip_to_content": "Zum Hauptinhalt springen", "header_open_nav": "Navigation öffnen", "header_close_nav": "Navigation schließen", "header_user_menu": "Benutzermenü", diff --git a/messages/en.json b/messages/en.json index b9216ce0..b9770c86 100644 --- a/messages/en.json +++ b/messages/en.json @@ -25,6 +25,7 @@ "sidebar_expand": "Expand sidebar", "sidebar_collapse": "Collapse sidebar", "sidebar_collapse_label": "Collapse", + "skip_to_content": "Skip to main content", "header_open_nav": "Open navigation", "header_close_nav": "Close navigation", "header_user_menu": "User menu", diff --git a/src/lib/components/layout/sidebar/Sidebar.svelte b/src/lib/components/layout/sidebar/Sidebar.svelte index 8b517ace..80935e6e 100644 --- a/src/lib/components/layout/sidebar/Sidebar.svelte +++ b/src/lib/components/layout/sidebar/Sidebar.svelte @@ -5,6 +5,7 @@ import type { Component } from 'svelte'; import IconChevronLeft from 'virtual:icons/material-symbols/chevron-left'; import IconChevronRight from 'virtual:icons/material-symbols/chevron-right'; + import { MediaQuery } from 'svelte/reactivity'; import type { NavItem } from '$lib/types/navigation.js'; import { getNavSections } from './nav-items.js'; @@ -20,6 +21,10 @@ const sections = $derived(getNavSections({ storageBrowserEnabled })); + // make mobile navbar not focusable if its hidden + const isDesktop = new MediaQuery('min-width: 64rem', true); + const hiddenFromUser = $derived(!isDesktop.current && !mobileOpen); + function isActive(href: string): boolean { if (href === '/') return page.url.pathname === '/'; return page.url.pathname.startsWith(href); @@ -40,8 +45,35 @@ } let sidebarEl: HTMLElement | undefined = $state(); + + $effect(() => { + if (isDesktop.current || !mobileOpen || !sidebarEl) return; + sidebarEl.focus(); + }); + + function handleWindowKeydown(event: KeyboardEvent) { + if (isDesktop.current || !mobileOpen) return; + + if (event.key === 'Escape') { + event.preventDefault(); + mobileOpen = false; + } + } + + // Tabbing off either end of the open panel would land on the page behind the + // backdrop, so send focus back to the top of the panel instead. + function handleFocusOut(event: FocusEvent) { + if (isDesktop.current || !mobileOpen) return; + + const next = event.relatedTarget as Node | null; + if (next && sidebarEl?.contains(next)) return; + + sidebarEl?.focus(); + } + + {#snippet navIcon(IconComponent: Component)}