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
+46 -6
View File
@@ -1792,6 +1792,7 @@ select option:checked {
transition: transform 0.2s ease, opacity 0.2s ease; transition: transform 0.2s ease, opacity 0.2s ease;
box-shadow: var(--glow-cyan); box-shadow: var(--glow-cyan);
} }
.lt-menu-btn:active { opacity: 0.7; }
.lt-menu-btn:focus-visible { outline: 1px solid var(--accent-cyan); outline-offset: 2px; } .lt-menu-btn:focus-visible { outline: 1px solid var(--accent-cyan); outline-offset: 2px; }
.lt-menu-btn.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); } .lt-menu-btn.open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.lt-menu-btn.open span:nth-child(2) { opacity: 0; } .lt-menu-btn.open span:nth-child(2) { opacity: 0; }
@@ -2472,7 +2473,9 @@ select option:checked {
transform: translateX(-50%) translateY(4px); transform: translateX(-50%) translateY(4px);
} }
[data-tooltip]:hover::before, [data-tooltip]:hover::before,
[data-tooltip]:hover::after { [data-tooltip]:focus-visible::before,
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after {
opacity: 1; opacity: 1;
transform: translateX(-50%) translateY(0); transform: translateX(-50%) translateY(0);
} }
@@ -2490,7 +2493,9 @@ select option:checked {
transform: translateX(-50%) translateY(-4px); transform: translateX(-50%) translateY(-4px);
} }
[data-tooltip][data-tooltip-pos="bottom"]:hover::before, [data-tooltip][data-tooltip-pos="bottom"]:hover::before,
[data-tooltip][data-tooltip-pos="bottom"]:hover::after { [data-tooltip][data-tooltip-pos="bottom"]:focus-visible::before,
[data-tooltip][data-tooltip-pos="bottom"]:hover::after,
[data-tooltip][data-tooltip-pos="bottom"]:focus-visible::after {
transform: translateX(-50%) translateY(0); transform: translateX(-50%) translateY(0);
} }
@@ -2551,6 +2556,12 @@ select option:checked {
box-shadow: var(--glow-orange); box-shadow: var(--glow-orange);
font-weight: 700; font-weight: 700;
} }
.lt-page-btn:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 1px;
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.lt-page-btn:disabled, .lt-page-btn:disabled,
.lt-page-btn[aria-disabled="true"] { .lt-page-btn[aria-disabled="true"] {
opacity: 0.35; opacity: 0.35;
@@ -2583,7 +2594,8 @@ select option:checked {
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
.lt-accordion-header:hover { background: var(--bg-tertiary); color: var(--accent-cyan); } .lt-accordion-header:hover { background: var(--bg-tertiary); color: var(--accent-cyan); }
.lt-accordion-header:active { background: var(--bg-tertiary); opacity: 0.8; }
.lt-accordion-header:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; } .lt-accordion-header:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; }
.lt-accordion-header[aria-expanded="true"] { color: var(--accent-orange); } .lt-accordion-header[aria-expanded="true"] { color: var(--accent-orange); }
.lt-accordion-icon { .lt-accordion-icon {
@@ -2641,6 +2653,7 @@ select option:checked {
transition: color 0.15s; transition: color 0.15s;
} }
.lt-alert-close:hover { color: var(--accent-red); } .lt-alert-close:hover { color: var(--accent-red); }
.lt-alert-close:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
.lt-alert.dismissed { max-height: 0 !important; opacity: 0; padding-top: 0; padding-bottom: 0; pointer-events: none; } .lt-alert.dismissed { max-height: 0 !important; opacity: 0; padding-top: 0; padding-bottom: 0; pointer-events: none; }
@@ -2655,7 +2668,21 @@ select option:checked {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.8rem; font-size: 0.8rem;
} }
.lt-toggle input { display: none; } /* Visually hidden but still keyboard-focusable */
.lt-toggle input {
position: absolute;
opacity: 0;
width: 1px; height: 1px;
margin: -1px; padding: 0;
border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.lt-toggle input:focus-visible ~ .lt-toggle-track {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
}
.lt-toggle-track { .lt-toggle-track {
width: 36px; width: 36px;
height: 18px; height: 18px;
@@ -2909,6 +2936,7 @@ input[type="range"].lt-range::-moz-range-thumb {
transition: all 0.15s; transition: all 0.15s;
} }
.lt-code-copy:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); } .lt-code-copy:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); }
.lt-code-copy:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }
.lt-code-copy.copied { border-color: var(--accent-green); color: var(--accent-green); } .lt-code-copy.copied { border-color: var(--accent-green); color: var(--accent-green); }
.lt-code-block pre { .lt-code-block pre {
margin: 0; margin: 0;
@@ -4275,6 +4303,7 @@ html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--acc
white-space: nowrap; white-space: nowrap;
} }
.lt-context-menu-item:hover { background: var(--accent-cyan-dim); color: var(--accent-cyan); } .lt-context-menu-item:hover { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
.lt-context-menu-item:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; background: var(--accent-cyan-dim); color: var(--accent-cyan); }
.lt-context-menu-item.is-danger:hover { background: var(--accent-red-dim); color: var(--accent-red); } .lt-context-menu-item.is-danger:hover { background: var(--accent-red-dim); color: var(--accent-red); }
.lt-context-menu-item .icon { width: 1rem; text-align: center; opacity: 0.7; font-size: 0.75rem; } .lt-context-menu-item .icon { width: 1rem; text-align: center; opacity: 0.7; font-size: 0.75rem; }
.lt-context-menu-item kbd { .lt-context-menu-item kbd {
@@ -4900,6 +4929,9 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
.lt-lightbox-close:hover, .lt-lightbox-close:hover,
.lt-lightbox-prev:hover, .lt-lightbox-prev:hover,
.lt-lightbox-next:hover { color: var(--accent-cyan); border-color: var(--accent-cyan-border); box-shadow: var(--box-glow-cyan); } .lt-lightbox-next:hover { color: var(--accent-cyan); border-color: var(--accent-cyan-border); box-shadow: var(--box-glow-cyan); }
.lt-lightbox-close:focus-visible,
.lt-lightbox-prev:focus-visible,
.lt-lightbox-next:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; color: var(--accent-cyan); }
.lt-lightbox-caption { .lt-lightbox-caption {
position: fixed; position: fixed;
bottom: 3rem; bottom: 3rem;
@@ -4947,6 +4979,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
border-radius: 2px; border-radius: 2px;
} }
.lt-sidebar-group-label:hover { color: var(--text-secondary); } .lt-sidebar-group-label:hover { color: var(--text-secondary); }
.lt-sidebar-group-label:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; border-radius: 2px; }
.lt-sidebar-group-label .chevron { .lt-sidebar-group-label .chevron {
font-size: 0.5rem; font-size: 0.5rem;
transition: transform 0.2s ease; transition: transform 0.2s ease;
@@ -5066,7 +5099,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
transition: opacity 0.15s ease, transform 0.15s ease; transition: opacity 0.15s ease, transform 0.15s ease;
overflow: hidden; overflow: hidden;
} }
.lt-notif-panel[aria-hidden="false"] { .lt-notif-panel:not([aria-hidden]) {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
pointer-events: auto; pointer-events: auto;
@@ -5111,6 +5144,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
transition: background 0.1s; transition: background 0.1s;
} }
.lt-notif-item:hover { background: var(--bg-tertiary); } .lt-notif-item:hover { background: var(--bg-tertiary); }
.lt-notif-item:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; background: var(--bg-tertiary); }
.lt-notif-item--unread { background: rgba(0, 212, 255, 0.04); } .lt-notif-item--unread { background: rgba(0, 212, 255, 0.04); }
.lt-notif-dot { .lt-notif-dot {
@@ -5179,7 +5213,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
right: 0; right: 0;
transform-origin: top right; transform-origin: top right;
} }
.lt-dropdown-panel[aria-hidden="false"] { .lt-dropdown-panel:not([aria-hidden]) {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
pointer-events: auto; pointer-events: auto;
@@ -5206,6 +5240,12 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-primary); color: var(--text-primary);
} }
.lt-dropdown-item:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: -2px;
background: var(--bg-tertiary);
color: var(--text-primary);
}
.lt-dropdown-item--danger { color: var(--accent-red); } .lt-dropdown-item--danger { color: var(--accent-red); }
.lt-dropdown-item--danger:hover { background: rgba(255,45,85,0.1); color: var(--accent-red); } .lt-dropdown-item--danger:hover { background: rgba(255,45,85,0.1); color: var(--accent-red); }
+34 -32
View File
@@ -123,34 +123,34 @@
<!-- Notifications with badge + dropdown --> <!-- Notifications with badge + dropdown -->
<div class="lt-notif-dropdown-wrap" id="lt-notif-bell"> <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> <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"> <div class="lt-notif-panel-header">
<span>Notifications</span> <span>Notifications</span>
<button class="lt-notif-panel-clear" id="lt-notif-clear-all">Mark all read</button> <button class="lt-notif-panel-clear" id="lt-notif-clear-all">Mark all read</button>
</div> </div>
<div class="lt-notif-panel-list"> <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> <span class="lt-notif-dot"></span>
<div class="lt-notif-item-body"> <div class="lt-notif-item-body">
<div class="lt-notif-item-title">P1 alert: storage link-down</div> <div class="lt-notif-item-title">P1 alert: storage link-down</div>
<div class="lt-notif-item-time">5 min ago</div> <div class="lt-notif-item-time">5 min ago</div>
</div> </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> <span class="lt-notif-dot"></span>
<div class="lt-notif-item-body"> <div class="lt-notif-item-body">
<div class="lt-notif-item-title">Worker node-03 reconnected</div> <div class="lt-notif-item-title">Worker node-03 reconnected</div>
<div class="lt-notif-item-time">12 min ago</div> <div class="lt-notif-item-time">12 min ago</div>
</div> </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> <span class="lt-notif-dot"></span>
<div class="lt-notif-item-body"> <div class="lt-notif-item-body">
<div class="lt-notif-item-title">Export CSV complete — 42 rows</div> <div class="lt-notif-item-title">Export CSV complete — 42 rows</div>
<div class="lt-notif-item-time">1 hr ago</div> <div class="lt-notif-item-time">1 hr ago</div>
</div> </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> <span class="lt-notif-dot lt-notif-dot--read"></span>
<div class="lt-notif-item-body"> <div class="lt-notif-item-body">
<div class="lt-notif-item-title">Scheduled maintenance completed</div> <div class="lt-notif-item-title">Scheduled maintenance completed</div>
@@ -359,9 +359,9 @@
</fieldset> </fieldset>
<div class="lt-filter-group"> <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"> <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 value="">All users</option>
<option>operator</option> <option>operator</option>
<option>admin</option> <option>admin</option>
@@ -559,19 +559,19 @@
<div class="lt-section-header">Open</div> <div class="lt-section-header">Open</div>
<div class="lt-section-body" id="kanban-col-open" style="min-height:60px"> <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"> <div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p1">P1</span> <span class="lt-p1" aria-hidden="true">P1</span>
<span class="lt-dot lt-dot-up"></span> <span class="lt-dot lt-dot-up" aria-hidden="true"></span>
</div> </div>
<div class="lt-text-sm">Storage array link-down</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 class="lt-text-xs lt-text-muted lt-mt-sm">5m ago · Unassigned</div>
</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"> <div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p3">P3</span> <span class="lt-p3" aria-hidden="true">P3</span>
<span class="lt-dot lt-dot-up"></span> <span class="lt-dot lt-dot-up" aria-hidden="true"></span>
</div> </div>
<div class="lt-text-sm">Update node_exporter on micro1</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> <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> <span class="lt-frame-br"></span>
<div class="lt-section-header">Pending</div> <div class="lt-section-header">Pending</div>
<div class="lt-section-body" id="kanban-col-pending" style="min-height:60px"> <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"> <div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p2">P2</span> <span class="lt-p2" aria-hidden="true">P2</span>
<span class="lt-dot lt-dot-warn"></span> <span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
</div> </div>
<div class="lt-text-sm">Scheduled maintenance window</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> <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> <span class="lt-frame-br"></span>
<div class="lt-section-header">In Progress</div> <div class="lt-section-header">In Progress</div>
<div class="lt-section-body" id="kanban-col-inprogress" style="min-height:60px"> <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"> <div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p2">P2</span> <span class="lt-p2" aria-hidden="true">P2</span>
<span class="lt-dot lt-dot-warn"></span> <span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
</div> </div>
<div class="lt-text-sm">Switch port flapping on USW-Pro</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> <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> <span class="lt-frame-br"></span>
<div class="lt-section-header">Closed</div> <div class="lt-section-header">Closed</div>
<div class="lt-section-body" id="kanban-col-closed" style="min-height:60px"> <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"> <div class="lt-flex lt-flex-between lt-mb-md">
<span class="lt-p4">P4</span> <span class="lt-p4" aria-hidden="true">P4</span>
<span class="lt-dot lt-dot-idle"></span> <span class="lt-dot lt-dot-idle" aria-hidden="true"></span>
</div> </div>
<div class="lt-text-sm">Update SSL cert on wiki</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> <div class="lt-text-xs lt-text-muted lt-mt-sm">3d ago · operator</div>
@@ -1418,7 +1418,7 @@
<!-- Counter --> <!-- Counter -->
<p class="lt-wizard-counter">Step <strong data-wizard-current>1</strong> of <strong data-wizard-total>3</strong></p> <p class="lt-wizard-counter">Step <strong data-wizard-current>1</strong> of <strong data-wizard-total>3</strong></p>
<!-- Steps --> <!-- 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-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">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> <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; if (!btn || !panel) return;
function open() { function open() {
panel.setAttribute('aria-hidden', 'false'); panel.removeAttribute('aria-hidden');
btn.setAttribute('aria-expanded', 'true'); btn.setAttribute('aria-expanded', 'true');
// Mark all as read visually // Mark all as read visually
} }
@@ -1887,7 +1887,7 @@ Storage array link-down on `compute-storage-01`.
btn.addEventListener('click', e => { btn.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
panel.getAttribute('aria-hidden') === 'false' ? close() : open(); panel.hasAttribute('aria-hidden') ? open() : close();
}); });
// "Mark all read" button // "Mark all read" button
@@ -1899,14 +1899,16 @@ Storage array link-down on `compute-storage-01`.
lt.toast.info('All notifications marked as read'); lt.toast.info('All notifications marked as read');
}); });
// Individual item click // Individual item click + keyboard activation
panel.querySelectorAll('.lt-notif-item').forEach(item => { panel.querySelectorAll('.lt-notif-item').forEach(item => {
item.addEventListener('click', () => { const activate = () => {
item.classList.remove('lt-notif-item--unread'); item.classList.remove('lt-notif-item--unread');
const dot = item.querySelector('.lt-notif-dot'); const dot = item.querySelector('.lt-notif-dot');
if (dot) { dot.classList.add('lt-notif-dot--read'); } if (dot) { dot.classList.add('lt-notif-dot--read'); }
close(); close();
}); };
item.addEventListener('click', activate);
item.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(); } });
}); });
// Close on outside click // Close on outside click
@@ -1926,15 +1928,15 @@ Storage array link-down on `compute-storage-01`.
btn.addEventListener('click', e => { btn.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
const isOpen = panel.getAttribute('aria-hidden') === 'false'; const isOpen = !panel.hasAttribute('aria-hidden');
// Close all other dropdowns first // 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'); p.setAttribute('aria-hidden', 'true');
const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger'); const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger');
if (t) t.setAttribute('aria-expanded', 'false'); if (t) t.setAttribute('aria-expanded', 'false');
}); });
if (!isOpen) { if (!isOpen) {
panel.setAttribute('aria-hidden', 'false'); panel.removeAttribute('aria-hidden');
btn.setAttribute('aria-expanded', 'true'); btn.setAttribute('aria-expanded', 'true');
} }
}); });
@@ -1942,7 +1944,7 @@ Storage array link-down on `compute-storage-01`.
// Close dropdowns on outside click / Esc // Close dropdowns on outside click / Esc
document.addEventListener('click', () => { 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'); p.setAttribute('aria-hidden', 'true');
const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger'); const t = p.closest('.lt-dropdown-wrap').querySelector('.lt-dropdown-trigger');
if (t) t.setAttribute('aria-expanded', 'false'); if (t) t.setAttribute('aria-expanded', 'false');
+22 -6
View File
@@ -282,8 +282,19 @@
const saved = localStorage.getItem('lt_activeTab_' + location.pathname); const saved = localStorage.getItem('lt_activeTab_' + location.pathname);
if (saved && document.getElementById(saved)) { switchTab(saved); } if (saved && document.getElementById(saved)) { switchTab(saved); }
} catch (_) {} } catch (_) {}
document.querySelectorAll('.lt-tab[data-tab]').forEach(btn => { document.querySelectorAll('[role="tablist"]').forEach(tablist => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab)); const btns = Array.from(tablist.querySelectorAll('.lt-tab[data-tab]'));
btns.forEach((btn, i) => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
btn.addEventListener('keydown', e => {
let idx = -1;
if (e.key === 'ArrowRight') idx = (i + 1) % btns.length;
else if (e.key === 'ArrowLeft') idx = (i - 1 + btns.length) % btns.length;
else if (e.key === 'Home') idx = 0;
else if (e.key === 'End') idx = btns.length - 1;
if (idx >= 0) { e.preventDefault(); btns[idx].focus(); switchTab(btns[idx].dataset.tab); }
});
});
}); });
} }
@@ -849,7 +860,7 @@
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return; const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
ov.classList.remove('is-open'); ov.classList.remove('is-open');
_unlockScroll(); _unlockScroll();
if (_cpTrigger) { _cpTrigger.focus(); _cpTrigger = null; } if (_cpTrigger) { if (document.contains(_cpTrigger)) _cpTrigger.focus(); _cpTrigger = null; }
} }
function _cpHighlight(text, q) { function _cpHighlight(text, q) {
@@ -970,6 +981,7 @@
function _validateField(el) { function _validateField(el) {
const val = el.value || '', type = (el.type || '').toLowerCase(); const val = el.value || '', type = (el.type || '').toLowerCase();
if ((type === 'checkbox' || type === 'radio') && el.required && !el.checked) return { valid: false, message: 'This field is required' };
if (el.required && !val.trim()) return { valid: false, message: 'This field is required' }; if (el.required && !val.trim()) return { valid: false, message: 'This field is required' };
if (el.minLength > 0 && val.length < el.minLength) return { valid: false, message: 'Minimum ' + el.minLength + ' characters' }; if (el.minLength > 0 && val.length < el.minLength) return { valid: false, message: 'Minimum ' + el.minLength + ' characters' };
if (el.maxLength > 0 && val.length > el.maxLength) return { valid: false, message: 'Maximum ' + el.maxLength + ' characters' }; if (el.maxLength > 0 && val.length > el.maxLength) return { valid: false, message: 'Maximum ' + el.maxLength + ' characters' };
@@ -1663,7 +1675,7 @@
// Position — keep on screen // Position — keep on screen
const vw = window.innerWidth, vh = window.innerHeight; const vw = window.innerWidth, vh = window.innerHeight;
const mw = _ctxMenu.offsetWidth || 180, mh = _ctxMenu.offsetHeight || 200; const mw = _ctxMenu.offsetWidth || 180, mh = _ctxMenu.offsetHeight || 200;
_ctxMenu.style.left = Math.min(x, vw - mw - 8) + 'px'; _ctxMenu.style.left = Math.max(8, Math.min(x, vw - mw - 8)) + 'px';
_ctxMenu.style.top = Math.min(y, vh - mh - 8) + 'px'; _ctxMenu.style.top = Math.min(y, vh - mh - 8) + 'px';
// Focus first item // Focus first item
const first = _ctxMenu.querySelector('[role="menuitem"]'); const first = _ctxMenu.querySelector('[role="menuitem"]');
@@ -2110,7 +2122,11 @@
group._sbInit = true; group._sbInit = true;
const label = group.querySelector('.lt-sidebar-group-label'); const label = group.querySelector('.lt-sidebar-group-label');
if (!label) return; if (!label) return;
label.addEventListener('click', () => group.classList.toggle('is-open')); label.setAttribute('tabindex', '0');
label.setAttribute('role', 'button');
const _toggle = () => group.classList.toggle('is-open');
label.addEventListener('click', _toggle);
label.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _toggle(); } });
// Open group if it contains the active link // Open group if it contains the active link
if (group.querySelector('.lt-sidebar-sub-link.active, .lt-sidebar-sub-link[aria-current="page"]')) { if (group.querySelector('.lt-sidebar-sub-link.active, .lt-sidebar-sub-link[aria-current="page"]')) {
group.classList.add('is-open'); group.classList.add('is-open');
@@ -2210,7 +2226,7 @@
function _show(idx) { function _show(idx) {
steps.forEach((s, i) => { steps.forEach((s, i) => {
s.classList.toggle('is-active', i === idx); s.classList.toggle('is-active', i === idx);
s.setAttribute('aria-hidden', i !== idx ? 'true' : 'false'); if (i !== idx) s.setAttribute('aria-hidden', 'true'); else s.removeAttribute('aria-hidden');
}); });
// Update step indicators // Update step indicators
container.querySelectorAll('[data-wizard-indicator]').forEach((ind, i) => { container.querySelectorAll('[data-wizard-indicator]').forEach((ind, i) => {