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
+22 -6
View File
@@ -282,8 +282,19 @@
const saved = localStorage.getItem('lt_activeTab_' + location.pathname);
if (saved && document.getElementById(saved)) { switchTab(saved); }
} catch (_) {}
document.querySelectorAll('.lt-tab[data-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
document.querySelectorAll('[role="tablist"]').forEach(tablist => {
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;
ov.classList.remove('is-open');
_unlockScroll();
if (_cpTrigger) { _cpTrigger.focus(); _cpTrigger = null; }
if (_cpTrigger) { if (document.contains(_cpTrigger)) _cpTrigger.focus(); _cpTrigger = null; }
}
function _cpHighlight(text, q) {
@@ -970,6 +981,7 @@
function _validateField(el) {
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.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' };
@@ -1663,7 +1675,7 @@
// Position — keep on screen
const vw = window.innerWidth, vh = window.innerHeight;
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';
// Focus first item
const first = _ctxMenu.querySelector('[role="menuitem"]');
@@ -2110,7 +2122,11 @@
group._sbInit = true;
const label = group.querySelector('.lt-sidebar-group-label');
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
if (group.querySelector('.lt-sidebar-sub-link.active, .lt-sidebar-sub-link[aria-current="page"]')) {
group.classList.add('is-open');
@@ -2210,7 +2226,7 @@
function _show(idx) {
steps.forEach((s, i) => {
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
container.querySelectorAll('[data-wizard-indicator]').forEach((ind, i) => {