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 -->
|
<!-- 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
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user