From 5c5a592177fdd50629c8446fa18484dcc1ab0a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 16:49:18 +0200 Subject: [PATCH] Let the setup flow's action rows wrap, and measure headroom per word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At 390 px with labels a third longer the action row pushed "Confirm and continue" past the viewport; both rows now wrap. The string-headroom sweep lengthens every word by 30% rather than hanging the whole surplus on the last one — that manufactured an unbreakable run no locale produces — leaves screen-reader-only boxes alone, and accepts a select value's ellipsis, which is the primitive's design app-wide. --- e2e/setup-flow-locale-overflow.spec.ts | 25 ++++++++++++++++---- src/components/onboarding/baseline-form.tsx | 4 ++-- src/components/onboarding/confirm-screen.tsx | 2 +- src/components/onboarding/step-actions.tsx | 7 ++++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/e2e/setup-flow-locale-overflow.spec.ts b/e2e/setup-flow-locale-overflow.spec.ts index 41b3ed824..5ac8961a6 100644 --- a/e2e/setup-flow-locale-overflow.spec.ts +++ b/e2e/setup-flow-locale-overflow.spec.ts @@ -57,9 +57,11 @@ const RAW_KEY = /\b[a-z]+(?:[A-Z][a-z]+)?\.[a-z][A-Za-z0-9_.-]+\b/; /** * Lengthen every text node under the shell by 30%, then measure. Padding - * appends letters to the LAST word so the longest unbreakable run grows, - * which is the case that overflows; a padding that only added spaces would - * wrap for free and prove nothing. + * lengthens every WORD by 30%, so the longest unbreakable run grows in the + * same proportion as the sentence — which is what a longer translation does. + * Appending the whole surplus to one word would manufacture an unbreakable + * run no locale produces; adding only spaces would wrap for free and prove + * nothing. */ async function padAndMeasure(page: Page) { return page.evaluate((shellSelector) => { @@ -76,8 +78,11 @@ async function padAndMeasure(page: Page) { letters >= 3 && node.parentElement?.closest("script,style") === null ) { - const extra = Math.ceil(letters * 0.3); - node.textContent = text.replace(/\s*$/, "") + "x".repeat(extra); + node.textContent = text.replace(/\S+/g, (word) => + word.length >= 2 + ? word + "x".repeat(Math.ceil(word.length * 0.3)) + : word, + ); padded += 1; } node = walker.nextNode(); @@ -86,6 +91,16 @@ async function padAndMeasure(page: Page) { const doc = document.documentElement; const clippers = [...shell.querySelectorAll("*")] .filter((el) => { + // Screen-reader-only text is a 1 px clipped box on purpose; it is + // read, not seen, and cannot truncate. + if (el.classList.contains("sr-only") || el.clientWidth <= 1) { + return false; + } + // A select's value ellipsises by design (shadcn `SelectValue`, app + // wide): the full text is the option in the list and the control's + // accessible name, and a trigger that grew with its longest option + // would move every neighbour. Not a layout that lost headroom. + if (el.dataset.slot === "select-value") return false; const overflowX = getComputedStyle(el).overflowX; return overflowX === "hidden" || overflowX === "clip"; }) diff --git a/src/components/onboarding/baseline-form.tsx b/src/components/onboarding/baseline-form.tsx index 2a090031e..104f290e4 100644 --- a/src/components/onboarding/baseline-form.tsx +++ b/src/components/onboarding/baseline-form.tsx @@ -226,7 +226,7 @@ export function BaselineForm({ />
{backHref ? ( @@ -238,7 +238,7 @@ export function BaselineForm({ ) : (