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:
@@ -3649,9 +3649,9 @@ html[data-theme="light"] .lt-textarea {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
html[data-theme="light"] .lt-input:focus,
|
html[data-theme="light"] .lt-input:focus-visible,
|
||||||
html[data-theme="light"] .lt-select:focus,
|
html[data-theme="light"] .lt-select:focus-visible,
|
||||||
html[data-theme="light"] .lt-textarea:focus {
|
html[data-theme="light"] .lt-textarea:focus-visible {
|
||||||
border-color: var(--accent-cyan);
|
border-color: var(--accent-cyan);
|
||||||
box-shadow: var(--box-glow-cyan);
|
box-shadow: var(--box-glow-cyan);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -559,7 +559,7 @@
|
|||||||
<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" role="article" tabindex="0" aria-label="P1 — Storage array link-down, 5m ago, Unassigned">
|
<div class="lt-card lt-mb-md lt-row-p1" role="article" 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" aria-hidden="true">P1</span>
|
<span class="lt-p1" aria-hidden="true">P1</span>
|
||||||
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
|
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
|
||||||
@@ -568,7 +568,7 @@
|
|||||||
<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" role="article" tabindex="0" aria-label="P3 — Update node_exporter on micro1, 1d ago, operator">
|
<div class="lt-card lt-row-p3" role="article" 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" aria-hidden="true">P3</span>
|
<span class="lt-p3" aria-hidden="true">P3</span>
|
||||||
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
|
<span class="lt-dot lt-dot-up" aria-hidden="true"></span>
|
||||||
@@ -586,7 +586,7 @@
|
|||||||
<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" role="article" tabindex="0" aria-label="P2 — Scheduled maintenance window, 2d ago, admin">
|
<div class="lt-card lt-row-p2" role="article" 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" aria-hidden="true">P2</span>
|
<span class="lt-p2" aria-hidden="true">P2</span>
|
||||||
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
|
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
|
||||||
@@ -603,7 +603,7 @@
|
|||||||
<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" role="article" tabindex="0" aria-label="P2 — Switch port flapping on USW-Pro, 2h ago, operator">
|
<div class="lt-card lt-row-p2 lt-item-running" role="article" 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" aria-hidden="true">P2</span>
|
<span class="lt-p2" aria-hidden="true">P2</span>
|
||||||
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
|
<span class="lt-dot lt-dot-warn" aria-hidden="true"></span>
|
||||||
@@ -620,7 +620,7 @@
|
|||||||
<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" role="article" tabindex="0" aria-label="P4 — Update SSL cert on wiki, 3d ago, operator">
|
<div class="lt-card lt-row-p4" style="opacity:0.6" role="article" 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" aria-hidden="true">P4</span>
|
<span class="lt-p4" aria-hidden="true">P4</span>
|
||||||
<span class="lt-dot lt-dot-idle" aria-hidden="true"></span>
|
<span class="lt-dot lt-dot-idle" aria-hidden="true"></span>
|
||||||
@@ -646,13 +646,13 @@
|
|||||||
<span class="lt-status lt-status-online">Online</span>
|
<span class="lt-status lt-status-online">Online</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-data-table-wrapper">
|
<div class="lt-data-table-wrapper">
|
||||||
<table class="lt-data-table">
|
<table class="lt-data-table" aria-label="pulse-worker-01 metrics">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="lt-text-muted">CPU</td> <td>12%</td></tr>
|
<tr><th scope="row" class="lt-text-muted">CPU</th> <td>12%</td></tr>
|
||||||
<tr><td class="lt-text-muted">Memory</td> <td>2.1 GB / 8 GB</td></tr>
|
<tr><th scope="row" class="lt-text-muted">Memory</th> <td>2.1 GB / 8 GB</td></tr>
|
||||||
<tr><td class="lt-text-muted">Load</td> <td>0.42 / 0.51 / 0.48</td></tr>
|
<tr><th scope="row" class="lt-text-muted">Load</th> <td>0.42 / 0.51 / 0.48</td></tr>
|
||||||
<tr><td class="lt-text-muted">Uptime</td> <td>14d 6h</td></tr>
|
<tr><th scope="row" class="lt-text-muted">Uptime</th> <td>14d 6h</td></tr>
|
||||||
<tr><td class="lt-text-muted">Tasks</td> <td>2 / 5</td></tr>
|
<tr><th scope="row" class="lt-text-muted">Tasks</th> <td>2 / 5</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -1311,7 +1311,7 @@
|
|||||||
<div class="lt-section-body">
|
<div class="lt-section-body">
|
||||||
<div class="lt-table-sticky-wrap">
|
<div class="lt-table-sticky-wrap">
|
||||||
<table class="lt-table lt-table-responsive">
|
<table class="lt-table lt-table-responsive">
|
||||||
<thead><tr><th>ID</th><th>Priority</th><th>Title</th><th>Status</th><th>Assignee</th></tr></thead>
|
<thead><tr><th scope="col">ID</th><th scope="col">Priority</th><th scope="col">Title</th><th scope="col">Status</th><th scope="col">Assignee</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td data-label="ID"><a href="#" class="lt-text-cyan">#001</a></td><td data-label="Priority"><span class="lt-badge lt-badge-p1">P1</span></td><td data-label="Title">Link-down on compute-storage-01</td><td data-label="Status"><span class="lt-badge lt-badge-open">Open</span></td><td data-label="Assignee">jdoe</td></tr>
|
<tr><td data-label="ID"><a href="#" class="lt-text-cyan">#001</a></td><td data-label="Priority"><span class="lt-badge lt-badge-p1">P1</span></td><td data-label="Title">Link-down on compute-storage-01</td><td data-label="Status"><span class="lt-badge lt-badge-open">Open</span></td><td data-label="Assignee">jdoe</td></tr>
|
||||||
<tr><td data-label="ID"><a href="#" class="lt-text-cyan">#002</a></td><td data-label="Priority"><span class="lt-badge lt-badge-p2">P2</span></td><td data-label="Title">Switch port flapping USW-Pro-24</td><td data-label="Status"><span class="lt-badge lt-badge-in-progress">In Progress</span></td><td data-label="Assignee">smith</td></tr>
|
<tr><td data-label="ID"><a href="#" class="lt-text-cyan">#002</a></td><td data-label="Priority"><span class="lt-badge lt-badge-p2">P2</span></td><td data-label="Title">Switch port flapping USW-Pro-24</td><td data-label="Status"><span class="lt-badge lt-badge-in-progress">In Progress</span></td><td data-label="Assignee">smith</td></tr>
|
||||||
@@ -1645,7 +1645,7 @@ Storage array link-down on `compute-storage-01`.
|
|||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<table class="lt-data-table" style="width:100%">
|
<table class="lt-data-table" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Shortcut</th><th>Action</th></tr>
|
<tr><th scope="col">Shortcut</th><th scope="col">Action</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Ctrl / ⌘ + K</td><td>Focus search box</td></tr>
|
<tr><td>Ctrl / ⌘ + K</td><td>Focus search box</td></tr>
|
||||||
|
|||||||
@@ -558,7 +558,8 @@
|
|||||||
ths.forEach((th, colIdx) => {
|
ths.forEach((th, colIdx) => {
|
||||||
let dir = 'asc';
|
let dir = 'asc';
|
||||||
th.setAttribute('aria-sort', 'none');
|
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'); });
|
ths.forEach(h => { h.removeAttribute('data-sort'); h.setAttribute('aria-sort', 'none'); });
|
||||||
th.setAttribute('data-sort', dir);
|
th.setAttribute('data-sort', dir);
|
||||||
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
|
th.setAttribute('aria-sort', dir === 'asc' ? 'ascending' : 'descending');
|
||||||
@@ -573,7 +574,9 @@
|
|||||||
});
|
});
|
||||||
rows.forEach(r => tbody.appendChild(r));
|
rows.forEach(r => tbody.appendChild(r));
|
||||||
dir = dir === 'asc' ? 'desc' : 'asc';
|
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) {
|
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 ((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.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' };
|
||||||
@@ -2133,6 +2137,25 @@
|
|||||||
});
|
});
|
||||||
divider.addEventListener('pointerup', () => { dragging = false; divider.classList.remove('is-dragging'); });
|
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);
|
_setRatio(initial);
|
||||||
return { setRatio: _setRatio };
|
return { setRatio: _setRatio };
|
||||||
},
|
},
|
||||||
@@ -2477,7 +2500,7 @@
|
|||||||
const lightbox = {
|
const lightbox = {
|
||||||
init(selector, opts = {}) {
|
init(selector, opts = {}) {
|
||||||
const { caption = 'alt', loop = true } = 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) {
|
function _getCaption(img) {
|
||||||
if (typeof caption === 'function') return caption(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-prev').addEventListener('click', () => lightbox.prev());
|
||||||
_overlay.querySelector('.lt-lightbox-next').addEventListener('click', () => lightbox.next());
|
_overlay.querySelector('.lt-lightbox-next').addEventListener('click', () => lightbox.next());
|
||||||
_overlay.addEventListener('click', e => { if (e.target === _overlay) lightbox.close(); });
|
_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) {
|
function _lbKey(e) {
|
||||||
@@ -2519,6 +2543,9 @@
|
|||||||
|
|
||||||
function _show(idx) {
|
function _show(idx) {
|
||||||
if (!_overlay) _buildOverlay();
|
if (!_overlay) _buildOverlay();
|
||||||
|
if (!_overlay.classList.contains('is-open')) {
|
||||||
|
_lbTrigger = (document.activeElement && document.activeElement !== document.body) ? document.activeElement : null;
|
||||||
|
}
|
||||||
_current = idx;
|
_current = idx;
|
||||||
const img = _images[idx];
|
const img = _images[idx];
|
||||||
const el = _overlay.querySelector('.lt-lightbox-img');
|
const el = _overlay.querySelector('.lt-lightbox-img');
|
||||||
@@ -2554,6 +2581,8 @@
|
|||||||
if (!_overlay) return;
|
if (!_overlay) return;
|
||||||
_overlay.classList.remove('is-open');
|
_overlay.classList.remove('is-open');
|
||||||
_unlockScroll();
|
_unlockScroll();
|
||||||
|
if (_lbTrigger && document.contains(_lbTrigger)) { _lbTrigger.focus(); }
|
||||||
|
_lbTrigger = null;
|
||||||
},
|
},
|
||||||
prev() { _show(loop ? (_current - 1 + _images.length) % _images.length : Math.max(0, _current - 1)); },
|
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)); },
|
next() { _show(loop ? (_current + 1) % _images.length : Math.min(_images.length - 1, _current + 1)); },
|
||||||
|
|||||||
Reference in New Issue
Block a user