fix(a11y): Dialog-Vorrang in Fokusfalle, eindeutige Dialog-IDs, Bookmark nur Enter

- settings.js: _makeTrap bricht ab, wenn ein .dialog-overlay offen ist, damit
  der Dialog-keydown-Handler Escape/Tab allein behandelt (kein Doppelschluss,
  Fokusfalle bleibt dicht)
- dialog.js: aria-labelledby/-describedby zeigen auf instanz-eindeutige IDs
  (Date.now + Modul-Zaehler) statt feste dialogTitle/dialogBody, damit kurz
  gestapelte Dialoge dem Screenreader nicht den falschen Titel liefern
- boards.js: Bookmark-keydown reagiert bei role=link nur noch auf Enter, Space
  entfernt (Space ist Button-Semantik)
This commit is contained in:
2026-06-13 21:11:49 +02:00
parent eda5fba8f3
commit 55e371f506
3 changed files with 16 additions and 7 deletions
+4 -3
View File
@@ -271,10 +271,11 @@ function bindBoardListEvents(list, board) {
}
});
// Tastatur: Enter oeffnet den Bookmark wie ein Klick. Delete-Button bleibt
// ein echter <button> und feuert seinen eigenen Klick ueber Space/Enter.
// Tastatur: Enter oeffnet den Bookmark wie ein Klick. role="link" erwartet
// nur Enter (Space ist Button-Semantik). Der Delete-Button bleibt ein echter
// <button> und feuert seinen eigenen Klick ueber Space/Enter selbst.
list.addEventListener('keydown', e => {
if (e.key !== 'Enter' && e.key !== ' ') return;
if (e.key !== 'Enter') return;
const bmItem = e.target.closest('.bm-item');
if (!bmItem || e.target !== bmItem) return; // nur wenn der li selbst fokussiert ist
e.preventDefault();
+8 -4
View File
@@ -48,22 +48,26 @@ const HellionDialog = {
box.className = 'dialog-box';
box.setAttribute('role', config.isConfirm ? 'alertdialog' : 'dialog');
box.setAttribute('aria-modal', 'true');
box.setAttribute('aria-labelledby', 'dialogTitle');
box.setAttribute('aria-describedby', 'dialogBody');
// Eindeutige IDs pro Dialog-Instanz: kurz gestapelte Dialoge (timer.js/
// image-ref.js feuern teils ohne await) duerfen sich keine festen IDs
// teilen, sonst liest der Screenreader ueber aria-* den falschen Titel.
const uid = 'dlg-' + Date.now().toString(36) + '-' + (HellionDialog._seq = (HellionDialog._seq || 0) + 1);
box.setAttribute('aria-labelledby', uid + '-title');
box.setAttribute('aria-describedby', uid + '-body');
// Header
const header = document.createElement('div');
header.className = 'dialog-header';
header.appendChild(this._createIcon(config.type));
const titleSpan = document.createElement('span');
titleSpan.id = 'dialogTitle';
titleSpan.id = uid + '-title';
titleSpan.textContent = config.title;
header.appendChild(titleSpan);
// Body
const body = document.createElement('div');
body.className = 'dialog-body';
body.id = 'dialogBody';
body.id = uid + '-body';
body.textContent = config.message;
// Actions
+4
View File
@@ -18,6 +18,10 @@ function _focusable(container) {
/** Tab/Shift+Tab im Container einfangen + Escape schliesst. */
function _makeTrap(container, closeFn) {
return function trap(e) {
// Ein offener HellionDialog (z.B. Reset-All-Confirm oder BG-URL-Alert aus
// dem Panel) hat Vorrang: sein eigener keydown-Handler uebernimmt Escape/Tab.
// Sonst schloessen beide Listener gleichzeitig und die Dialog-Fokusfalle wird loechrig.
if (document.querySelector('.dialog-overlay')) return;
if (e.key === 'Escape') { e.preventDefault(); closeFn(); return; }
if (e.key !== 'Tab') return;
const items = _focusable(container);