From 677019b53be6954d42f7ed130eedc426ce0b0736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 23:50:30 +0200 Subject: [PATCH] fix(onboarding): let the confirm screen's sex select shrink at 390 px MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The string-headroom sweep went red on CI for French at phone width by two pixels the Mac never showed: the runner's fallback fonts render a touch wider. The box was the sex select on the confirm screen — its value is whitespace-nowrap, and a grid item's automatic minimum is its min-content, so the track was pinned at the padded option's width and the shell scrolled sideways. Both cells of that row and the trigger carry min-w-0 now: the track takes the free space and the value ellipsises, which is the select primitive's own design. Reproduced and verified locally by narrowing the viewport to 372 px, which stands in for the wider fonts (336 px of scroll before, none after). The sweep now names what overflowed: the boxes past the shell's edge, with tag, slot, classes, text prefix and edges, ride the failure message and the outside list, so the next red run says what rather than only that. --- e2e/setup-flow-locale-overflow.spec.ts | 42 +++++++++++++++---- src/components/onboarding/baseline-fields.tsx | 11 ++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/e2e/setup-flow-locale-overflow.spec.ts b/e2e/setup-flow-locale-overflow.spec.ts index 5dabc891d..81977d71c 100644 --- a/e2e/setup-flow-locale-overflow.spec.ts +++ b/e2e/setup-flow-locale-overflow.spec.ts @@ -118,6 +118,36 @@ async function padAndMeasure(page: Page) { scrollWidth: el.scrollWidth, clientWidth: el.clientWidth, })); + /** + * A name for the box that overflowed, so a red run says WHAT rather than + * only that the page grew: tag, `data-slot`, the first classes, the text + * it starts with, and its edges. The CI runner's fallback fonts render a + * pixel or two wider than a Mac's, so the culprit is often invisible + * locally and the message is the whole diagnosis. + */ + const describe = (el: HTMLElement, r: DOMRect) => ({ + tag: el.tagName.toLowerCase(), + slot: el.dataset.slot ?? null, + classes: el.className.toString().split(/\s+/).slice(0, 4).join(" "), + text: (el.textContent ?? "").trim().slice(0, 40), + left: Math.round(r.left), + right: Math.round(r.right), + width: Math.round(r.width), + }); + const shellRight = shell.getBoundingClientRect().right; + // Every box that reaches past the shell's own right edge, widest first, + // deepest first among equals (the leaf is the culprit, its ancestors + // merely grew with it). + const widest = [...shell.querySelectorAll("*")] + .map((el) => ({ el, r: el.getBoundingClientRect() })) + .filter(({ r }) => r.width > 0 && r.right > shellRight + 1) + .sort( + (a, b) => + b.r.right - a.r.right || b.el.children.length - a.el.children.length, + ) + .reverse() + .slice(0, 6) + .map(({ el, r }) => describe(el, r)); const outside = [ ...shell.querySelectorAll( "button, input, select, textarea, a[href], [tabindex]:not([tabindex='-1'])", @@ -126,11 +156,7 @@ async function padAndMeasure(page: Page) { .map((el) => ({ el, r: el.getBoundingClientRect() })) .filter(({ r }) => r.width > 0 && r.height > 0) .filter(({ r }) => r.right > window.innerWidth + 1 || r.left < -1) - .map(({ el, r }) => ({ - slot: el.dataset.slot ?? el.tagName.toLowerCase(), - left: Math.round(r.left), - right: Math.round(r.right), - })); + .map(({ el, r }) => describe(el, r)); return { padded, @@ -138,6 +164,7 @@ async function padAndMeasure(page: Page) { pageClientWidth: doc.clientWidth, shellScrollWidth: shell.scrollWidth, shellClientWidth: shell.clientWidth, + widest, clippers, outside, }; @@ -191,13 +218,14 @@ test.describe("setup flow — every locale at 30% longer strings", () => { result!.padded, `${label}: nothing was padded, so this measured nothing`, ).toBeGreaterThan(3); + const culprits = JSON.stringify(result!.widest); expect( result!.pageScrollWidth, - `${label}: the page scrolls sideways`, + `${label}: the page scrolls sideways; past the shell's edge: ${culprits}`, ).toBeLessThanOrEqual(result!.pageClientWidth + 1); expect( result!.shellScrollWidth, - `${label}: the shell scrolls sideways`, + `${label}: the shell scrolls sideways; past the shell's edge: ${culprits}`, ).toBeLessThanOrEqual(result!.shellClientWidth + 1); expect(result!.clippers, `${label}: text is clipped`).toEqual([]); expect( diff --git a/src/components/onboarding/baseline-fields.tsx b/src/components/onboarding/baseline-fields.tsx index 5eaf113e5..a2190b783 100644 --- a/src/components/onboarding/baseline-fields.tsx +++ b/src/components/onboarding/baseline-fields.tsx @@ -95,8 +95,16 @@ export function BaselineFields({ /> + {/* `min-w-0` on both cells: a grid item's automatic minimum is its + min-content, and the sex select's value is `whitespace-nowrap` — a + locale whose longest option is a third longer, or a runner whose + fallback fonts render two pixels wider, then pinned the track past + the card at 390 px (the string-headroom sweep caught it on CI). With + the minimum released the track takes the free space and the value + ellipsises, which is the select primitive's own design. */}