[fix](trx-frontend-http): drop the tabs to icons when the bar runs out of room
CI put the tab strip 9px into the controls at 1440px on a run that passed locally: its system font is wider, and the bar had no move left to make. Controls are moved into the overflow menu until the bar fits, but once all of them were in the menu nothing else gave — the nav may shrink below its content, so the tabs kept full width and ran under the controls, leaving the destinations nearest them unclickable. Labels dropping to icons was the other half of the answer, but it hung off a max-width:1360px media query and so was unavailable at 1440px. That class now goes on by measurement, as the last step after the menu is exhausted, which is the same reasoning the controls' own fit test already uses: how much fits depends on the rig name and on how wide the platform draws the labels, not on the viewport. The class is cleared before measuring so the decision cannot ratchet, and icon widths are fixed, so it always buys back the labels' width. Labels now stay put between 1100px and 1360px while they fit, with the style picker and theme toggle behind the overflow menu instead. The suite could not have caught this: it passed on the fonts of the machine that wrote it. The layout section now repeats its fit check with the bar's text scaled up, which reproduces a wider system font anywhere — with this fix reverted it fails on macOS too. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -818,6 +818,8 @@ function elementById(id) {
|
|||||||
return needed <= bar.clientWidth;
|
return needed <= bar.clientWidth;
|
||||||
};
|
};
|
||||||
const reflowOverflow = () => {
|
const reflowOverflow = () => {
|
||||||
|
const nav = document.querySelector(".tab-bar-nav");
|
||||||
|
nav?.classList.remove("nav-icons-only");
|
||||||
overflowOrder.forEach((selector) => {
|
overflowOrder.forEach((selector) => {
|
||||||
const element = menu.querySelector(selector);
|
const element = menu.querySelector(selector);
|
||||||
if (element) actions.insertBefore(element, wrap);
|
if (element) actions.insertBefore(element, wrap);
|
||||||
@@ -830,6 +832,7 @@ function elementById(id) {
|
|||||||
wrap.hidden = false;
|
wrap.hidden = false;
|
||||||
menu.appendChild(element);
|
menu.appendChild(element);
|
||||||
}
|
}
|
||||||
|
if (nav && !barFits()) nav.classList.add("nav-icons-only");
|
||||||
wrap.hidden = menu.children.length === 0;
|
wrap.hidden = menu.children.length === 0;
|
||||||
if (wrap.hidden) closeMenu();
|
if (wrap.hidden) closeMenu();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1562,11 +1562,17 @@ small { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
.tab-bar-nav .tab { flex: 0 0 auto; }
|
.tab-bar-nav .tab { flex: 0 0 auto; }
|
||||||
/* Icons before scrolling: every tab already carries one, and four icons plus
|
/* Icons before scrolling: every tab already carries one, and four icons plus
|
||||||
* More always fit, so the strip never has to hide a destination. */
|
* More always fit, so the strip never has to hide a destination. Applied by
|
||||||
@media (max-width: 1360px) and (min-width: 701px) {
|
* measurement (ui-core's reflowOverflow) rather than at a viewport width: how
|
||||||
.tab-bar-nav .tab .tab-label { display: none; }
|
* much fits depends on the rig name and on how wide the platform renders the
|
||||||
.tab-bar-nav .tab .tab-icon, .tab-bar-nav .tab .tab-more-icon { display: block; }
|
* labels, so the same width fits on one machine and clips on another.
|
||||||
.tab-bar-nav .tab { padding: 0.5rem 0.6rem; }
|
* Bounded below 761px, where the strip becomes the bottom nav and keeps its
|
||||||
|
* labels under the icons. */
|
||||||
|
@media (min-width: 761px) {
|
||||||
|
.tab-bar-nav.nav-icons-only .tab .tab-label { display: none; }
|
||||||
|
.tab-bar-nav.nav-icons-only .tab .tab-icon,
|
||||||
|
.tab-bar-nav.nav-icons-only .tab .tab-more-icon { display: block; }
|
||||||
|
.tab-bar-nav.nav-icons-only .tab { padding: 0.45rem 0.55rem; }
|
||||||
}
|
}
|
||||||
/* The selected destination is boxed, not underlined — the same treatment the
|
/* The selected destination is boxed, not underlined — the same treatment the
|
||||||
mobile bottom nav already used, so one navigation model reads the same at
|
mobile bottom nav already used, so one navigation model reads the same at
|
||||||
|
|||||||
@@ -422,6 +422,10 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
return needed <= bar.clientWidth;
|
return needed <= bar.clientWidth;
|
||||||
};
|
};
|
||||||
const reflowOverflow = () => {
|
const reflowOverflow = () => {
|
||||||
|
const nav = document.querySelector<HTMLElement>(".tab-bar-nav");
|
||||||
|
// Measure from the roomiest state every time, so the decision is a
|
||||||
|
// function of the current widths alone and cannot ratchet.
|
||||||
|
nav?.classList.remove("nav-icons-only");
|
||||||
overflowOrder.forEach((selector) => {
|
overflowOrder.forEach((selector) => {
|
||||||
const element = menu.querySelector<HTMLElement>(selector);
|
const element = menu.querySelector<HTMLElement>(selector);
|
||||||
if (element) actions.insertBefore(element, wrap);
|
if (element) actions.insertBefore(element, wrap);
|
||||||
@@ -434,6 +438,12 @@ function elementById<T extends HTMLElement>(id: string): T {
|
|||||||
wrap.hidden = false;
|
wrap.hidden = false;
|
||||||
menu.appendChild(element);
|
menu.appendChild(element);
|
||||||
}
|
}
|
||||||
|
// Last resort, once every movable control is already in the menu: drop
|
||||||
|
// the tabs to their icons. Without it the nav — which may shrink below
|
||||||
|
// its content — keeps its tabs at full width and runs them under the
|
||||||
|
// controls, so the destinations nearest the controls become unclickable.
|
||||||
|
// Icon widths are fixed, so this always buys back the labels' width.
|
||||||
|
if (nav && !barFits()) nav.classList.add("nav-icons-only");
|
||||||
wrap.hidden = menu.children.length === 0;
|
wrap.hidden = menu.children.length === 0;
|
||||||
if (wrap.hidden) closeMenu();
|
if (wrap.hidden) closeMenu();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -169,6 +169,34 @@ try {
|
|||||||
assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`);
|
assert.ok(menu.height > 40 && menu.width > 80, `menu rendered ${menu.width}x${menu.height}`);
|
||||||
assert.ok(menu.onTop, "menu is painted underneath the page");
|
assert.ok(menu.onTop, "menu is painted underneath the page");
|
||||||
|
|
||||||
|
// Wider text than this machine renders. The checks above passed on macOS
|
||||||
|
// while CI, whose system font is wider, put the tab strip 9px into the
|
||||||
|
// controls: the bar had run out of controls to move into the overflow menu
|
||||||
|
// and the tabs kept their full width anyway. Scaling the bar's own text
|
||||||
|
// reproduces that on any machine. The tabs carry their font size themselves,
|
||||||
|
// so the parent size alone does not cascade to them.
|
||||||
|
await page.addStyleTag({ content: `
|
||||||
|
.tab-bar { font-size: 160%; }
|
||||||
|
.tab-bar .tab, .tab-bar select, .tab-bar button { font-size: 1.52rem; }
|
||||||
|
` });
|
||||||
|
for (const width of [1440, 1280, 1100]) {
|
||||||
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
const crowded = await page.evaluate(() => {
|
||||||
|
const nav = document.querySelector(".tab-bar-nav");
|
||||||
|
const actions = document.querySelector(".top-bar-actions");
|
||||||
|
const tabs = [...nav.querySelectorAll(".tab")].filter((tab) => tab.offsetParent !== null);
|
||||||
|
return {
|
||||||
|
overlap: Math.round(Math.max(...tabs.map((tab) => tab.getBoundingClientRect().right))
|
||||||
|
- actions.getBoundingClientRect().left),
|
||||||
|
iconsOnly: nav.classList.contains("nav-icons-only"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.ok(crowded.overlap <= 0,
|
||||||
|
`with wider text the tab strip overlaps the controls by ${crowded.overlap}px at ${width}px`);
|
||||||
|
assert.equal(crowded.iconsOnly, true, `the strip kept its labels at ${width}px with no room for them`);
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
await fixture.close();
|
await fixture.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user