diff --git a/src/js/settings.js b/src/js/settings.js
index 86ca2ff..bd8594f 100644
--- a/src/js/settings.js
+++ b/src/js/settings.js
@@ -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; }
}
/**