From cff995e00c95cc1ff6a7b4ee4fd594248a153d42 Mon Sep 17 00:00:00 2001 From: you Date: Sat, 21 Mar 2026 06:57:48 +0000 Subject: [PATCH] region filter: add label, ARIA, dropdown mode for >4 regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'Region:' label before filter controls - ARIA roles: group with aria-label, checkbox roles on pills, aria-checked - When >4 regions: render multi-select dropdown with checkboxes - Trigger shows summary (All / selected names / N selected) - All option at top, click-outside closes - Pill bar mode unchanged for ≤4 regions (just added label + ARIA) --- public/index.html | 2 +- public/region-filter.js | 134 +++++++++++++++++++++++++++++++--------- public/style.css | 22 +++++++ 3 files changed, 127 insertions(+), 31 deletions(-) diff --git a/public/index.html b/public/index.html index 7fb35c39..19a9db1c 100644 --- a/public/index.html +++ b/public/index.html @@ -80,7 +80,7 @@ - + diff --git a/public/region-filter.js b/public/region-filter.js index 25725e7f..b2c7ef98 100644 --- a/public/region-filter.js +++ b/public/region-filter.js @@ -65,22 +65,47 @@ return p ? '®ion=' + encodeURIComponent(p) : ''; } - /** Render the filter bar into a container element */ - function render(container) { - var codes = Object.keys(_regions); - if (codes.length < 2) { - container.innerHTML = ''; - container.style.display = 'none'; - return; + /** Handle a region toggle (shared logic for both pill and dropdown modes) */ + function toggleRegion(region, codes, container) { + if (region === '__all__') { + _selected = null; + } else { + if (!_selected) { + _selected = new Set([region]); + } else if (_selected.has(region)) { + _selected.delete(region); + if (_selected.size === 0) _selected = null; + } else { + _selected.add(region); + } + if (_selected && _selected.size === codes.length) _selected = null; } - container.style.display = ''; + saveToStorage(); + render(container); + _listeners.forEach(function (fn) { fn(getSelected()); }); + } + + /** Build summary label for dropdown trigger */ + function dropdownLabel(codes) { + if (!_selected) return 'All'; + var sel = Array.from(_selected); + if (sel.length === 0) return 'All'; + if (sel.length <= 2) return sel.map(function (c) { return _regions[c] || c; }).join(', '); + return sel.length + ' selected'; + } + + /** Render pill bar mode (≤4 regions) */ + function renderPills(container, codes) { var allSelected = !_selected; - var html = '
'; - html += ''; + var html = '
'; + html += 'Region:'; + html += ''; codes.forEach(function (code) { var label = _regions[code] || code; var active = allSelected || (_selected && _selected.has(code)); - html += ''; + html += ''; }); html += '
'; container.innerHTML = html; @@ -88,28 +113,77 @@ container.onclick = function (e) { var btn = e.target.closest('[data-region]'); if (!btn) return; - var region = btn.dataset.region; - if (region === '__all__') { - _selected = null; - } else { - if (!_selected) { - // Switch from "all" to just this one - _selected = new Set([region]); - } else if (_selected.has(region)) { - _selected.delete(region); - if (_selected.size === 0) _selected = null; // back to all - } else { - _selected.add(region); - } - // If all individually selected, switch to "all" mode - if (_selected && _selected.size === codes.length) _selected = null; - } - saveToStorage(); - render(container); - _listeners.forEach(function (fn) { fn(getSelected()); }); + toggleRegion(btn.dataset.region, codes, container); }; } + /** Render dropdown mode (>4 regions) */ + function renderDropdown(container, codes) { + var allSelected = !_selected; + var html = '
'; + html += 'Region:'; + html += ''; + html += '
'; + container.innerHTML = html; + + var trigger = container.querySelector('.region-dropdown-trigger'); + var menu = container.querySelector('.region-dropdown-menu'); + + trigger.onclick = function () { + var open = !menu.hidden; + menu.hidden = open; + trigger.setAttribute('aria-expanded', String(!open)); + }; + + menu.onchange = function (e) { + var input = e.target; + if (!input.dataset.region) return; + toggleRegion(input.dataset.region, codes, container); + }; + + // Close on outside click + function onDocClick(e) { + if (!container.contains(e.target)) { + menu.hidden = true; + trigger.setAttribute('aria-expanded', 'false'); + } + } + document.addEventListener('click', onDocClick, true); + container._regionCleanup = function () { + document.removeEventListener('click', onDocClick, true); + }; + } + + /** Render the filter bar into a container element */ + function render(container) { + // Clean up previous outside-click listener if any + if (container._regionCleanup) { container._regionCleanup(); container._regionCleanup = null; } + + var codes = Object.keys(_regions); + if (codes.length < 2) { + container.innerHTML = ''; + container.style.display = 'none'; + return; + } + container.style.display = ''; + + if (codes.length > 4) { + renderDropdown(container, codes); + } else { + renderPills(container, codes); + } + } + /** Subscribe to selection changes. Callback receives selected array or null */ function onChange(fn) { _listeners.push(fn); diff --git a/public/style.css b/public/style.css index 9b74f4ee..2e4baa4d 100644 --- a/public/style.css +++ b/public/style.css @@ -1461,3 +1461,25 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); } background: var(--accent); color: #fff; border-color: var(--accent); } .region-pill-active:hover { opacity: 0.85; } +.region-filter-label { + font-size: 12px; font-weight: 600; color: var(--text-muted); align-self: center; + margin-right: 2px; user-select: none; +} +.region-dropdown-wrap { position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 8px 0; } +.region-dropdown-trigger { + display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 16px; + font-size: 12px; font-weight: 500; cursor: pointer; border: 1.5px solid var(--border); + background: transparent; color: var(--text); transition: all 0.15s; +} +.region-dropdown-trigger:hover { border-color: var(--accent); color: var(--accent); } +.region-dropdown-menu { + position: absolute; top: 100%; left: 0; z-index: 90; + min-width: 170px; max-height: 260px; overflow-y: auto; + background: var(--card-bg, #fff); border: 1px solid var(--border); border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); padding: 4px 0; +} +.region-dropdown-item { + display: flex; align-items: center; gap: 6px; padding: 6px 12px; + font-size: 12px; cursor: pointer; color: var(--text); +} +.region-dropdown-item:hover { background: var(--row-hover, #f5f5f5); }