a11y(modals): role=dialog + Fokus-Falle und -Rueckgabe fuer Settings und Theme-Picker
This commit is contained in:
+4
-4
@@ -103,9 +103,9 @@
|
||||
|
||||
<!-- SETTINGS PANEL -->
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@@ -291,9 +291,9 @@
|
||||
|
||||
<!-- THEME PICKER MODAL -->
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="theme-grid">
|
||||
|
||||
+62
-4
@@ -3,30 +3,88 @@
|
||||
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 ----
|
||||
// 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() {
|
||||
const panel = document.getElementById('settingsPanel');
|
||||
_focusReturn.settings = document.activeElement;
|
||||
withViewTransition(() => {
|
||||
document.getElementById('settingsPanel').classList.add('open');
|
||||
panel.classList.add('open');
|
||||
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() {
|
||||
const panel = document.getElementById('settingsPanel');
|
||||
withViewTransition(() => {
|
||||
document.getElementById('settingsPanel').classList.remove('open');
|
||||
panel.classList.remove('open');
|
||||
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 ----
|
||||
let _themeTrap = null;
|
||||
function openThemeModal() {
|
||||
const overlay = document.getElementById('themeOverlay');
|
||||
const modal = document.getElementById('themeModal');
|
||||
_focusReturn.theme = document.activeElement;
|
||||
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() {
|
||||
const overlay = document.getElementById('themeOverlay');
|
||||
const modal = document.getElementById('themeModal');
|
||||
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; }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user