audit pass 6: accessibility, ARIA, and keyboard fixes

- JS: fix checkbox/radio required validation using .checked not .value
- JS: guard _cpTrigger.focus() with document.contains() check
- JS: add arrow/Home/End key navigation to tab groups (WCAG 2.1)
- JS: clamp context menu left edge with Math.max(8, ...) to prevent off-screen
- JS: fix wizard _show() to removeAttribute aria-hidden on active step
- HTML: add role="region" + aria-label to notification panel
- HTML: convert Assigned To span+div to label+select with for/id association
- HTML: add role="article" tabindex="0" aria-label to all kanban cards
- HTML: remove aria-hidden="false" anti-pattern from wizard active step
- CSS/HTML/JS: replace aria-hidden="false" show-hook with :not([aria-hidden])
  so open state is represented by absent attribute rather than false value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 19:51:21 -04:00
parent fdcadad23b
commit d08007cdd7
3 changed files with 102 additions and 44 deletions
+34 -32
View File
@@ -123,34 +123,34 @@
<!-- Notifications with badge + dropdown -->
<div class="lt-notif-dropdown-wrap" id="lt-notif-bell">
<button class="lt-btn lt-btn-sm lt-notif-bell-btn" id="lt-notif-bell-btn" aria-label="Open notifications" aria-expanded="false" aria-haspopup="true" style="padding:0 0.6rem;">🔔</button>
<div class="lt-notif-panel" id="lt-notif-panel" aria-hidden="true">
<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>
</div>
<div class="lt-notif-panel-list">
<div class="lt-notif-item lt-notif-item--unread">
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></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">
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></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">
<div class="lt-notif-item lt-notif-item--unread" role="button" tabindex="0">
<span class="lt-notif-dot"></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">
<div class="lt-notif-item" role="button" tabindex="0">
<span class="lt-notif-dot lt-notif-dot--read"></span>
<div class="lt-notif-item-body">
<div class="lt-notif-item-title">Scheduled maintenance completed</div>
@@ -359,9 +359,9 @@
</fieldset>
<div class="lt-filter-group">
<span class="lt-filter-label">Assigned To</span>
<label class="lt-filter-label" for="filter-assigned-to">Assigned To</label>
<div class="lt-form-group">
<select class="lt-select lt-btn-sm">
<select class="lt-select lt-btn-sm" id="filter-assigned-to">
<option value="">All users</option>
<option>operator</option>
<option>admin</option>
@@ -559,19 +559,19 @@
<div class="lt-section-header">Open</div>
<div class="lt-section-body" id="kanban-col-open" style="min-height:60px">
<div class="lt-card lt-mb-md lt-row-p1">
<div class="lt-card lt-mb-md lt-row-p1" role="article" tabindex="0" aria-label="P1 — Storage array link-down, 5m ago, Unassigned">
<div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p1">P1</span>
<span class="lt-dot lt-dot-up"></span>
<span class="lt-p1" aria-hidden="true">P1</span>
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
</div>
<div class="lt-text-sm">Storage array link-down</div>
<div class="lt-text-xs lt-text-muted lt-mt-sm">5m ago · Unassigned</div>
</div>
<div class="lt-card lt-row-p3">
<div class="lt-card lt-row-p3" role="article" tabindex="0" aria-label="P3 — Update node_exporter on micro1, 1d ago, operator">
<div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p3">P3</span>
<span class="lt-dot lt-dot-up"></span>
<span class="lt-p3" aria-hidden="true">P3</span>
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
</div>
<div class="lt-text-sm">Update node_exporter on micro1</div>
<div class="lt-text-xs lt-text-muted lt-mt-sm">1d ago · operator</div>
@@ -586,10 +586,10 @@
<span class="lt-frame-br"></span>
<div class="lt-section-header">Pending</div>
<div class="lt-section-body" id="kanban-col-pending" style="min-height:60px">
<div class="lt-card lt-row-p2">
<div class="lt-card lt-row-p2" role="article" tabindex="0" aria-label="P2 — Scheduled maintenance window, 2d ago, admin">
<div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p2">P2</span>
<span class="lt-dot lt-dot-warn"></span>
<span class="lt-p2" aria-hidden="true">P2</span>
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
</div>
<div class="lt-text-sm">Scheduled maintenance window</div>
<div class="lt-text-xs lt-text-muted lt-mt-sm">2d ago · admin</div>
@@ -603,10 +603,10 @@
<span class="lt-frame-br"></span>
<div class="lt-section-header">In Progress</div>
<div class="lt-section-body" id="kanban-col-inprogress" style="min-height:60px">
<div class="lt-card lt-row-p2 lt-item-running">
<div class="lt-card lt-row-p2 lt-item-running" role="article" tabindex="0" aria-label="P2 — Switch port flapping on USW-Pro, 2h ago, operator">
<div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p2">P2</span>
<span class="lt-dot lt-dot-warn"></span>
<span class="lt-p2" aria-hidden="true">P2</span>
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
</div>
<div class="lt-text-sm">Switch port flapping on USW-Pro</div>
<div class="lt-text-xs lt-text-muted lt-mt-sm">2h ago · operator</div>
@@ -620,10 +620,10 @@
<span class="lt-frame-br"></span>
<div class="lt-section-header">Closed</div>
<div class="lt-section-body" id="kanban-col-closed" style="min-height:60px">
<div class="lt-card lt-row-p4" style="opacity:0.6">
<div class="lt-card lt-row-p4" style="opacity:0.6" role="article" tabindex="0" aria-label="P4 — Update SSL cert on wiki, 3d ago, operator">
<div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p4">P4</span>
<span class="lt-dot lt-dot-idle"></span>
<span class="lt-p4" aria-hidden="true">P4</span>
<span class="lt-dot lt-dot-idle" aria-hidden="true"></span>
</div>
<div class="lt-text-sm">Update SSL cert on wiki</div>
<div class="lt-text-xs lt-text-muted lt-mt-sm">3d ago · operator</div>
@@ -1418,7 +1418,7 @@
<!-- Counter -->
<p class="lt-wizard-counter">Step <strong data-wizard-current>1</strong> of <strong data-wizard-total>3</strong></p>
<!-- Steps -->
<div data-wizard-step="1" class="is-active" aria-hidden="false">
<div data-wizard-step="1" class="is-active">
<div class="lt-grid lt-grid-2" style="gap:1rem;margin-top:1rem">
<div class="lt-form-group"><label class="lt-label">Ticket Title</label><input type="text" name="title" class="lt-input lt-w-full" placeholder="Brief description…"></div>
<div class="lt-form-group"><label class="lt-label">Priority</label><select name="priority" class="lt-select lt-w-full"><option value="p1">P1 Critical</option><option value="p2">P2 High</option><option value="p3" selected>P3 Medium</option><option value="p4">P4 Low</option></select></div>
@@ -1876,7 +1876,7 @@ Storage array link-down on `compute-storage-01`.
if (!btn || !panel) return;
function open() {
panel.setAttribute('aria-hidden', 'false');
panel.removeAttribute('aria-hidden');
btn.setAttribute('aria-expanded', 'true');
// Mark all as read visually
}
@@ -1887,7 +1887,7 @@ Storage array link-down on `compute-storage-01`.
btn.addEventListener('click', e => {
e.stopPropagation();
panel.getAttribute('aria-hidden') === 'false' ? close() : open();
panel.hasAttribute('aria-hidden') ? open() : close();
});
// "Mark all read" button
@@ -1899,14 +1899,16 @@ Storage array link-down on `compute-storage-01`.
lt.toast.info('All notifications marked as read');
});
// Individual item click
// Individual item click + keyboard activation
panel.querySelectorAll('.lt-notif-item').forEach(item => {
item.addEventListener('click', () => {
const activate = () => {
item.classList.remove('lt-notif-item--unread');
const dot = item.querySelector('.lt-notif-dot');
if (dot) { dot.classList.add('lt-notif-dot--read'); }
close();
});
};
item.addEventListener('click', activate);
item.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(); } });
});
// Close on outside click
@@ -1926,15 +1928,15 @@ Storage array link-down on `compute-storage-01`.
btn.addEventListener('click', e => {
e.stopPropagation();
const isOpen = panel.getAttribute('aria-hidden') === 'false';
const isOpen = !panel.hasAttribute('aria-hidden');
// Close all other dropdowns first
document.querySelectorAll('.lt-dropdown-panel[aria-hidden="false"]').forEach(p => {
document.querySelectorAll('.lt-dropdown-panel:not([aria-hidden])').forEach(p => {
p.setAttribute('aria-hidden', 'true');
const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger');
if (t) t.setAttribute('aria-expanded', 'false');
});
if (!isOpen) {
panel.setAttribute('aria-hidden', 'false');
panel.removeAttribute('aria-hidden');
btn.setAttribute('aria-expanded', 'true');
}
});
@@ -1942,7 +1944,7 @@ Storage array link-down on `compute-storage-01`.
// Close dropdowns on outside click / Esc
document.addEventListener('click', () => {
document.querySelectorAll('.lt-dropdown-panel[aria-hidden="false"]').forEach(p => {
document.querySelectorAll('.lt-dropdown-panel:not([aria-hidden])').forEach(p => {
p.setAttribute('aria-hidden', 'true');
const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger');
if (t) t.setAttribute('aria-expanded', 'false');