a11y(modals): role=dialog + Fokus-Falle und -Rueckgabe fuer Settings und Theme-Picker

This commit is contained in:
2026-06-13 20:58:42 +02:00
parent 87cd070beb
commit 0a93340792
2 changed files with 66 additions and 8 deletions
+4 -4
View File
@@ -103,9 +103,9 @@
<!-- SETTINGS PANEL --> <!-- SETTINGS PANEL -->
<div class="panel-overlay" id="settingsOverlay"></div> <div class="panel-overlay" id="settingsOverlay"></div>
<aside class="settings-panel" id="settingsPanel"> <aside class="settings-panel" id="settingsPanel" role="dialog" aria-modal="true" aria-labelledby="settingsPanelTitle" aria-hidden="true">
<div class="panel-header"> <div class="panel-header">
<span data-i18n="settings.title">Einstellungen</span> <span id="settingsPanelTitle" data-i18n="settings.title">Einstellungen</span>
<button class="btn-close" id="btnCloseSettings" data-i18n-title="dialog.close"></button> <button class="btn-close" id="btnCloseSettings" data-i18n-title="dialog.close"></button>
</div> </div>
<div class="panel-body"> <div class="panel-body">
@@ -291,9 +291,9 @@
<!-- THEME PICKER MODAL --> <!-- THEME PICKER MODAL -->
<div class="modal-overlay" id="themeOverlay"> <div class="modal-overlay" id="themeOverlay">
<div class="theme-modal" id="themeModal"> <div class="theme-modal" id="themeModal" role="dialog" aria-modal="true" aria-labelledby="themeModalTitle" aria-hidden="true">
<div class="modal-header"> <div class="modal-header">
<span data-i18n="modal.theme_header">Darstellung</span> <span id="themeModalTitle" data-i18n="modal.theme_header">Darstellung</span>
<button class="btn-close" id="btnCloseTheme" data-i18n-title="dialog.close"></button> <button class="btn-close" id="btnCloseTheme" data-i18n-title="dialog.close"></button>
</div> </div>
<div class="theme-grid"> <div class="theme-grid">
+62 -4
View File
@@ -3,30 +3,88 @@
Settings Panel, Theme-Modal, Accordion, Toggles Settings Panel, Theme-Modal, Accordion, Toggles
============================================= */ ============================================= */
// ---- A11Y: Fokus-Management fuer Modals ----
// Merkt sich das vor dem Oeffnen fokussierte Element, damit wir es beim
// Schliessen restaurieren koennen. Pro offenem Modal eine Closure-Variable.
const _focusReturn = { settings: null, theme: null };
/** Liefert die fokussierbaren Elemente innerhalb eines Containers. */
function _focusable(container) {
return Array.from(container.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)).filter(el => el.offsetParent !== null);
}
/** Tab/Shift+Tab im Container einfangen + Escape schliesst. */
function _makeTrap(container, closeFn) {
return function trap(e) {
if (e.key === 'Escape') { e.preventDefault(); closeFn(); return; }
if (e.key !== 'Tab') return;
const items = _focusable(container);
if (items.length === 0) return;
const first = items[0];
const last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
};
}
// ---- SETTINGS PANEL ---- // ---- SETTINGS PANEL ----
// Hinweis: withViewTransition (Phase 4) bleibt fuer das Fade erhalten; das
// Fokus-Management (merken, Falle, Rueckgabe) liegt bewusst ausserhalb des
// Transition-Callbacks. activeElement wird vor der Mutation gelesen.
let _settingsTrap = null;
function openSettings() { function openSettings() {
const panel = document.getElementById('settingsPanel');
_focusReturn.settings = document.activeElement;
withViewTransition(() => { withViewTransition(() => {
document.getElementById('settingsPanel').classList.add('open'); panel.classList.add('open');
document.getElementById('settingsOverlay').classList.add('active'); document.getElementById('settingsOverlay').classList.add('active');
}); });
panel.setAttribute('aria-hidden', 'false');
_settingsTrap = _makeTrap(panel, closeSettings);
document.addEventListener('keydown', _settingsTrap);
const first = _focusable(panel)[0];
if (first) first.focus();
} }
function closeSettings() { function closeSettings() {
const panel = document.getElementById('settingsPanel');
withViewTransition(() => { withViewTransition(() => {
document.getElementById('settingsPanel').classList.remove('open'); panel.classList.remove('open');
document.getElementById('settingsOverlay').classList.remove('active'); document.getElementById('settingsOverlay').classList.remove('active');
}); });
panel.setAttribute('aria-hidden', 'true');
if (_settingsTrap) { document.removeEventListener('keydown', _settingsTrap); _settingsTrap = null; }
if (_focusReturn.settings) { _focusReturn.settings.focus(); _focusReturn.settings = null; }
} }
// ---- THEME MODAL ---- // ---- THEME MODAL ----
let _themeTrap = null;
function openThemeModal() { function openThemeModal() {
const overlay = document.getElementById('themeOverlay');
const modal = document.getElementById('themeModal');
_focusReturn.theme = document.activeElement;
withViewTransition(() => { withViewTransition(() => {
document.getElementById('themeOverlay').classList.add('active'); overlay.classList.add('active');
}); });
modal.setAttribute('aria-hidden', 'false');
_themeTrap = _makeTrap(modal, closeThemeModal);
document.addEventListener('keydown', _themeTrap);
const first = _focusable(modal)[0];
if (first) first.focus();
} }
function closeThemeModal() { function closeThemeModal() {
const overlay = document.getElementById('themeOverlay');
const modal = document.getElementById('themeModal');
withViewTransition(() => { withViewTransition(() => {
document.getElementById('themeOverlay').classList.remove('active'); overlay.classList.remove('active');
}); });
modal.setAttribute('aria-hidden', 'true');
if (_themeTrap) { document.removeEventListener('keydown', _themeTrap); _themeTrap = null; }
if (_focusReturn.theme) { _focusReturn.theme.focus(); _focusReturn.theme = null; }
} }
/** /**