diff --git a/src/js/palette.js b/src/js/palette.js index 995914e..54002cb 100644 --- a/src/js/palette.js +++ b/src/js/palette.js @@ -16,8 +16,11 @@ function initPalette() { // Deckt Settings (.panel-overlay), Theme/Add-Board/Add-Bookmark/Rename // (.modal-overlay) sowie HellionDialog UND Onboarding (.dialog-overlay) ab. function isBlocked() { + // .palette-overlay ausklammern: das eigene (beim Schliessen noch deferred .active + // tragende) Overlay darf den Reopen-Guard nicht selbst blockieren (Self-Block-Race + // beim Toggle-Spam, da close() .active erst in withViewTransition entfernt). return !!document.querySelector( - '.panel-overlay.active, .modal-overlay.active, .dialog-overlay.active' + '.panel-overlay.active, .modal-overlay.active, .dialog-overlay.active:not(.palette-overlay)' ); } @@ -48,6 +51,11 @@ function initPalette() { // ---- Treffer oeffnen (wie boards.js:270) ---- function openResult(item) { if (!item || !item.url) return; + // Sicherheit: nur sichere Protokolle oeffnen. Verhindert javascript:/data:-URLs aus + // importierten Bookmarks (XSS im Extension-Origin, besonders bei _self). http/https/ftp only. + let safe = false; + try { safe = ['http:', 'https:', 'ftp:'].includes(new URL(item.url).protocol); } catch (e) { /* ungueltige URL */ } + if (!safe) { close(); return; } window.open(item.url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer'); close(); } @@ -58,11 +66,11 @@ function initPalette() { activeIndex = results.length ? 0 : -1; if (results.length === 0) { + // Kein role="option": der Leerzustand ist keine auswaehlbare Option, sondern eine + // Statuszeile. Die Ansage uebernimmt die aria-live-Region (liveEl) unten. const empty = document.createElement('li'); empty.className = 'palette-empty'; - empty.id = 'palette-opt-empty'; - empty.setAttribute('role', 'option'); - empty.setAttribute('aria-disabled', 'true'); + empty.setAttribute('role', 'presentation'); empty.textContent = t('palette.no_results'); listEl.appendChild(empty); liveEl.textContent = t('palette.no_results'); @@ -97,6 +105,10 @@ function initPalette() { // ---- aktiven Eintrag setzen (aria-activedescendant + aria-selected) ---- function setActive(listEl, idx) { + // Guard: close() nullt overlay synchron, das DOM-Removal laeuft aber deferred in + // withViewTransition. In diesem Frame-Fenster kann ein mousemove auf einem noch + // lebenden Treffer-
  • setActive() ausloesen -> ohne diesen Guard Null-Deref auf overlay. + if (!overlay) return; const options = listEl.querySelectorAll('.palette-option'); if (options.length === 0) return; activeIndex = Math.max(0, Math.min(idx, options.length - 1)); @@ -133,14 +145,16 @@ function initPalette() { const box = document.createElement('div'); box.className = 'palette-box'; - box.setAttribute('role', 'combobox'); - box.setAttribute('aria-haspopup', 'listbox'); - box.setAttribute('aria-expanded', 'true'); + box.setAttribute('role', 'none'); + // ARIA-1.2-Combobox: role=combobox gehoert auf das fokussierbare Textfeld selbst, + // nicht auf die Huelle. aria-expanded/haspopup/controls/activedescendant ebenfalls hier. const input = document.createElement('input'); input.className = 'palette-input'; input.type = 'text'; - input.setAttribute('role', 'searchbox'); + input.setAttribute('role', 'combobox'); + input.setAttribute('aria-expanded', 'true'); + input.setAttribute('aria-haspopup', 'listbox'); input.setAttribute('aria-label', t('palette.aria_label')); input.setAttribute('aria-autocomplete', 'list'); input.setAttribute('aria-controls', 'palette-listbox');