audit pass 9: XSS fix, focus management, ARIA labels, and :focus-visible gaps

JS:
- Lightbox: remove keydown listener on close (memory leak fix)
- Lightbox: restore focus to trigger on close
- Right drawer: fix aria-hidden="false" anti-pattern to removeAttribute
- Markdown renderer: block javascript:/data: protocol URIs in link and
  image replacements to prevent XSS
- Sidebar submenus: add aria-expanded tracking on toggle; hide decorative
  chevron from screen readers; initialize aria-expanded on mount

CSS:
- Add :focus-visible to .lt-sidebar-toggle (interactive button)
- Add :focus-visible to .lt-dropzone (focusable container)
- Fix .lt-stat-card:focus-visible outline-offset to -2px (clip-path clips it)
- Add light theme override for .lt-nav-drawer-link:focus-visible
- Adjust .lt-split-divider:focus-visible outline-offset to 3px

HTML:
- Range input: update aria-valuenow dynamically on input event
- Combobox label: add for="demo-combobox-input" association
- Typeahead label: add for="demo-typeahead-input" association
- Dropzone file input: add aria-label
- Notification items: add descriptive aria-label to all 4 items;
  add aria-hidden="true" to decorative dot spans
- Mark all read button: add type="button" to prevent accidental form submit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 20:16:12 -04:00
parent e8c1197613
commit 8b54efef61
3 changed files with 38 additions and 21 deletions
+13 -13
View File
@@ -126,32 +126,32 @@
<div class="lt-notif-panel" id="lt-notif-panel" role="region" aria-label="Notifications" aria-hidden="true">
<div class="lt-notif-panel-header">
<span>Notifications</span>
<button class="lt-notif-panel-clear" id="lt-notif-clear-all">Mark all read</button>
<button type="button" class="lt-notif-panel-clear" id="lt-notif-clear-all">Mark all read</button>
</div>
<div class="lt-notif-panel-list">
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></span>
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0" aria-label="Unread: P1 alert: storage link-down, 5 min ago. Press Enter to dismiss.">
<span class="lt-notif-dot" aria-hidden="true"></span>
<div class="lt-notif-item-body">
<div class="lt-notif-item-title">P1 alert: storage link-down</div>
<div class="lt-notif-item-time">5 min ago</div>
</div>
</div>
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></span>
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0" aria-label="Unread: Worker node-03 reconnected, 12 min ago. Press Enter to dismiss.">
<span class="lt-notif-dot" aria-hidden="true"></span>
<div class="lt-notif-item-body">
<div class="lt-notif-item-title">Worker node-03 reconnected</div>
<div class="lt-notif-item-time">12 min ago</div>
</div>
</div>
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></span>
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0" aria-label="Unread: Export CSV complete — 42 rows, 1 hr ago. Press Enter to dismiss.">
<span class="lt-notif-dot" aria-hidden="true"></span>
<div class="lt-notif-item-body">
<div class="lt-notif-item-title">Export CSV complete — 42 rows</div>
<div class="lt-notif-item-time">1 hr ago</div>
</div>
</div>
<div class="lt-notif-item" role="button" tabindex="0">
<span class="lt-notif-dot lt-notif-dot--read"></span>
<div class="lt-notif-item" role="button" tabindex="0" aria-label="Scheduled maintenance completed, 3 hr ago. Press Enter to dismiss.">
<span class="lt-notif-dot lt-notif-dot--read" aria-hidden="true"></span>
<div class="lt-notif-item-body">
<div class="lt-notif-item-title">Scheduled maintenance completed</div>
<div class="lt-notif-item-time">3 hr ago</div>
@@ -1098,7 +1098,7 @@
<div class="lt-section-header">File Dropzone</div>
<div class="lt-section-body">
<div class="lt-dropzone" data-dropzone data-accept=".json,.csv,.log" data-max-size="10485760">
<input type="file" accept=".json,.csv,.log" multiple>
<input type="file" accept=".json,.csv,.log" multiple aria-label="Upload JSON, CSV, or log files (max 10 MB each)">
<div class="lt-dropzone-icon"></div>
<div class="lt-dropzone-text">Drop files here or <strong>click to browse</strong></div>
<div class="lt-dropzone-hint">Accepts .json, .csv, .log — max 10 MB each</div>
@@ -1284,7 +1284,7 @@
<div class="lt-section-body">
<div class="lt-grid lt-grid-2" style="gap:1.5rem">
<div>
<label class="lt-label">Assign Workers (multi-select)</label>
<label class="lt-label" for="demo-combobox-input">Assign Workers (multi-select)</label>
<div class="lt-combobox" id="demo-combobox">
<div class="lt-combobox-input-wrap">
<input type="text" class="lt-combobox-input" id="demo-combobox-input" placeholder="Search workers…" autocomplete="off">
@@ -1293,7 +1293,7 @@
</div>
</div>
<div>
<label class="lt-label">Search Tickets (typeahead)</label>
<label class="lt-label" for="demo-typeahead-input">Search Tickets (typeahead)</label>
<div class="lt-typeahead" id="demo-typeahead">
<input type="text" class="lt-input lt-w-full" id="demo-typeahead-input" placeholder="Type to search…" autocomplete="off">
<div class="lt-typeahead-dropdown"></div>
@@ -1750,7 +1750,7 @@ Storage array link-down on `compute-storage-01`.
const label = wrap && wrap.querySelector('.lt-range-value');
if (label) {
label.textContent = r.value;
r.addEventListener('input', () => { label.textContent = r.value; });
r.addEventListener('input', () => { label.textContent = r.value; r.setAttribute('aria-valuenow', r.value); });
}
});