Map control bar: fold the toggles in, and stop autofill over the frequency #40

Merged
sjg merged 2 commits from fix/map-control-bar into main 2026-08-05 20:19:55 +02:00
6 changed files with 133 additions and 76 deletions
@@ -1335,7 +1335,8 @@ var mapWindow = window;
function applyMapOverlayPanelVisibility() { function applyMapOverlayPanelVisibility() {
const panel = document.querySelector("#map-stage .map-overlay-panel"); const panel = document.querySelector("#map-stage .map-overlay-panel");
if (!panel) return; if (!panel) return;
panel.classList.toggle("is-hidden", !mapOverlayPanelVisible); panel.classList.toggle("filters-hidden", !mapOverlayPanelVisible);
panel.querySelector(".map-overlay-filters")?.classList.toggle("is-hidden", !mapOverlayPanelVisible);
} }
function updateMapOverlayToggleButton() { function updateMapOverlayToggleButton() {
const btn = mapEl("map-overlay-toggle-btn"); const btn = mapEl("map-overlay-toggle-btn");
@@ -209,11 +209,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="label"><span>Signal strength</span></div> <div class="label"><span>Signal strength</span></div>
</div> </div>
<div class="freq-field frequency-col"> <div class="freq-field frequency-col">
<input class="status-input" id="freq" type="text" value="--" aria-describedby="freq-label" aria-label="Tuned frequency" /> <input class="status-input" id="freq" type="text" value="--" aria-describedby="freq-label" aria-label="Tuned frequency" autocomplete="off" autocorrect="off" spellcheck="false" />
<div class="label" id="freq-label"><span>Frequency</span></div> <div class="label" id="freq-label"><span>Frequency</span></div>
</div> </div>
<div class="freq-field frequency-col center-frequency-col" id="center-freq-field" style="display:none;"> <div class="freq-field frequency-col center-frequency-col" id="center-freq-field" style="display:none;">
<input class="status-input" id="center-freq" type="text" value="--" aria-describedby="center-freq-label" aria-label="SDR center frequency" /> <input class="status-input" id="center-freq" type="text" value="--" aria-describedby="center-freq-label" aria-label="SDR center frequency" autocomplete="off" autocorrect="off" spellcheck="false" />
<div class="label" id="center-freq-label"><span>Center Frequency</span></div> <div class="label" id="center-freq-label"><span>Center Frequency</span></div>
</div> </div>
</div> </div>
@@ -1001,6 +1001,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<template id="tmpl-map"> <template id="tmpl-map">
<div id="map-stage"> <div id="map-stage">
<div class="map-overlay-panel"> <div class="map-overlay-panel">
<div class="map-overlay-filters">
<div class="map-locator-filter-group"> <div class="map-locator-filter-group">
<span class="map-locator-filter-label">Filter by</span> <span class="map-locator-filter-label">Filter by</span>
<div id="map-locator-phase" class="map-locator-phase-row"></div> <div id="map-locator-phase" class="map-locator-phase-row"></div>
@@ -1040,10 +1041,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
</div> </div>
<div class="map-corner-controls"> <div class="map-overlay-actions">
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button> <button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
<button type="button" id="map-overlay-toggle-btn" class="map-overlay-toggle-btn">Hide Filters</button> <button type="button" id="map-overlay-toggle-btn" class="map-overlay-toggle-btn">Hide Filters</button>
</div> </div>
</div>
<div id="map-band-legend" class="map-band-legend" aria-label="Band color legend"></div> <div id="map-band-legend" class="map-band-legend" aria-label="Band color legend"></div>
<div id="aprs-map"></div> <div id="aprs-map"></div>
</div> </div>
@@ -2558,19 +2558,20 @@ button.map-qso-card:focus-visible {
} }
/* A toolbar across the top of the map rather than a panel parked in a corner: /* A toolbar across the top of the map rather than a panel parked in a corner:
it leaves the map itself unobscured, and the bottom-left band legend keeps it leaves the map itself unobscured, and the bottom-left band legend keeps
its place. It stops short of the corner controls, which stay outside it -- its place. Fullscreen and the filter toggle ride at its right-hand end;
the button that hides the filters cannot live inside the thing it hides. */ only the filters themselves collapse, so the button that hides them is
still there to bring them back. */
.map-overlay-panel { .map-overlay-panel {
position: absolute; position: absolute;
top: 0.7rem; top: 0.7rem;
/* Clear of Leaflet's zoom buttons, which draw over the top-left corner. */ /* Clear of Leaflet's zoom buttons, which draw over the top-left corner. */
left: 3.4rem; left: 3.4rem;
right: 9.6rem; right: 0.7rem;
z-index: 410; z-index: 410;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row nowrap;
align-items: center; align-items: center;
gap: 0.35rem 0.5rem; gap: 0.5rem;
width: auto; width: auto;
max-height: calc(100% - 1.4rem); max-height: calc(100% - 1.4rem);
padding: 0.45rem 0.6rem; padding: 0.45rem 0.6rem;
@@ -2584,6 +2585,32 @@ button.map-qso-card:focus-visible {
overflow: auto; overflow: auto;
transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease; transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease;
} }
.map-overlay-filters {
display: flex;
flex: 1 1 auto;
flex-flow: row wrap;
align-items: center;
gap: 0.35rem 0.5rem;
min-width: 0;
}
.map-overlay-filters.is-hidden {
display: none;
}
/* With the filters collapsed the bar has no reason to span the map. */
.map-overlay-panel.filters-hidden {
left: auto;
}
.map-overlay-actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 0.4rem;
}
/* Set off from the filters, but only while there are filters to set off. */
.map-overlay-panel:not(.filters-hidden) .map-overlay-actions {
padding-left: 0.5rem;
border-left: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
}
.map-overlay-panel .map-locator-filter-group { .map-overlay-panel .map-locator-filter-group {
flex: 0 1 auto; flex: 0 1 auto;
/* The label stays beside its control; only the bar itself wraps. */ /* The label stays beside its control; only the bar itself wraps. */
@@ -2628,16 +2655,6 @@ button.map-qso-card:focus-visible {
transform: translateY(0.25rem); transform: translateY(0.25rem);
pointer-events: none; pointer-events: none;
} }
.map-corner-controls {
position: absolute;
top: 0.7rem;
right: 0.7rem;
z-index: 410;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.4rem;
}
.map-fullscreen-btn { .map-fullscreen-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -4166,7 +4183,7 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
top: 0.55rem; top: 0.55rem;
left: 3.25rem; left: 3.25rem;
right: 0.55rem; right: 0.55rem;
flex-direction: column; flex-flow: column nowrap;
align-items: stretch; align-items: stretch;
width: auto; width: auto;
max-height: min(16.5rem, calc(100% - 1.1rem)); max-height: min(16.5rem, calc(100% - 1.1rem));
@@ -4179,15 +4196,14 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
padding-right: 0; padding-right: 0;
border-right: 0; border-right: 0;
} }
.map-overlay-actions {
justify-content: flex-end;
}
.map-overlay-panel .map-paths-hint { .map-overlay-panel .map-paths-hint {
display: block; display: block;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted); color: var(--text-muted);
} }
.map-corner-controls {
top: 0.55rem;
right: 0.55rem;
}
.map-band-legend { .map-band-legend {
left: 0.55rem; left: 0.55rem;
bottom: 0.55rem; bottom: 0.55rem;
@@ -1637,7 +1637,10 @@ const mapWindow = window as unknown as MapWindow;
function applyMapOverlayPanelVisibility() { function applyMapOverlayPanelVisibility() {
const panel = document.querySelector("#map-stage .map-overlay-panel"); const panel = document.querySelector("#map-stage .map-overlay-panel");
if (!panel) return; if (!panel) return;
panel.classList.toggle("is-hidden", !mapOverlayPanelVisible); // Only the filters collapse. The bar itself stays, because it carries the
// button that brings them back.
panel.classList.toggle("filters-hidden", !mapOverlayPanelVisible);
panel.querySelector(".map-overlay-filters")?.classList.toggle("is-hidden", !mapOverlayPanelVisible);
} }
function updateMapOverlayToggleButton() { function updateMapOverlayToggleButton() {
@@ -376,6 +376,18 @@ try {
`text grew without a resize and the strip overlaps by ${unresized.overlap}px`); `text grew without a resize and the strip overlaps by ${unresized.overlap}px`);
assert.equal(unresized.iconsOnly, true, "the strip kept its labels with no room for them"); assert.equal(unresized.iconsOnly, true, "the strip kept its labels with no room for them");
// The frequency readouts are typed into, so the browser remembers what went
// in and offers it back in a dropdown over the reading — Edge does this by
// default. Nothing here wants to be autofilled from what was tuned last week.
const autofill = await page.evaluate(() => ["freq", "center-freq"].map((id) => {
const input = document.getElementById(id);
return { id, autocomplete: input?.getAttribute("autocomplete") ?? null };
}));
for (const field of autofill) {
assert.equal(field.autocomplete, "off",
`#${field.id} offers autofill (autocomplete=${field.autocomplete})`);
}
} finally { } finally {
await browser.close(); await browser.close();
await fixture.close(); await fixture.close();
@@ -225,9 +225,11 @@ try {
// The map's filter panel is a bar across the top of the map, not a window // The map's filter panel is a bar across the top of the map, not a window
// sitting on it: it has to stay one or two rows tall, span most of the width, // sitting on it: it has to stay one or two rows tall, span most of the width,
// and keep clear of the things that share the map's corners — Leaflet's zoom // and keep clear of what shares the map's corners — Leaflet's zoom buttons and
// buttons, the Fullscreen/Hide Filters controls, and the band legend. A panel // the band legend. A panel that grew a column would cover the map it filters.
// that grew a column would cover the map it filters. // Fullscreen and the filter toggle ride at the bar's right-hand end, so only
// the filters collapse: the bar itself has to survive Hide Filters, or there
// is nothing left to click to bring them back.
const mapFixture = await startWebFixture({ spectrum: true }); const mapFixture = await startWebFixture({ spectrum: true });
const mapView = await startBrowser(chromium); const mapView = await startBrowser(chromium);
try { try {
@@ -242,7 +244,6 @@ try {
const panel = document.querySelector(".map-overlay-panel"); const panel = document.querySelector(".map-overlay-panel");
const stage = box(document.getElementById("map-stage")); const stage = box(document.getElementById("map-stage"));
const zoom = box(document.querySelector("#aprs-map .leaflet-control-zoom")); const zoom = box(document.querySelector("#aprs-map .leaflet-control-zoom"));
const corner = box(document.querySelector(".map-corner-controls"));
const legend = box(document.getElementById("map-band-legend")); const legend = box(document.getElementById("map-band-legend"));
const panelBox = box(panel); const panelBox = box(panel);
const hits = (a, b) => !!a && !!b const hits = (a, b) => !!a && !!b
@@ -252,8 +253,10 @@ try {
height: Math.round(panelBox.height), height: Math.round(panelBox.height),
outsideStage: panelBox.right > stage.right + 1 || panelBox.bottom > stage.bottom + 1, outsideStage: panelBox.right > stage.right + 1 || panelBox.bottom > stage.bottom + 1,
hitsZoom: hits(panelBox, zoom), hitsZoom: hits(panelBox, zoom),
hitsCorner: hits(panelBox, corner),
hitsLegend: hits(panelBox, legend), hitsLegend: hits(panelBox, legend),
// Both controls belong to the bar now, not to a floating corner block.
actionsInBar: [...panel.querySelectorAll(".map-overlay-actions button")]
.map((button) => button.id).join(","),
clipped: panel.scrollWidth > panel.clientWidth + 1 || panel.scrollHeight > panel.clientHeight + 1, clipped: panel.scrollWidth > panel.clientWidth + 1 || panel.scrollHeight > panel.clientHeight + 1,
}; };
}); });
@@ -261,21 +264,41 @@ try {
assert.ok(bar.height <= 140, `the filter bar is ${bar.height}px tall at ${width}px, not a bar`); assert.ok(bar.height <= 140, `the filter bar is ${bar.height}px tall at ${width}px, not a bar`);
assert.equal(bar.outsideStage, false, `the filter bar runs off the map at ${width}px`); assert.equal(bar.outsideStage, false, `the filter bar runs off the map at ${width}px`);
assert.equal(bar.hitsZoom, false, `the filter bar covers the zoom buttons at ${width}px`); assert.equal(bar.hitsZoom, false, `the filter bar covers the zoom buttons at ${width}px`);
assert.equal(bar.hitsCorner, false, `the filter bar covers the map controls at ${width}px`); assert.equal(bar.actionsInBar, "map-fullscreen-btn,map-overlay-toggle-btn",
`the bar carries "${bar.actionsInBar}" at ${width}px`);
assert.equal(bar.hitsLegend, false, `the filter bar covers the band legend at ${width}px`); assert.equal(bar.hitsLegend, false, `the filter bar covers the band legend at ${width}px`);
assert.equal(bar.clipped, false, `the filter bar is clipping its own controls at ${width}px`); assert.equal(bar.clipped, false, `the filter bar is clipping its own controls at ${width}px`);
} }
// Hiding it still works, and gives the whole map back. // Hiding gives the map back, but leaves the bar itself — collapsed to its
// two controls at the right-hand edge — so the filters can be brought back.
await mapView.page.locator("#map-overlay-toggle-btn").click(); await mapView.page.locator("#map-overlay-toggle-btn").click();
await mapView.page.waitForTimeout(400); await mapView.page.waitForTimeout(400);
const toggled = await mapView.page.evaluate(() => ({ const toggled = await mapView.page.evaluate(() => {
hidden: document.querySelector(".map-overlay-panel").classList.contains("is-hidden"), const panel = document.querySelector(".map-overlay-panel");
const stage = document.getElementById("map-stage").getBoundingClientRect();
const panelBox = panel.getBoundingClientRect();
const visible = (id) => document.getElementById(id).getBoundingClientRect().width > 0;
return {
filtersHidden: panel.querySelector(".map-overlay-filters").classList.contains("is-hidden"),
widthPct: Math.round((panelBox.width / stage.width) * 100),
rightGap: Math.round(stage.right - panelBox.right),
label: document.getElementById("map-overlay-toggle-btn").textContent.trim(), label: document.getElementById("map-overlay-toggle-btn").textContent.trim(),
})); togglesVisible: visible("map-fullscreen-btn") && visible("map-overlay-toggle-btn"),
assert.equal(toggled.hidden, true, "the filter bar stayed up after Hide Filters"); };
});
assert.equal(toggled.filtersHidden, true, "the filters stayed up after Hide Filters");
assert.equal(toggled.togglesVisible, true, "Hide Filters took its own button down with it");
assert.ok(toggled.widthPct < 30, `the collapsed bar still covers ${toggled.widthPct}% of the map`);
assert.ok(toggled.rightGap < 30, `the collapsed bar sits ${toggled.rightGap}px from the map's edge`);
assert.equal(toggled.label, "Show Filters", `the toggle still reads "${toggled.label}"`); assert.equal(toggled.label, "Show Filters", `the toggle still reads "${toggled.label}"`);
await mapView.page.locator("#map-overlay-toggle-btn").click();
await mapView.page.waitForTimeout(400);
const restored = await mapView.page.evaluate(() =>
!document.querySelector(".map-overlay-filters").classList.contains("is-hidden"));
assert.equal(restored, true, "Show Filters did not bring the filters back");
assert.deepEqual(mapView.runtimeErrors, []); assert.deepEqual(mapView.runtimeErrors, []);
} finally { } finally {
await mapView.browser.close(); await mapView.browser.close();