audit pass 8: keyboard accessibility and table semantics

CSS:
- Fix light theme input/select/textarea :focus -> :focus-visible

HTML:
- Worker metrics table: convert label <td> to <th scope="row"> for screen readers
- Add aria-label to worker metrics table
- Sticky table: add scope="col" to all column headers
- Keyboard shortcuts modal table: add scope="col" to headers
- Kanban cards: remove tabindex="0" from role="article" (non-interactive)
- Advanced filter: ensure all 3 label/select pairs have for/id associations

JS:
- Lightbox: fix keydown listener leak by storing bound reference for removeEventListener
- Lightbox: save/restore trigger focus on open/close
- Sortable table: add tabindex="0" and Enter/Space keydown handler on sortable <th>
- Split pane: add tabindex="0", role="separator", aria-label, and arrow/Home/End
  keyboard resize support on divider (5% steps)
- Form validation: handle <select multiple> required check via selectedOptions.length

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 20:09:29 -04:00
parent b84d71dd7a
commit e8c1197613
3 changed files with 49 additions and 20 deletions
+33 -4
View File
@@ -558,7 +558,8 @@
ths.forEach((th, colIdx) => {
let dir = 'asc';
th.setAttribute('aria-sort', 'none');
th.addEventListener('click', () => {
th.setAttribute('tabindex', '0');
const _sort = () => {
ths.forEach(h => { h.removeAttribute('data-sort'); h.setAttribute('aria-sort', 'none'); });
th.setAttribute('data-sort', dir);
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
@@ -573,7 +574,9 @@
});
rows.forEach(r => tbody.appendChild(r));
dir = dir === 'asc' ? 'desc' : 'asc';
});
};
th.addEventListener('click', _sort);
th.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _sort(); } });
});
}
@@ -993,6 +996,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.tagName === 'SELECT' && el.multiple && el.required && el.selectedOptions.length === 0) return { valid: false, message: 'Please select at least one option' };
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' };
@@ -2133,6 +2137,25 @@
});
divider.addEventListener('pointerup', () => { dragging = false; divider.classList.remove('is-dragging'); });
// Keyboard resize support
divider.setAttribute('tabindex', '0');
divider.setAttribute('role', 'separator');
divider.setAttribute('aria-label', 'Resize panes');
divider.addEventListener('keydown', e => {
const step = 0.05;
const total = vertical ? container.clientHeight : container.clientWidth;
const divSize = vertical ? divider.offsetHeight : divider.offsetWidth;
const available = total - divSize;
const currentSize = vertical ? panes[0].offsetHeight : panes[0].offsetWidth;
const currentRatio = currentSize / available;
if ((e.key === 'ArrowRight' && !vertical) || (e.key === 'ArrowDown' && vertical)) {
e.preventDefault(); _setRatio(Math.min(1, currentRatio + step));
} else if ((e.key === 'ArrowLeft' && !vertical) || (e.key === 'ArrowUp' && vertical)) {
e.preventDefault(); _setRatio(Math.max(0, currentRatio - step));
} else if (e.key === 'Home') { e.preventDefault(); _setRatio(0); }
else if (e.key === 'End') { e.preventDefault(); _setRatio(1); }
});
_setRatio(initial);
return { setRatio: _setRatio };
},
@@ -2477,7 +2500,7 @@
const lightbox = {
init(selector, opts = {}) {
const { caption = 'alt', loop = true } = opts;
let _images = [], _current = 0, _overlay = null;
let _images = [], _current = 0, _overlay = null, _lbKeyBound = null, _lbTrigger = null;
function _getCaption(img) {
if (typeof caption === 'function') return caption(img);
@@ -2507,7 +2530,8 @@
_overlay.querySelector('.lt-lightbox-prev').addEventListener('click', () => lightbox.prev());
_overlay.querySelector('.lt-lightbox-next').addEventListener('click', () => lightbox.next());
_overlay.addEventListener('click', e => { if (e.target === _overlay) lightbox.close(); });
document.addEventListener('keydown', _lbKey);
_lbKeyBound = _lbKey.bind(null);
document.addEventListener('keydown', _lbKeyBound);
}
function _lbKey(e) {
@@ -2519,6 +2543,9 @@
function _show(idx) {
if (!_overlay) _buildOverlay();
if (!_overlay.classList.contains('is-open')) {
_lbTrigger = (document.activeElement && document.activeElement !== document.body) ? document.activeElement : null;
}
_current = idx;
const img = _images[idx];
const el = _overlay.querySelector('.lt-lightbox-img');
@@ -2554,6 +2581,8 @@
if (!_overlay) return;
_overlay.classList.remove('is-open');
_unlockScroll();
if (_lbTrigger && document.contains(_lbTrigger)) { _lbTrigger.focus(); }
_lbTrigger = null;
},
prev() { _show(loop ? (_current - 1 + _images.length) % _images.length : Math.max(0, _current - 1)); },
next() { _show(loop ? (_current + 1) % _images.length : Math.min(_images.length - 1, _current + 1)); },