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
|
// 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');
|
||||||
|
|||||||
Reference in New Issue
Block a user