audit pass 7: ARIA, focus management, and label fixes
CSS: - Add :focus-visible to sortable th, breadcrumb links, list links, cmd-item, combobox-option, typeahead-item, sidebar-sub-link, split-divider, stat-card - Fix .lt-skip-link:focus to also include :focus-visible for spec compliance JS: - Mobile nav: add focus trap (_trapFocus), save/restore trigger focus, fix aria-hidden="false" to removeAttribute pattern, add document.contains guard - Combobox: add aria-activedescendant on _moveFocus; add unique IDs to options; clear aria-activedescendant on close; wrap querySelectorAll in Array.from - Typeahead: add aria-activedescendant on _moveFocus; add unique IDs to items; add aria-busy during async search; clear aria-activedescendant on select; wrap querySelectorAll in Array.from - Command palette: add unique IDs to items; set/clear aria-activedescendant on move and mouseenter; clear on close - Lightbox: add document.contains guard on focus setTimeout - Stats filter: add Enter/Space keyboard handler for role="button" cards HTML: - Stat cards: add role="button" tabindex="0" aria-label (interactive divs) - Advanced filter selects: add id/for associations to all 3 label+select pairs - Accordion SVG icons: add aria-hidden="true" (decorative) - Range input: add aria-label, aria-valuemin/max/now - Wizard form controls: add id/for to all 4 label+input/select pairs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1058,6 +1058,7 @@ select option:checked {
|
||||
color: var(--accent-orange);
|
||||
text-shadow: var(--glow-orange);
|
||||
}
|
||||
.lt-table th[data-sort-key]:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; }
|
||||
|
||||
.lt-table td {
|
||||
padding: 0.55rem 0.85rem;
|
||||
@@ -1495,11 +1496,13 @@ select option:checked {
|
||||
}
|
||||
|
||||
.lt-stat-card:hover,
|
||||
.lt-stat-card.active {
|
||||
.lt-stat-card.active,
|
||||
.lt-stat-card:focus-visible {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-cyan-border);
|
||||
box-shadow: var(--box-glow-cyan);
|
||||
}
|
||||
.lt-stat-card:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }
|
||||
.lt-stat-card:hover::before,
|
||||
.lt-stat-card.active::before { height: 100%; }
|
||||
|
||||
@@ -2306,7 +2309,7 @@ select option:checked {
|
||||
z-index: var(--z-toast);
|
||||
transition: top 0.1s;
|
||||
}
|
||||
.lt-skip-link:focus { top: var(--space-sm); }
|
||||
.lt-skip-link:focus, .lt-skip-link:focus-visible { top: var(--space-sm); }
|
||||
|
||||
.lt-sr-only {
|
||||
position: absolute;
|
||||
@@ -2519,6 +2522,7 @@ select option:checked {
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.lt-breadcrumb-item a:hover { color: var(--accent-cyan); }
|
||||
.lt-breadcrumb-item a:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
|
||||
.lt-breadcrumb-item.active { color: var(--accent-orange); }
|
||||
.lt-breadcrumb-sep { color: var(--border-dim); }
|
||||
.lt-breadcrumb-sep::before { content: '/'; }
|
||||
@@ -2867,7 +2871,8 @@ input[type="range"].lt-range::-moz-range-thumb {
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.lt-cmd-item:hover,
|
||||
.lt-cmd-item.is-selected {
|
||||
.lt-cmd-item.is-selected,
|
||||
.lt-cmd-item:focus-visible {
|
||||
background: rgba(0,212,255,0.08);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
@@ -3115,6 +3120,7 @@ input[type="range"].lt-range::-moz-range-thumb {
|
||||
flex: 1;
|
||||
}
|
||||
.lt-list-item a:hover { color: var(--accent-cyan); }
|
||||
.lt-list-item a:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; border-radius: 2px; }
|
||||
.lt-list-item-meta { color: var(--text-dim); font-size: 0.7rem; margin-left: auto; }
|
||||
.lt-list-item--active { background: rgba(255,107,0,0.06); border-left: 2px solid var(--accent-orange); }
|
||||
|
||||
@@ -4258,7 +4264,8 @@ html[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: var(--acc
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.lt-combobox-option:hover,
|
||||
.lt-combobox-option.is-focused { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
.lt-combobox-option.is-focused,
|
||||
.lt-combobox-option:focus-visible { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
.lt-combobox-option.is-selected::before {
|
||||
content: '✓';
|
||||
color: var(--accent-green);
|
||||
@@ -4515,6 +4522,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
||||
}
|
||||
.lt-split-divider:hover,
|
||||
.lt-split-divider.is-dragging { background: var(--accent-cyan); }
|
||||
.lt-split-divider:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }
|
||||
.lt-split--vertical .lt-split-divider::before { top: -6px; bottom: -6px; left: 0; right: 0; cursor: row-resize; }
|
||||
.lt-split--vertical .lt-split-divider { cursor: row-resize; }
|
||||
/* On mobile, stack vertically and hide divider */
|
||||
@@ -4659,7 +4667,8 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.lt-typeahead-item:hover,
|
||||
.lt-typeahead-item.is-focused { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
.lt-typeahead-item.is-focused,
|
||||
.lt-typeahead-item:focus-visible { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
.lt-typeahead-item mark {
|
||||
background: none;
|
||||
color: var(--accent-orange);
|
||||
@@ -5010,6 +5019,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lt-sidebar-sub-link:hover { color: var(--accent-cyan); background: var(--accent-cyan-dim); }
|
||||
.lt-sidebar-sub-link:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; border-radius: 2px; }
|
||||
.lt-sidebar-sub-link.active,
|
||||
.lt-sidebar-sub-link[aria-current="page"] {
|
||||
color: var(--accent-orange);
|
||||
|
||||
@@ -276,29 +276,29 @@
|
||||
data-filter-key / data-filter-val → wired by lt.statsFilter
|
||||
========================================================== -->
|
||||
<div class="lt-stats-grid">
|
||||
<div class="lt-stat-card active" data-filter-key="status" data-filter-val="Open">
|
||||
<span class="lt-stat-icon">📋</span>
|
||||
<div class="lt-stat-card active" role="button" tabindex="0" data-filter-key="status" data-filter-val="Open" aria-label="Open tickets: 42">
|
||||
<span class="lt-stat-icon" aria-hidden="true">📋</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value">42</span>
|
||||
<span class="lt-stat-label">Open</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" data-filter-key="priority" data-filter-val="1">
|
||||
<span class="lt-stat-icon">🔴</span>
|
||||
<div class="lt-stat-card" role="button" tabindex="0" data-filter-key="priority" data-filter-val="1" aria-label="Critical tickets: 3">
|
||||
<span class="lt-stat-icon" aria-hidden="true">🔴</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value">3</span>
|
||||
<span class="lt-stat-label">Critical</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" data-filter-key="assigned_to" data-filter-val="0">
|
||||
<span class="lt-stat-icon">👤</span>
|
||||
<div class="lt-stat-card" role="button" tabindex="0" data-filter-key="assigned_to" data-filter-val="0" aria-label="Unassigned tickets: 11">
|
||||
<span class="lt-stat-icon" aria-hidden="true">👤</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value">11</span>
|
||||
<span class="lt-stat-label">Unassigned</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card" data-filter-key="created" data-filter-val="today">
|
||||
<span class="lt-stat-icon">📅</span>
|
||||
<div class="lt-stat-card" role="button" tabindex="0" data-filter-key="created" data-filter-val="today" aria-label="Today's tickets: 7">
|
||||
<span class="lt-stat-icon" aria-hidden="true">📅</span>
|
||||
<div class="lt-stat-info">
|
||||
<span class="lt-stat-value">7</span>
|
||||
<span class="lt-stat-label">Today</span>
|
||||
@@ -392,8 +392,8 @@
|
||||
<div class="lt-dropdown-panel" id="adv-filter-panel" aria-hidden="true">
|
||||
<div style="padding:0.75rem;display:grid;gap:0.5rem;width:clamp(200px,60vw,260px)">
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" style="font-size:0.75rem">Status</label>
|
||||
<select class="lt-select" style="font-size:0.8rem">
|
||||
<label class="lt-label" style="font-size:0.75rem" for="adv-filter-status">Status</label>
|
||||
<select id="adv-filter-status" class="lt-select" style="font-size:0.8rem">
|
||||
<option value="">All</option>
|
||||
<option>Open</option>
|
||||
<option>In Progress</option>
|
||||
@@ -402,8 +402,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" style="font-size:0.75rem">Priority</label>
|
||||
<select class="lt-select" style="font-size:0.8rem">
|
||||
<label class="lt-label" style="font-size:0.75rem" for="adv-filter-priority">Priority</label>
|
||||
<select id="adv-filter-priority" class="lt-select" style="font-size:0.8rem">
|
||||
<option value="">All</option>
|
||||
<option>P1 Critical</option>
|
||||
<option>P2 High</option>
|
||||
@@ -412,8 +412,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
<label class="lt-label" style="font-size:0.75rem">Assignee</label>
|
||||
<select class="lt-select" style="font-size:0.8rem">
|
||||
<label class="lt-label" style="font-size:0.75rem" for="adv-filter-assignee">Assignee</label>
|
||||
<select id="adv-filter-assignee" class="lt-select" style="font-size:0.8rem">
|
||||
<option value="">Anyone</option>
|
||||
<option>Unassigned</option>
|
||||
<option>jdoe</option>
|
||||
@@ -878,21 +878,21 @@
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-1" data-accordion>
|
||||
SYSTEM OVERVIEW
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="lt-accordion-body" id="acc-body-1"><div class="lt-accordion-content">Node running at 72% CPU. 12 active processes. Last restart: 3d ago.</div></div>
|
||||
</div>
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-2" data-accordion>
|
||||
NETWORK CONFIG
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="lt-accordion-body" id="acc-body-2"><div class="lt-accordion-content">eth0: 10.0.0.7 — MTU 1500 — RX 4.2 GB — TX 1.1 GB</div></div>
|
||||
</div>
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-3" data-accordion>
|
||||
FIREWALL RULES
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M1 1l4 4 4-4"/></svg>
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="lt-accordion-body" id="acc-body-3"><div class="lt-accordion-content">22/tcp ALLOW — 80/tcp ALLOW — 443/tcp ALLOW — */* DENY</div></div>
|
||||
</div>
|
||||
@@ -949,7 +949,7 @@
|
||||
<span class="lt-range-label">Refresh Interval (s)</span>
|
||||
<span class="lt-range-value">30</span>
|
||||
</div>
|
||||
<input type="range" class="lt-range" min="5" max="60" value="30">
|
||||
<input type="range" class="lt-range" min="5" max="60" value="30" aria-label="Refresh Interval in seconds" aria-valuemin="5" aria-valuemax="60" aria-valuenow="30">
|
||||
</div>
|
||||
<div class="lt-tags">
|
||||
<span class="lt-tag lt-tag--orange">CRITICAL</span>
|
||||
@@ -1420,13 +1420,13 @@
|
||||
<!-- Steps -->
|
||||
<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>
|
||||
<div class="lt-form-group"><label class="lt-label" for="wizard-title">Ticket Title</label><input id="wizard-title" type="text" name="title" class="lt-input lt-w-full" placeholder="Brief description…"></div>
|
||||
<div class="lt-form-group"><label class="lt-label" for="wizard-priority">Priority</label><select id="wizard-priority" 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>
|
||||
</div>
|
||||
<div data-wizard-step="2" aria-hidden="true">
|
||||
<div class="lt-form-group" style="margin-top:1rem"><label class="lt-label">Assign To</label><input type="text" name="assignee" class="lt-input lt-w-full" placeholder="Username…"></div>
|
||||
<div class="lt-form-group"><label class="lt-label">Due Date</label><input type="date" name="due" class="lt-input"></div>
|
||||
<div class="lt-form-group" style="margin-top:1rem"><label class="lt-label" for="wizard-assignee">Assign To</label><input id="wizard-assignee" type="text" name="assignee" class="lt-input lt-w-full" placeholder="Username…"></div>
|
||||
<div class="lt-form-group"><label class="lt-label" for="wizard-due">Due Date</label><input id="wizard-due" type="date" name="due" class="lt-input"></div>
|
||||
</div>
|
||||
<div data-wizard-step="3" aria-hidden="true">
|
||||
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon">✅</div><div class="lt-empty-state-title">Review & Submit</div><div class="lt-empty-state-body">Check the details above and click Submit when ready.</div></div>
|
||||
|
||||
@@ -586,14 +586,16 @@
|
||||
---------------------------------------------------------------- */
|
||||
function initStatsFilter() {
|
||||
document.querySelectorAll('.lt-stat-card[data-filter-key]').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const _activate = () => {
|
||||
const key = card.dataset.filterKey, val = card.dataset.filterVal;
|
||||
const wasActive = card.classList.contains('active');
|
||||
document.querySelectorAll('.lt-stat-card').forEach(c => c.classList.remove('active'));
|
||||
if (!wasActive) card.classList.add('active');
|
||||
if (typeof global.lt_onStatFilter === 'function')
|
||||
global.lt_onStatFilter(wasActive ? null : key, wasActive ? null : val);
|
||||
});
|
||||
};
|
||||
card.addEventListener('click', _activate);
|
||||
card.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _activate(); } });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -860,6 +862,8 @@
|
||||
const ov = document.getElementById('lt-cmd-overlay'); if (!ov) return;
|
||||
ov.classList.remove('is-open');
|
||||
_unlockScroll();
|
||||
const inp = document.querySelector('#lt-cmd-palette .lt-cmd-input');
|
||||
if (inp) inp.removeAttribute('aria-activedescendant');
|
||||
if (_cpTrigger) { if (document.contains(_cpTrigger)) _cpTrigger.focus(); _cpTrigger = null; }
|
||||
}
|
||||
|
||||
@@ -903,7 +907,7 @@
|
||||
if (!groups[g] || !groups[g].length) return;
|
||||
html += '<div class="lt-cmd-section-label">' + escHtml(g) + '</div>';
|
||||
groups[g].forEach(cmd => {
|
||||
html += '<div class="lt-cmd-item' + (idx === 0 ? ' is-selected' : '') + '" data-cmd-id="' + escHtml(cmd.id) + '">' +
|
||||
html += '<div class="lt-cmd-item' + (idx === 0 ? ' is-selected' : '') + '" id="lt-cmd-item-' + idx + '" data-cmd-id="' + escHtml(cmd.id) + '">' +
|
||||
'<span class="lt-cmd-item-icon">' + escHtml(cmd.icon || '◦') + '</span>' +
|
||||
'<span class="lt-cmd-item-label">' + _cpHighlight(cmd.label, query) + '</span>' +
|
||||
(cmd.kbd ? '<span class="lt-cmd-item-kbd">' + escHtml(cmd.kbd) + '</span>' : '') +
|
||||
@@ -913,10 +917,15 @@
|
||||
});
|
||||
|
||||
results.innerHTML = html;
|
||||
results.querySelectorAll('.lt-cmd-item').forEach((item, i) => {
|
||||
const pal = document.getElementById('lt-cmd-palette');
|
||||
const inp = pal && pal.querySelector('.lt-cmd-input');
|
||||
const allItems = Array.from(results.querySelectorAll('.lt-cmd-item'));
|
||||
if (inp && allItems[0]) inp.setAttribute('aria-activedescendant', allItems[0].id);
|
||||
allItems.forEach((item, i) => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
results.querySelectorAll('.lt-cmd-item').forEach(x => x.classList.remove('is-selected'));
|
||||
allItems.forEach(x => x.classList.remove('is-selected'));
|
||||
item.classList.add('is-selected'); _cpSelected = i;
|
||||
if (inp) inp.setAttribute('aria-activedescendant', item.id);
|
||||
});
|
||||
item.addEventListener('click', () => _cpExec(item.dataset.cmdId));
|
||||
});
|
||||
@@ -941,6 +950,8 @@
|
||||
_cpSelected = (_cpSelected + dir + items.length) % items.length;
|
||||
items[_cpSelected] && items[_cpSelected].classList.add('is-selected');
|
||||
items[_cpSelected] && items[_cpSelected].scrollIntoView({ block: 'nearest' });
|
||||
const inp = ov.querySelector('.lt-cmd-input');
|
||||
if (inp && items[_cpSelected]) inp.setAttribute('aria-activedescendant', items[_cpSelected].id);
|
||||
}
|
||||
|
||||
function initCmdPalette(commands) {
|
||||
@@ -1392,7 +1403,7 @@
|
||||
Auto-inits on [id="lt-menu-btn"] + [id="lt-nav-drawer"]
|
||||
Swipe right from left edge (≤ 20px) opens; swipe left closes.
|
||||
================================================================ */
|
||||
let _mnOpen = false;
|
||||
let _mnOpen = false, _mnTrigger = null;
|
||||
|
||||
function _mnSetOpen(open) {
|
||||
_mnOpen = open;
|
||||
@@ -1402,20 +1413,28 @@
|
||||
if (!drawer) return;
|
||||
|
||||
if (open) {
|
||||
_mnTrigger = (document.activeElement && document.activeElement !== document.body) ? document.activeElement : null;
|
||||
drawer.classList.add('open');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
drawer.removeAttribute('aria-hidden');
|
||||
if (overlay) overlay.classList.add('open');
|
||||
if (btn) { btn.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); }
|
||||
document.body.style.overflow = 'hidden';
|
||||
// Trap focus inside drawer
|
||||
if (!drawer._mnTrapHandler) {
|
||||
drawer._mnTrapHandler = e => { if (e.key === 'Tab') _trapFocus(drawer, e); };
|
||||
drawer.addEventListener('keydown', drawer._mnTrapHandler);
|
||||
}
|
||||
const first = drawer.querySelector('button, a, [tabindex]');
|
||||
if (first) setTimeout(() => first.focus(), 50);
|
||||
if (first) setTimeout(() => { if (document.contains(first)) first.focus(); }, 50);
|
||||
} else {
|
||||
drawer.classList.remove('open');
|
||||
drawer.setAttribute('aria-hidden', 'true');
|
||||
if (overlay) overlay.classList.remove('open');
|
||||
if (btn) { btn.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); }
|
||||
document.body.style.overflow = '';
|
||||
if (drawer._mnTrapHandler) { drawer.removeEventListener('keydown', drawer._mnTrapHandler); delete drawer._mnTrapHandler; }
|
||||
if (_mnTrigger && document.contains(_mnTrigger)) { _mnTrigger.focus(); }
|
||||
_mnTrigger = null;
|
||||
}
|
||||
bus.emit('mobileNav:' + (open ? 'open' : 'close'));
|
||||
}
|
||||
@@ -1877,6 +1896,7 @@
|
||||
}
|
||||
filtered.forEach((opt, i) => {
|
||||
const el = document.createElement('div');
|
||||
el.id = dropId + '-opt-' + i;
|
||||
el.className = 'lt-combobox-option' + (selected.includes(opt.value) ? ' is-selected' : '');
|
||||
el.setAttribute('role', 'option');
|
||||
el.setAttribute('data-value', opt.value);
|
||||
@@ -1901,16 +1921,18 @@
|
||||
}
|
||||
|
||||
function _moveFocus(dir) {
|
||||
const items = dropdown.querySelectorAll('.lt-combobox-option');
|
||||
const items = Array.from(dropdown.querySelectorAll('.lt-combobox-option'));
|
||||
if (!items.length) return;
|
||||
focusedIdx = Math.max(0, Math.min(focusedIdx + dir, items.length - 1));
|
||||
items.forEach((el, i) => el.classList.toggle('is-focused', i === focusedIdx));
|
||||
items[focusedIdx].scrollIntoView({ block: 'nearest' });
|
||||
inputEl.setAttribute('aria-activedescendant', items[focusedIdx].id);
|
||||
}
|
||||
|
||||
function _setOpen(open) {
|
||||
dropdown.classList.toggle('is-open', open);
|
||||
inputEl.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
if (!open) { inputEl.removeAttribute('aria-activedescendant'); focusedIdx = -1; }
|
||||
}
|
||||
inputEl.addEventListener('input', () => { _setOpen(true); _renderDropdown(inputEl.value); });
|
||||
inputEl.addEventListener('focus', () => { _setOpen(true); _renderDropdown(inputEl.value); });
|
||||
@@ -1966,6 +1988,7 @@
|
||||
const q = query.toLowerCase();
|
||||
_items.forEach((item, i) => {
|
||||
const el = document.createElement('div');
|
||||
el.id = (inputEl.id || 'lt-ta') + '-item-' + i;
|
||||
el.className = 'lt-typeahead-item';
|
||||
el.setAttribute('role', 'option');
|
||||
const safeItemLabel = escHtml(item.label);
|
||||
@@ -1980,27 +2003,32 @@
|
||||
async function _search(query) {
|
||||
dropdown.innerHTML = '<div class="lt-typeahead-loading">Searching…</div>';
|
||||
dropdown.classList.add('is-open');
|
||||
inputEl.setAttribute('aria-busy', 'true');
|
||||
try {
|
||||
const results = typeof source === 'function' ? await source(query) : source.filter(i => i.label.toLowerCase().includes(query.toLowerCase()));
|
||||
_render(results, query);
|
||||
} catch(e) {
|
||||
dropdown.innerHTML = '<div class="lt-typeahead-empty">Error loading results</div>';
|
||||
} finally {
|
||||
inputEl.setAttribute('aria-busy', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function _select(item) {
|
||||
inputEl.value = item.label;
|
||||
inputEl.removeAttribute('aria-activedescendant');
|
||||
dropdown.classList.remove('is-open');
|
||||
if (onSelect) onSelect(item);
|
||||
bus.emit('typeahead:select', { item });
|
||||
}
|
||||
|
||||
function _moveFocus(dir) {
|
||||
const els = dropdown.querySelectorAll('.lt-typeahead-item');
|
||||
const els = Array.from(dropdown.querySelectorAll('.lt-typeahead-item'));
|
||||
if (!els.length) return;
|
||||
_focusedIdx = Math.max(0, Math.min(_focusedIdx + dir, els.length - 1));
|
||||
els.forEach((el, i) => el.classList.toggle('is-focused', i === _focusedIdx));
|
||||
els[_focusedIdx].scrollIntoView({ block: 'nearest' });
|
||||
inputEl.setAttribute('aria-activedescendant', els[_focusedIdx].id);
|
||||
}
|
||||
|
||||
inputEl.addEventListener('input', () => {
|
||||
@@ -2502,7 +2530,7 @@
|
||||
_overlay.querySelector('.lt-lightbox-next').style.display = (loop || idx < _images.length - 1) && _images.length > 1 ? '' : 'none';
|
||||
_overlay.classList.add('is-open');
|
||||
_lockScroll();
|
||||
setTimeout(() => el.focus?.(), 50);
|
||||
setTimeout(() => { if (document.contains(el)) el.focus?.(); }, 50);
|
||||
}
|
||||
|
||||
function _collect() {
|
||||
|
||||
Reference in New Issue
Block a user