fix(palette): Review-Befunde — Close-Crash-Guard, Self-Block-Race, ARIA-Combobox, URL-Protokoll-Guard
This commit is contained in:
+22
-8
@@ -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-<li> 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');
|
||||
|
||||
Reference in New Issue
Block a user