[fix](trx-frontend-http): tidy up the map's filter bar
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m7s
CI / frontend (push) Successful in 2m57s
CI / reuse (push) Successful in 3s
CI / frontend (pull_request) Successful in 3m47s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m19s

The bar explained itself in prose: "All bands visible by default" sat
between the chips and the next group, taking width the bar could not
spare and reading as a stray line of text. An "All" chip says the same
thing in a chip's width and gives the selection somewhere to be undone.

Band chips also came up dimmed at the very moment every band was on the
map -- an empty selection is no filter at all, so nothing is dimmed
until something is picked. The path toggles drop their "On"/"Off"
suffix, which cost most of a row and only repeated what their own
highlight already said; state moves to aria-pressed and the tooltip.

The rest is alignment. The rule dividing the buttons from the filters
is drawn on the button block's edge, and a centred block left it
floating as a stub beside a two-row bar; stacked, it lay down the left
of a block that sits underneath. The labels sat at their natural
widths, so each row's first control started somewhere different, and
the two pairs of phase buttons differed in width, so the groups after
them missed each other by four pixels. One gutter for every label, one
width for both pairs, and the search field moved last where it can take
the room the fixed-width groups leave.

The map layout test now covers the chips, the divider's height and the
rows' shared start.

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit was merged in pull request #41.
This commit is contained in:
sjg
2026-08-05 21:43:57 +02:00
parent 90ab7781ad
commit 09634eb851
5 changed files with 209 additions and 53 deletions
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
import { chromium } from "playwright-core";
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
/* global document, getComputedStyle */
/* global document, getComputedStyle, window */
const BOOKMARKS = [
{ id: "b1", name: "40m FT8", freq_hz: 7074000, mode: "DIG", category: "Digital", comment: "", locator: "" },
@@ -257,6 +257,24 @@ try {
// 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(","),
// The rule dividing them from the filters is drawn on this block's
// edge, so the block has to run the height of the filters beside it —
// centred, it left the rule floating as a stub against a two-row bar.
actionsShort: Math.round(box(panel.querySelector(".map-overlay-filters")).height
- box(panel.querySelector(".map-overlay-actions")).height),
// Every label sits in a gutter of one width, so whichever group leads
// a row leads it from the same place: with the labels at their natural
// widths, a row starting "Search" began 10px off one starting "Filter".
rowStarts: (() => {
const leaders = new Map();
for (const group of panel.querySelectorAll(".map-overlay-filters .map-locator-filter-group")) {
const rect = box(group);
// Groups of a row differ in height, so bucket them by their middle.
const row = Math.round((rect.top + rect.height / 2) / 20);
if (!leaders.has(row) || rect.left < box(leaders.get(row)).left) leaders.set(row, group);
}
return [...leaders.values()].map((group) => Math.round(box(group.children[1]).left));
})(),
clipped: panel.scrollWidth > panel.clientWidth + 1 || panel.scrollHeight > panel.clientHeight + 1,
};
});
@@ -266,10 +284,66 @@ try {
assert.equal(bar.hitsZoom, false, `the filter bar covers the zoom buttons at ${width}px`);
assert.equal(bar.actionsInBar, "map-fullscreen-btn,map-overlay-toggle-btn",
`the bar carries "${bar.actionsInBar}" at ${width}px`);
assert.ok(bar.actionsShort <= 1,
`the buttons' divider falls ${bar.actionsShort}px short of the bar at ${width}px`);
assert.equal(new Set(bar.rowStarts).size, 1,
`the bar's rows start at ${bar.rowStarts.join(", ")}px 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`);
}
// Band chips only exist once something has been heard on a band. The bar
// used to explain "all bands visible by default" in a line of prose wedged
// between the chips and the next group, which is neither what a toolbar is
// for nor a width it can spare: an "All" chip says it and undoes a
// selection. Nothing is dimmed while nothing is filtered out, either — every
// chip used to come up greyed at the very moment all of them were showing.
await mapView.page.evaluate(() => {
const ts = Date.now();
for (const [grid, hz] of [["JO94", 14_074_000], ["JN48", 7_074_000], ["FN42", 21_074_000]]) {
window.trxPluginRuntime.dispatch("ft8",
{ message: `CQ TEST ${grid}`, grid, freq_hz: hz, snr_db: -8, ts_ms: ts, rig_id: "rig-a" });
}
});
await mapView.page.waitForTimeout(1500);
const chipRow = () => mapView.page.evaluate(() => {
const row = document.getElementById("map-locator-choice-filter");
const chips = [...row.querySelectorAll(".map-locator-chip")];
const all = row.querySelector(".map-locator-chip-all");
return {
bands: chips.filter((chip) => chip !== all).map((chip) => chip.textContent.trim()),
prose: row.querySelector(".map-locator-empty")?.textContent ?? null,
allActive: all?.classList.contains("is-active") ?? null,
dimmed: chips.filter((chip) => chip.classList.contains("is-inactive"))
.map((chip) => chip.textContent.trim()),
selected: chips.filter((chip) => chip.getAttribute("aria-pressed") === "true"
&& chip !== all).map((chip) => chip.textContent.trim()),
};
});
const unfiltered = await chipRow();
assert.ok(unfiltered.bands.includes("20m") && unfiltered.bands.includes("40m"),
`the chip row is showing ${unfiltered.bands.join(",")}`);
assert.equal(unfiltered.prose, null, `the bar is explaining itself in prose: "${unfiltered.prose}"`);
assert.equal(unfiltered.allActive, true, "All is not lit while every band is on the map");
assert.deepEqual(unfiltered.dimmed, [], `${unfiltered.dimmed.join(",")} came up dimmed with no filter set`);
await mapView.page.locator('#map-locator-choice-filter .map-locator-chip[data-filter-key="20m"]').click();
await mapView.page.waitForTimeout(300);
const filtered = await chipRow();
assert.deepEqual(filtered.selected, ["20m"], `picking 20m selected ${filtered.selected.join(",")}`);
assert.equal(filtered.allActive, false, "All stayed lit with a band picked out");
assert.ok(filtered.dimmed.includes("40m"), "the bands now filtered out are not shown as such");
// And back: All is how a selection is undone without hunting for the chips
// that are in it.
await mapView.page.locator('#map-locator-choice-filter .map-locator-chip-all').click();
await mapView.page.waitForTimeout(300);
const cleared = await chipRow();
assert.equal(cleared.allActive, true, "All did not clear the band selection");
assert.deepEqual(cleared.selected, [], `${cleared.selected.join(",")} survived All`);
assert.deepEqual(cleared.dimmed, [], `${cleared.dimmed.join(",")} stayed dimmed after All`);
// 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();