From 04c93adff3df4a34a0be3a717b8973cddcf31960 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Mon, 22 Jun 2026 23:44:31 +0000 Subject: [PATCH] T23: Add modal keyboard accessibility (Escape, focus trap, ARIA) and dropdown outside-click/Escape close --- .../components/PrintPreview/PrintPreview.tsx | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/PrintPreview/PrintPreview.tsx b/frontend/src/components/PrintPreview/PrintPreview.tsx index 3daa68d..d82ac33 100644 --- a/frontend/src/components/PrintPreview/PrintPreview.tsx +++ b/frontend/src/components/PrintPreview/PrintPreview.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import type { YjsDocument } from '../../crdt/YjsDocument'; import { preparePrint, @@ -21,10 +21,16 @@ interface PrintPreviewProps { const PAGE_SIZES: PageSize[] = ['a4', 'a3', 'a2', 'a1']; const SCALE_PRESETS: ScalePreset[] = ['1:50', '1:100', '1:200', 'custom']; +const FOCUSABLE_SELECTOR = + 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; + const PrintPreview: React.FC = ({ yjsDoc, onClose }) => { const [settings, setSettings] = useState({ ...DEFAULT_PRINT_SETTINGS }); const [currentPage, setCurrentPage] = useState(0); + const modalRef = useRef(null); + const previouslyFocusedRef = useRef(null); + const printResult: PrintResult | null = useMemo(() => { const doc = yjsDoc?.current; if (!doc) return null; @@ -49,12 +55,70 @@ const PrintPreview: React.FC = ({ yjsDoc, onClose }) => { } }, [printResult]); + // Keyboard accessibility: Escape to close, focus trap, focus management + useEffect(() => { + // Store the element that had focus before the modal opened + previouslyFocusedRef.current = document.activeElement as HTMLElement; + + // Focus the first focusable element in the modal + const modal = modalRef.current; + if (modal) { + const focusable = modal.querySelectorAll(FOCUSABLE_SELECTOR); + if (focusable.length > 0) { + focusable[0].focus(); + } else { + modal.focus(); + } + } + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + return; + } + + if (e.key === 'Tab') { + const modalEl = modalRef.current; + if (!modalEl) return; + + const focusableElements = modalEl.querySelectorAll(FOCUSABLE_SELECTOR); + if (focusableElements.length === 0) return; + + const first = focusableElements[0]; + const last = focusableElements[focusableElements.length - 1]; + + if (e.shiftKey) { + if (document.activeElement === first || !modalEl.contains(document.activeElement)) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last || !modalEl.contains(document.activeElement)) { + e.preventDefault(); + first.focus(); + } + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + // Restore focus to the trigger element + if (previouslyFocusedRef.current) { + previouslyFocusedRef.current.focus(); + } + }; + }, [onClose]); + if (!printResult) { return ( -
+
-

Druckvorschau

+
Kein Dokument verfügbar.
@@ -68,11 +132,11 @@ const PrintPreview: React.FC = ({ yjsDoc, onClose }) => { const previewUrl = page ? svgToDataUrl(page.svg) : ''; return ( -
+
{/* Header */}
-

Druckvorschau

+
@@ -265,7 +329,7 @@ const PrintPreview: React.FC = ({ yjsDoc, onClose }) => { key={i} className={`print-preview-grid-cell ${i === currentPage ? 'active' : ''}`} onClick={() => setCurrentPage(i)} - title={`Seite ${i + 1} (Spalte ${p.col + 1}, Zeile ${p.row + 1})}`} + title={`Seite ${i + 1} (Spalte ${p.col + 1}, Zeile ${p.row + 1})`} > {i + 1}