Skip to content

Commit 3bb37a5

Browse files
author
m.buchhorn-roth
committed
a11y: mobile nav hamburger, ARIA live region for narration, 3D graph accessible label
- Mobile: hamburger button (aria-expanded/controls), dropdown menu (role=menu/menuitem), Escape-to-close - Narration: role=status aria-live=polite region announces play/pause state - 3D graph: role=img + aria-label on container, aria-label on control buttons, aria-hidden on decorative icons - Closes all 3 known open WCAG gaps
1 parent df895be commit 3bb37a5

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

src/App.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import {
4848
Pause,
4949
Volume2,
5050
VolumeX,
51+
Menu,
52+
X,
5153
} from "lucide-react"
5254
import { useState, useEffect, useRef, useCallback, useMemo } from "react"
5355
import { RELIEFKnowledgeGraph3D } from "@/components/RELIEFKnowledgeGraph3D"
@@ -60,6 +62,8 @@ function App() {
6062
const [showIntroGuide, setShowIntroGuide] = useState<boolean>(true)
6163
const [isPlayingNarration, setIsPlayingNarration] = useState(false)
6264
const [isMuted, setIsMuted] = useState(false)
65+
const [mobileNavOpen, setMobileNavOpen] = useState(false)
66+
const [narrationStatus, setNarrationStatus] = useState('')
6367
const audioRef = useRef<HTMLAudioElement | null>(null)
6468
const architectureRef = useRef<HTMLDivElement>(null)
6569

@@ -88,8 +92,10 @@ function App() {
8892
if (!audioRef.current) return
8993
if (isPlayingNarration) {
9094
audioRef.current.pause()
95+
setNarrationStatus('Narration pausiert')
9196
} else {
9297
audioRef.current.play()
98+
setNarrationStatus('Narration wird abgespielt')
9399
}
94100
setIsPlayingNarration(!isPlayingNarration)
95101
}, [isPlayingNarration])
@@ -107,8 +113,19 @@ function App() {
107113
// Move focus to the section for keyboard/screen-reader users
108114
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1')
109115
el.focus({ preventScroll: true })
116+
setMobileNavOpen(false)
110117
}
111118

119+
// ── Close mobile nav on Escape ──
120+
useEffect(() => {
121+
if (!mobileNavOpen) return
122+
const handleKeyDown = (e: KeyboardEvent) => {
123+
if (e.key === 'Escape') setMobileNavOpen(false)
124+
}
125+
document.addEventListener('keydown', handleKeyDown)
126+
return () => document.removeEventListener('keydown', handleKeyDown)
127+
}, [mobileNavOpen])
128+
112129
// ── Side navigation sections ──
113130
const sideNavSections = useMemo(() => [
114131
{ id: 'challenges', label: 'Herausforderungen', group: 'Problem' },
@@ -402,6 +419,10 @@ function App() {
402419
>
403420
Zum Hauptinhalt springen
404421
</a>
422+
{/* ── WCAG: ARIA live region for narration status ── */}
423+
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
424+
{narrationStatus}
425+
</div>
405426
<AnimatePresence>
406427
{showIntroGuide && (
407428
<motion.div
@@ -461,8 +482,46 @@ function App() {
461482
<ArrowRight className="ml-2 h-4 w-4" aria-hidden="true" />
462483
</a>
463484
</Button>
485+
{/* ── Mobile hamburger button ── */}
486+
<Button
487+
variant="ghost"
488+
size="icon"
489+
className="md:hidden"
490+
aria-expanded={mobileNavOpen}
491+
aria-controls="mobile-nav-menu"
492+
aria-label={mobileNavOpen ? 'Navigation schließen' : 'Navigation öffnen'}
493+
onClick={() => setMobileNavOpen(v => !v)}
494+
>
495+
{mobileNavOpen ? <X className="h-5 w-5" aria-hidden="true" /> : <Menu className="h-5 w-5" aria-hidden="true" />}
496+
</Button>
464497
</nav>
465498
</div>
499+
{/* ── Mobile nav dropdown ── */}
500+
{mobileNavOpen && (
501+
<div
502+
id="mobile-nav-menu"
503+
role="menu"
504+
className="md:hidden border-t border-border bg-card/95 backdrop-blur-md px-6 py-3 flex flex-col gap-1"
505+
>
506+
{[
507+
{ id: 'fall-becker', label: 'Fall Becker' },
508+
{ id: 'architecture', label: 'Architektur' },
509+
{ id: 'document-ai', label: 'Document AI' },
510+
{ id: 'process-modernization', label: 'Prozesse' },
511+
{ id: 'scenarios', label: 'Szenarien' },
512+
{ id: 'tech-stack', label: 'Technologie' },
513+
].map(item => (
514+
<button
515+
key={item.id}
516+
role="menuitem"
517+
onClick={() => scrollToSection(item.id)}
518+
className="text-left text-sm px-3 py-2 rounded-lg hover:bg-accent/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary transition-colors"
519+
>
520+
{item.label}
521+
</button>
522+
))}
523+
</div>
524+
)}
466525
</header>
467526

468527
{/* ── SIDE NAVIGATION (desktop only, top-right below header) ── */}

src/components/RELIEFKnowledgeGraph3D.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,8 @@ export function RELIEFKnowledgeGraph3D() {
527527
return (
528528
<div
529529
ref={containerRef}
530+
role="img"
531+
aria-label={`Interaktiver 3D Knowledge Graph: RELIEF E-AKTE — ${graphData.nodes.length} Knoten, ${graphData.links.length} Beziehungen. Zeigt SGB-II-Rechtsstruktur der Bedarfsgemeinschaft Becker mit Dokumenten, Gesetzen und KI-Verarbeitungsschritten.`}
530532
className={`relative rounded-xl overflow-hidden border-2 border-border bg-[#0f172a] ${
531533
isFullscreen ? 'fixed inset-0 z-50 rounded-none' : 'w-full h-full'
532534
}`}
@@ -537,15 +539,18 @@ export function RELIEFKnowledgeGraph3D() {
537539
onClick={resetCamera}
538540
className="p-2 rounded-lg bg-slate-800/80 text-slate-300 hover:bg-slate-700/80 hover:text-white transition-colors backdrop-blur-sm"
539541
title="Kamera zurücksetzen"
542+
aria-label="Kamera zurücksetzen"
540543
>
541-
<RotateCcw className="h-4 w-4" />
544+
<RotateCcw className="h-4 w-4" aria-hidden="true" />
542545
</button>
543546
<button
544547
onClick={toggleFullscreen}
545548
className="p-2 rounded-lg bg-slate-800/80 text-slate-300 hover:bg-slate-700/80 hover:text-white transition-colors backdrop-blur-sm"
546549
title={isFullscreen ? 'Vollbild beenden' : 'Vollbild'}
550+
aria-label={isFullscreen ? 'Vollbild beenden' : 'Vollbild aktivieren'}
551+
aria-pressed={isFullscreen}
547552
>
548-
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
553+
{isFullscreen ? <Minimize2 className="h-4 w-4" aria-hidden="true" /> : <Maximize2 className="h-4 w-4" aria-hidden="true" />}
549554
</button>
550555
</div>
551556

0 commit comments

Comments
 (0)