fix(#1029): use getBoundingClientRect for tab overflow cutoff

The old measurement used tabbar.clientWidth which includes horizontal
padding (~2em / ~30px total), causing an over-allocation of the same
amount. Flex gap between tabs (0.2em) was also not counted in the
accumulated offsetWidth, adding another ~20px of error. Together these
caused the algorithm to leave too many tabs visible, overflowing the
bar and pushing the ⋮ button off the right edge of the screen.

Replace the clientWidth + cumulative-offsetWidth approach with
getBoundingClientRect: the cutoff is the tabbar's actual right edge
minus its right padding, minus a reserve for the overflow wrapper.
Individual tabs are overflowed once their right edge exceeds the
cutoff; subsequent tabs are overflowed unconditionally to keep the
visible set contiguous and left-anchored.
This commit is contained in:
iris 2026-06-04 19:56:34 +02:00 committed by mara
commit 0723737d89

View file

@ -3730,20 +3730,32 @@ window.marked = marked;
// Step 1: un-overflow all dynamic tabs so we can measure natural widths.
dynamic.forEach(t => t.classList.remove('tab-overflowed'));
// Step 2: measure available width (bar width minus overflow-btn width).
// We must read layout AFTER restoring all dynamic tabs.
// overflowBtn may be hidden (offsetWidth=0) on the very first call; fall
// back to a 32px estimate so the first-pass measurement isn't off.
const btnW = overflowBtn.offsetWidth || 32;
const availWidth = tabbar.clientWidth - btnW - 8;
// Step 2: compute the right boundary where visible tabs must end.
// getBoundingClientRect is used instead of clientWidth because
// clientWidth includes the tabbar's horizontal padding (~2em total),
// causing an over-allocation of ~2em; flex gap between tabs is also
// not captured by offsetWidth accumulation. Together these pushed
// the ⋮ button off the right edge of the screen.
const tabbarRect = tabbar.getBoundingClientRect();
const padR = parseFloat(getComputedStyle(tabbar).paddingRight) || 0;
// Right edge of the flex content area (inside right padding).
// Fall back to clientWidth-based estimate when the rect is zero
// (element not in layout, e.g. display:none ancestor).
const contentRight = tabbarRect.width > 0
? (tabbarRect.right - padR)
: (tabbar.clientWidth - padR);
// Space to reserve for the overflow wrapper. Use its actual offsetWidth
// when available; fall back to 40px on the first call (button hidden).
const btnReserve = (overflowWrap.offsetWidth || 40) + 4;
const cutoffRight = contentRight - btnReserve;
// Step 3: walk dynamic tabs left-to-right, accumulating widths.
// Any that go over the available width get overflowed.
let cumWidth = 0;
// Step 3: any tab whose right edge exceeds the cutoff is overflowed,
// along with all subsequent tabs (keeps the visible set contiguous
// and left-anchored). Once the first offending tab is found, all
// following tabs are also overflowed without re-measuring.
const dynamicOverflow = [];
for (const tab of dynamic) {
cumWidth += tab.offsetWidth;
if (cumWidth > availWidth) {
if (dynamicOverflow.length > 0 || tab.getBoundingClientRect().right > cutoffRight) {
dynamicOverflow.push(tab);
}
}