fix(palette): Review-Befunde — Close-Crash-Guard, Self-Block-Race, ARIA-Combobox, URL-Protokoll-Guard

This commit is contained in:
2026-06-14 09:42:00 +02:00
parent 6eaa3457d0
commit fcaea64604
+22 -8
View File
@@ -16,8 +16,11 @@ function initPalette() {
// Deckt Settings (.panel-overlay), Theme/Add-Board/Add-Bookmark/Rename // Deckt Settings (.panel-overlay), Theme/Add-Board/Add-Bookmark/Rename
// (.modal-overlay) sowie HellionDialog UND Onboarding (.dialog-overlay) ab. // (.modal-overlay) sowie HellionDialog UND Onboarding (.dialog-overlay) ab.
function isBlocked() { 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( 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) ---- // ---- Treffer oeffnen (wie boards.js:270) ----
function openResult(item) { function openResult(item) {
if (!item || !item.url) return; 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'); window.open(item.url, settings.newTab ? '_blank' : '_self', 'noopener,noreferrer');
close(); close();
} }
@@ -58,11 +66,11 @@ function initPalette() {
activeIndex = results.length ? 0 : -1; activeIndex = results.length ? 0 : -1;
if (results.length === 0) { 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'); const empty = document.createElement('li');
empty.className = 'palette-empty'; empty.className = 'palette-empty';
empty.id = 'palette-opt-empty'; empty.setAttribute('role', 'presentation');
empty.setAttribute('role', 'option');
empty.setAttribute('aria-disabled', 'true');
empty.textContent = t('palette.no_results'); empty.textContent = t('palette.no_results');
listEl.appendChild(empty); listEl.appendChild(empty);
liveEl.textContent = t('palette.no_results'); liveEl.textContent = t('palette.no_results');
@@ -97,6 +105,10 @@ function initPalette() {
// ---- aktiven Eintrag setzen (aria-activedescendant + aria-selected) ---- // ---- aktiven Eintrag setzen (aria-activedescendant + aria-selected) ----
function setActive(listEl, idx) { 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-<li> setActive() ausloesen -> ohne diesen Guard Null-Deref auf overlay.
if (!overlay) return;
const options = listEl.querySelectorAll('.palette-option'); const options = listEl.querySelectorAll('.palette-option');
if (options.length === 0) return; if (options.length === 0) return;
activeIndex = Math.max(0, Math.min(idx, options.length - 1)); activeIndex = Math.max(0, Math.min(idx, options.length - 1));
@@ -133,14 +145,16 @@ function initPalette() {
const box = document.createElement('div'); const box = document.createElement('div');
box.className = 'palette-box'; box.className = 'palette-box';
box.setAttribute('role', 'combobox'); box.setAttribute('role', 'none');
box.setAttribute('aria-haspopup', 'listbox');
box.setAttribute('aria-expanded', 'true');
// 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'); const input = document.createElement('input');
input.className = 'palette-input'; input.className = 'palette-input';
input.type = 'text'; 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-label', t('palette.aria_label'));
input.setAttribute('aria-autocomplete', 'list'); input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-controls', 'palette-listbox'); input.setAttribute('aria-controls', 'palette-listbox');