[fix](trx-frontend-http): let the bottom nav's labels fit inside its tabs
CI / lint (pull_request) Successful in 2m17s
CI / test (pull_request) Successful in 8m22s
CI / frontend (pull_request) Failing after 1m46s
CI / reuse (pull_request) Successful in 2s

The bottom nav keeps its labels under the icons -- that is what makes it
navigation rather than five glyphs -- but the labels did not fit the
tabs.  On a 360 px screen "Bookmarks" and "Digital modes" were cut off
mid-word and ran into each other: "Bookmarks igital mode".

The stylesheet already meant to handle it.  Three rules shrink the long
labels, and a rule twenty lines further down sets the size for all of
them; identical specificity, later in the file, so the blanket rule won
and nothing was ever shortened.  Those rules now come after it.

Beyond that, tabs sized themselves to their own labels, so "Map" and
"Digital modes" were given the same room.  They divide the bar evenly
now, which is the shape of every bottom nav, and a label that still runs
long ellipsises rather than being cut through a letter.

"Digital modes" does not fit at any size worth reading, so the nav shows
"Digital" and the button carries the full name as its accessible label;
the short form is hidden from assistive tech, which reads the button's
name instead.

The phone layout test now checks that no label in the nav is cut off at
430, 390 or 360 px, and that the shortened tab still says what it is to
something that reads the page rather than looks at it.

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
sjg
2026-08-06 19:14:31 +02:00
parent 14ad6e241b
commit 6284747339
3 changed files with 76 additions and 6 deletions
@@ -52,9 +52,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
<svg class="tab-icon" aria-hidden="true"><use href="#icon-bookmark"/></svg> <svg class="tab-icon" aria-hidden="true"><use href="#icon-bookmark"/></svg>
<span class="tab-label">Bookmarks</span> <span class="tab-label">Bookmarks</span>
</button> </button>
<button class="tab" data-tab="digital-modes"> <button class="tab" data-tab="digital-modes" aria-label="Digital modes">
<svg class="tab-icon" aria-hidden="true"><use href="#icon-digital"/></svg> <svg class="tab-icon" aria-hidden="true"><use href="#icon-digital"/></svg>
<span class="tab-label">Digital modes</span> <span class="tab-label">Digital modes</span>
<!-- A fifth of a phone screen does not hold "Digital modes"; the
button keeps the full name for assistive tech. -->
<span class="tab-label-short" aria-hidden="true">Digital</span>
</button> </button>
<button class="tab" data-tab="map"> <button class="tab" data-tab="map">
<svg class="tab-icon" aria-hidden="true"><use href="#icon-map"/></svg> <svg class="tab-icon" aria-hidden="true"><use href="#icon-map"/></svg>
@@ -1717,6 +1717,8 @@ small { color: var(--text-muted); }
flex-shrink: 0; flex-shrink: 0;
} }
.tab-label { display: block; } .tab-label { display: block; }
/* Only the bottom nav is short of room. */
.tab-label-short { display: none; }
.tab-more-icon { display: none; } .tab-more-icon { display: none; }
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; } .about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; } .about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
@@ -4028,10 +4030,6 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent); box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
} }
.tab-icon, .tab-more-icon { display: block; } .tab-icon, .tab-more-icon { display: block; }
/* Shorten long tab labels to keep bottom nav compact */
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="statistics"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="statistics"], .tab[data-tab="recorder"], .tab[data-tab="statistics"], .tab[data-tab="recorder"],
.tab[data-tab="settings"], .tab[data-tab="about"] { display: none; } .tab[data-tab="settings"], .tab[data-tab="about"] { display: none; }
.mobile-more-btn { display: flex; } .mobile-more-btn { display: flex; }
@@ -4045,7 +4043,42 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
color: var(--accent-text); color: var(--accent-text);
background: color-mix(in srgb, var(--accent-green) 10%, transparent); background: color-mix(in srgb, var(--accent-green) 10%, transparent);
} }
.tab .tab-label, .tab[data-tab] .tab-label { font-size: 0.75rem; } /* The bottom nav divides the bar between its five destinations, so a tab is
about a fifth of the screen and the label has to live inside that. Sharing
the width evenly beats sizing each tab to its own label, which gave
"Digital modes" the same room as "Map" and clipped it mid-word. */
.tab-bar-nav .tab {
flex: 1 1 0;
min-width: 0;
padding-left: 0.15rem;
padding-right: 0.15rem;
}
.tab .tab-label, .tab[data-tab] .tab-label {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.75rem;
}
/* The long ones, smaller so they fit whole rather than ellipsised. These
used to sit above the rule that sets the size for all of them — same
specificity, so the later one won and nothing was ever shortened. */
.tab[data-tab="bookmarks"] .tab-label,
.tab[data-tab="statistics"] .tab-label {
font-size: 0.6rem;
}
/* "Digital modes" does not fit at any size worth reading, so the nav shows
the short form and the button carries the full one as its label. */
.tab[data-tab="digital-modes"] .tab-label { display: none; }
.tab[data-tab="digital-modes"] .tab-label-short {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.6rem;
}
.decoder-tab-select { .decoder-tab-select {
display: block; display: block;
width: 100%; width: 100%;
@@ -59,8 +59,42 @@ try {
// else into the overflow menu. // else into the overflow menu.
assert.ok(layout.rigSelect <= 112, assert.ok(layout.rigSelect <= 112,
`the rig picker is ${layout.rigSelect}px wide at ${width}px`); `the rig picker is ${layout.rigSelect}px wide at ${width}px`);
// The bottom nav keeps its labels — that is what makes it navigation
// rather than five glyphs — so the labels have to fit inside a tab, which
// is a fifth of a phone. They were being cut off mid-word: "igital mode".
const nav = await page.evaluate(() => {
const bar = document.querySelector(".tab-bar-nav");
return [...bar.querySelectorAll(".tab")]
.filter((tab) => tab.getBoundingClientRect().width > 0)
.map((tab) => {
const label = [...tab.querySelectorAll(".tab-label, .tab-label-short")]
.find((span) => getComputedStyle(span).display !== "none");
return {
text: (label?.textContent ?? "").trim(),
clipped: label ? label.scrollWidth > label.clientWidth + 1 : false,
};
});
});
assert.ok(nav.length >= 4, `the bottom nav has ${nav.length} destinations at ${width}px`);
const clipped = nav.filter((tab) => tab.clipped).map((tab) => tab.text);
assert.deepEqual(clipped, [],
`labels cut off at ${width}px: ${clipped.join(", ")}`);
} }
// A tab whose visible label is shortened for the nav still has to say what
// it is to anything that reads the page rather than looks at it.
const named = await page.evaluate(() => {
const tab = document.querySelector('.tab[data-tab="digital-modes"]');
const short = tab.querySelector(".tab-label-short");
return {
label: tab.getAttribute("aria-label"),
shortHidden: short?.getAttribute("aria-hidden"),
};
});
assert.equal(named.label, "Digital modes", "the shortened tab lost its full name");
assert.equal(named.shortHidden, "true", "the short label is read out as well as shown");
// Hiding the map's filters leaves the bar, and the bar has to still be // Hiding the map's filters leaves the bar, and the bar has to still be
// there to bring them back — it carries the only button that does. // there to bring them back — it carries the only button that does.
await page.setViewportSize({ width: 1400, height: 900 }); await page.setViewportSize({ width: 1400, height: 900 });