From e046a6f632e9a5dfe4c60a3308a3eaa3afa9f820 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sun, 5 Apr 2026 15:06:14 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20mobile=20accessibility=20=E2=80=94=20tou?= =?UTF-8?q?ch=20targets,=20ARIA,=20small=20viewport=20support=20(#630)=20(?= =?UTF-8?q?#633)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes critical and major mobile accessibility items from #630, focused on small phone viewports (320px–375px). ### Critical fixes 1. **Touch targets ≥ 44px** — All interactive elements (filter buttons, tab buttons, search inputs, nav buttons, region pills, dropdowns) get `min-height: 44px; min-width: 44px` via `@media (pointer: coarse)` — desktop/mouse users are unaffected. 2. **ARIA live regions** — Added `aria-live="polite"` to: packet list (`#pktLeft`), node list (`#nodesLeft`), analytics content (`#analyticsContent`), live feed (`#liveFeed` with `role="log"`). Screen readers now announce dynamic content updates. 3. **Color-only status indicators** — Status dots in live view marked `aria-hidden="true"` (text labels like "Online"/"Degraded"/"Offline" already present alongside). 4. **Detail panel on mobile** — Side panel (`panel-right`) renders as a full-screen fixed overlay on ≤640px. Close button (✕) added to nodes detail panel. Escape key closes both nodes and packets detail panels. ### Major fixes 5. **Analytics tabs overflow** — Tabs switch to `flex-wrap: nowrap; overflow-x: auto` on ≤640px, preventing overflow on 320px screens. 6. **Table horizontal scroll** — Added `.table-scroll-wrap` class and `min-width: 480px` on `.data-table` at ≤640px for horizontal scrolling when columns don't fit. 7. **SPA focus management** — On every page navigation, focus moves to first heading (`h1`/`h2`/`h3`) or falls back to `#app`. Uses `requestAnimationFrame` for correct DOM timing. ### Bonus - Analytics tabs get `role="tablist"` + `aria-label` for screen reader semantics. ### Known follow-ups (not blocking) - Individual tab buttons should get `role="tab"` + `aria-selected` + `aria-controls` for complete ARIA tab pattern. - `sr-status-label` and `table-scroll-wrap` CSS classes are defined but not yet used in JS — ready for future use when status text labels and table wrappers are wired up. Closes #630 Co-authored-by: you --- public/analytics.js | 4 +-- public/app.js | 6 ++++ public/live.js | 4 +-- public/nodes.js | 14 ++++++++- public/packets.js | 2 +- public/style.css | 74 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 6 deletions(-) diff --git a/public/analytics.js b/public/analytics.js index 86c1d22d..d9376c5c 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -75,7 +75,7 @@

📊 Mesh Analytics

Deep dive into your mesh network data

-
+
@@ -90,7 +90,7 @@
-
+
Loading analytics…
`; diff --git a/public/app.js b/public/app.js index 6323e47e..93310f85 100644 --- a/public/app.js +++ b/public/app.js @@ -472,6 +472,12 @@ function navigate() { const ms = performance.now() - t0; if (ms > 100) console.warn(`[SLOW PAGE] ${basePage} init took ${Math.round(ms)}ms`); app.classList.remove('page-enter'); void app.offsetWidth; app.classList.add('page-enter'); + // #630-7: SPA focus management — move focus to first heading or main content + requestAnimationFrame(function() { + var heading = app.querySelector('h1, h2, h3, [role="heading"]'); + if (heading) { heading.setAttribute('tabindex', '-1'); heading.focus({ preventScroll: true }); } + else { app.setAttribute('tabindex', '-1'); app.focus({ preventScroll: true }); } + }); } else { app.innerHTML = `

${route}

Page not yet implemented.

`; } diff --git a/public/live.js b/public/live.js index 72d2cfea..80cc8024 100644 --- a/public/live.js +++ b/public/live.js @@ -754,7 +754,7 @@ -
+
@@ -1323,7 +1323,7 @@ let html = `
- ● +

${escapeHtml(n.name || 'Unknown')}

diff --git a/public/nodes.js b/public/nodes.js index 94de855f..bd557330 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -324,7 +324,7 @@
-
+
Select a node to view details
`; @@ -920,6 +920,17 @@ } }); + // #630: Close button for node detail panel (important for mobile full-screen overlay) + document.getElementById('nodesRight').addEventListener('click', function(e) { + if (e.target.closest('.panel-close-btn')) { + const panel = document.getElementById('nodesRight'); + panel.classList.add('empty'); + panel.innerHTML = 'Select a node to view details'; + selectedKey = null; + renderRows(); + } + }); + renderRows(); } @@ -1007,6 +1018,7 @@ const dupBadge = dupNameBadge(n.name, n.public_key, dupMap); panel.innerHTML = ` +
${escapeHtml(n.name || '(unnamed)')}${dupBadge}
${renderNodeBadges(n, roleColor)} diff --git a/public/packets.js b/public/packets.js index ae51174a..dac9314d 100644 --- a/public/packets.js +++ b/public/packets.js @@ -283,7 +283,7 @@ } } app.innerHTML = `
-
+
${PANEL_CLOSE_HTML} diff --git a/public/style.css b/public/style.css index 5fc62168..86d9bf08 100644 --- a/public/style.css +++ b/public/style.css @@ -2165,3 +2165,77 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } width: 100%; } .cc-picker-clear:hover { color: var(--text-primary, #e0e0e0); border-color: var(--text-primary, #e0e0e0); } + +/* === #630 — Mobile Accessibility Fixes === */ + +/* #630-1: Touch targets — minimum 44px on touch devices */ +@media (pointer: coarse) { + .filter-bar .btn, + .filter-group .btn, + .tab-btn, + .filter-bar input, + .filter-bar select, + .nav-btn, + .region-pill, + .region-dropdown-trigger, + .multi-select-trigger, + .node-count-pill, + .analytics-time-range button, + .detail-back-btn, + .filter-toggle-btn { + min-height: 44px; + min-width: 44px; + } + .filter-bar input, + .filter-bar select { + height: 44px; + } + .region-dropdown-trigger, + .multi-select-trigger { + height: 44px; + } +} + +/* #630-3: Status text labels — visually hidden text for screen readers */ +.sr-status-label { font-size: 11px; margin-left: 4px; } + +/* #630-4: Detail panel as full-width overlay on mobile */ +@media (max-width: 640px) { + .split-layout .panel-right:not(.empty) { + display: block; + position: fixed; + top: 52px; + left: 0; + right: 0; + bottom: 0; + width: 100%; + min-width: 0; + z-index: 150; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } +} + +/* #630-5: Analytics tabs — horizontal scroll on small screens */ +@media (max-width: 640px) { + .analytics-tabs { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + padding-bottom: 4px; + } + .analytics-tabs .tab-btn { + flex-shrink: 0; + white-space: nowrap; + } +} + +/* #630-6: Tables — horizontal scroll wrapper */ +.table-scroll-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +@media (max-width: 640px) { + .data-table { min-width: 480px; } +}