← back to Commercialrealestate
a11y(corner-nav): focus trap, arrow-key nav, dialog ARIA roles, keyboard accordion (TK-10324)
aab6e2d9d2b3e07b0d4c8a080ca5684d8703c316 · 2026-08-07 04:05:36 -0700 · steve@designerwallcoverings.com
- drawer() now has role=dialog aria-modal=true aria-label
- Tab/Shift+Tab trapped inside open drawer, wrapping at ends
- ArrowDown/Up navigate among links within a drawer
- focusables() helper enumerates visible interactive elements
- Section h4 headers: role=button tabindex=0 aria-expanded + Enter/Space key handler
- aria-expanded state updated on toggle (was missing)
Files touched
Diff
commit aab6e2d9d2b3e07b0d4c8a080ca5684d8703c316
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Fri Aug 7 04:05:36 2026 -0700
a11y(corner-nav): focus trap, arrow-key nav, dialog ARIA roles, keyboard accordion (TK-10324)
- drawer() now has role=dialog aria-modal=true aria-label
- Tab/Shift+Tab trapped inside open drawer, wrapping at ends
- ArrowDown/Up navigate among links within a drawer
- focusables() helper enumerates visible interactive elements
- Section h4 headers: role=button tabindex=0 aria-expanded + Enter/Space key handler
- aria-expanded state updated on toggle (was missing)
---
public/corner-nav.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 53 insertions(+), 1 deletion(-)
diff --git a/public/corner-nav.js b/public/corner-nav.js
index 28d9e01..3e81a6c 100644
--- a/public/corner-nav.js
+++ b/public/corner-nav.js
@@ -102,21 +102,73 @@
var sec = el('div', 'cnav-sec collapsed' + (opts.current ? ' cur-sec' : ''));
var dot = opts.current ? '<span class="curdot" title="you are here"></span>' : '';
var cnt = (opts.count != null) ? '<span class="cnt">' + opts.count + '</span>' : '';
+ var expanded = !opts.current; // starts collapsed unless it's the current section
+ var hId = 'cnav-h-' + Math.random().toString(36).slice(2, 8);
var h = el('h4', null, '<span class="lt">' + dot + '<span>' + title + '</span></span>' +
'<span class="rt">' + cnt + '<span class="car">▾</span></span>');
+ h.setAttribute('role', 'button');
+ h.setAttribute('tabindex', '0');
+ h.setAttribute('aria-expanded', expanded ? 'true' : 'false');
+ h.id = hId;
var list = el('div', 'cnav-list');
- h.addEventListener('click', function () { sec.classList.toggle('collapsed'); });
+ list.setAttribute('role', 'list');
+ function toggleSection() {
+ sec.classList.toggle('collapsed');
+ var isExpanded = !sec.classList.contains('collapsed');
+ h.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
+ }
+ h.addEventListener('click', toggleSection);
+ h.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection(); }
+ });
sec.appendChild(h); sec.appendChild(list); sec._list = list;
return sec;
}
+ // Return all keyboard-focusable elements inside a container (tab order).
+ function focusables(container) {
+ return Array.prototype.slice.call(
+ container.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),[tabindex="0"]')
+ ).filter(function (el) { return el.offsetParent !== null || el === document.activeElement; });
+ }
+
function drawer(side, title, sub) {
var d = el('div', 'cnav-drawer ' + side);
+ d.setAttribute('role', 'dialog');
+ d.setAttribute('aria-modal', 'true');
+ d.setAttribute('aria-label', title);
var hd = el('div', 'cnav-hd', '<span>' + title + (sub ? '<span class="sub">' + sub + '</span>' : '') + '</span>');
var x = el('button', null, '✕'); x.setAttribute('aria-label', 'Close'); x.addEventListener('click', closeAll);
hd.appendChild(x);
var body = el('div', 'cnav-body');
d.appendChild(hd); d.appendChild(body); document.body.appendChild(d); d._body = body;
+
+ // Focus trap: keep Tab/Shift+Tab inside the open drawer; arrow keys navigate links.
+ d.addEventListener('keydown', function (e) {
+ if (!d.classList.contains('on')) return;
+ var focs = focusables(d);
+ if (!focs.length) return;
+ var idx = focs.indexOf(document.activeElement);
+ if (e.key === 'Tab') {
+ if (e.shiftKey) {
+ e.preventDefault();
+ focs[idx <= 0 ? focs.length - 1 : idx - 1].focus();
+ } else {
+ e.preventDefault();
+ focs[idx >= focs.length - 1 ? 0 : idx + 1].focus();
+ }
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ var links = focs.filter(function (f) { return f.tagName === 'A' || (f.tagName === 'BUTTON' && f !== x); });
+ var li = links.indexOf(document.activeElement);
+ if (links.length) links[li >= links.length - 1 ? 0 : li + 1].focus();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ var links2 = focs.filter(function (f) { return f.tagName === 'A' || (f.tagName === 'BUTTON' && f !== x); });
+ var li2 = links2.indexOf(document.activeElement);
+ if (links2.length) links2[li2 <= 0 ? links2.length - 1 : li2 - 1].focus();
+ }
+ });
return d;
}
← 9fe920b auto-data-snapshot: 2026-08-07T00:47:33 (3 data files) — dat
·
back to Commercialrealestate
·
auto-data-snapshot: 2026-08-07T04:23:46 (2 data files) — dat 2a9f7af →