diff --git a/src/app.css b/src/app.css index 8395f41..1e728bb 100644 --- a/src/app.css +++ b/src/app.css @@ -159,3 +159,21 @@ button { button:active:not(:disabled) { transition: transform 0.08s var(--ease-out); } + +/* ── Reduced motion: respect user preference ────────────────────── + Users with vestibular disorders or motion sensitivity expect + apps to minimise movement. We disable all CSS animations and + transitions globally, and components that use JS-driven + animation frames (e.g. GraphView physics) should check this + media query via window.matchMedia('(prefers-reduced-motion: reduce)'). + ─────────────────────────────────────────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/lib/components/DetailPanel.svelte b/src/lib/components/DetailPanel.svelte index 1f83f93..c98d8f0 100644 --- a/src/lib/components/DetailPanel.svelte +++ b/src/lib/components/DetailPanel.svelte @@ -11,19 +11,56 @@ let { memoryId, onclose, onneighborclick, onedit, children }: Props = $props(); + let panelEl: HTMLElement | null = null; + let previouslyFocused: HTMLElement | null = null; + function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onclose(); + return; + } + // Focus trap: cycle Tab/Shift+Tab within the dialog. + if (e.key !== 'Tab' || !panelEl) return; + const focusable = panelEl.querySelectorAll( + 'button, [href], input, textarea, select, [tabindex]:not([tabindex="-1"])', + ); + if (focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first || !panelEl!.contains(document.activeElement)) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } } } onMount(() => { + previouslyFocused = document.activeElement as HTMLElement; window.addEventListener('keydown', handleKeydown); + // Move focus into the panel on open so screen readers announce it. + if (panelEl) { + const first = panelEl.querySelector( + 'button, [href], input, [tabindex]:not([tabindex="-1"])', + ); + if (first) { + first.focus(); + } else { + panelEl.focus(); + } + } }); onDestroy(() => { window.removeEventListener('keydown', handleKeydown); + // Restore focus to the element that opened the panel. + previouslyFocused?.focus(); }); @@ -35,7 +72,7 @@ > -