mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 04:01:16 +00:00
## 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 <you@example.com>
This commit is contained in:
1 parent
0f5e2db5cf
commit
e046a6f632
6 files changed
+98
-6
No files matched your search
+2
-2
@@ -75,7 +75,7 @@
|
||||
<h2>📊 Mesh Analytics</h2>
|
||||
<p class="text-muted">Deep dive into your mesh network data</p>
|
||||
<div id="analyticsRegionFilter" class="region-filter-container"></div>
|
||||
<div class="analytics-tabs" id="analyticsTabs">
|
||||
<div class="analytics-tabs" id="analyticsTabs" role="tablist" aria-label="Analytics tabs">
|
||||
<button class="tab-btn active" data-tab="overview">Overview</button>
|
||||
<button class="tab-btn" data-tab="rf">RF / Signal</button>
|
||||
<button class="tab-btn" data-tab="topology">Topology</button>
|
||||
@@ -90,7 +90,7 @@
|
||||
<button class="tab-btn" data-tab="prefix-tool">Prefix Tool</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="analyticsContent" class="analytics-content">
|
||||
<div id="analyticsContent" class="analytics-content" aria-live="polite">
|
||||
<div class="text-center text-muted" style="padding:40px">Loading analytics…</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -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 = `<div style="padding:40px;text-align:center;color:#6b7280"><h2>${route}</h2><p>Page not yet implemented.</p></div>`;
|
||||
}
|
||||
|
||||
+2
-2
@@ -754,7 +754,7 @@
|
||||
<label class="audio-slider-label">Vol <input type="range" id="audioVolSlider" min="0" max="100" value="30" class="audio-slider"><span id="audioVolVal">30</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-overlay live-feed" id="liveFeed">
|
||||
<div class="live-overlay live-feed" id="liveFeed" aria-live="polite" aria-relevant="additions" role="log">
|
||||
<button class="feed-hide-btn" id="feedHideBtn" title="Hide feed">✕</button>
|
||||
</div>
|
||||
<button class="feed-show-btn hidden" id="feedShowBtn" title="Show feed">📋</button>
|
||||
@@ -1323,7 +1323,7 @@
|
||||
let html = `
|
||||
<div style="padding:16px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span class="${statusDot}" style="font-size:18px">●</span>
|
||||
<span class="${statusDot}" style="font-size:18px" aria-hidden="true">●</span>
|
||||
<h3 style="margin:0;font-size:16px;font-weight:700;">${escapeHtml(n.name || 'Unknown')}</h3>
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
|
||||
+13
-1
@@ -324,7 +324,7 @@
|
||||
</div>
|
||||
<div id="nodesRegionFilter" class="region-filter-container"></div>
|
||||
<div class="split-layout">
|
||||
<div class="panel-left" id="nodesLeft"></div>
|
||||
<div class="panel-left" id="nodesLeft" aria-live="polite" aria-relevant="additions removals"></div>
|
||||
<div class="panel-right empty" id="nodesRight"><span>Select a node to view details</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -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 = '<span>Select a node to view details</span>';
|
||||
selectedKey = null;
|
||||
renderRows();
|
||||
}
|
||||
});
|
||||
|
||||
renderRows();
|
||||
}
|
||||
|
||||
@@ -1007,6 +1018,7 @@
|
||||
const dupBadge = dupNameBadge(n.name, n.public_key, dupMap);
|
||||
|
||||
panel.innerHTML = `
|
||||
<button class="panel-close-btn" title="Close detail pane (Esc)">✕</button>
|
||||
<div class="node-detail">
|
||||
<div class="node-detail-name">${escapeHtml(n.name || '(unnamed)')}${dupBadge}</div>
|
||||
<div class="node-detail-role">${renderNodeBadges(n, roleColor)}
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@
|
||||
}
|
||||
}
|
||||
app.innerHTML = `<div class="split-layout detail-collapsed">
|
||||
<div class="panel-left" id="pktLeft"></div>
|
||||
<div class="panel-left" id="pktLeft" aria-live="polite" aria-relevant="additions removals"></div>
|
||||
<div class="panel-right empty" id="pktRight" aria-live="polite">
|
||||
<div class="panel-resize-handle" id="pktResizeHandle"></div>
|
||||
${PANEL_CLOSE_HTML}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user