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