From 9d18868a072094ac393651abe767bc190f0b140f Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 7 Sep 2026 09:31:18 +0200 Subject: [PATCH] fix(usage): stop WebKit collapsing the consent dialog to its header and footer In Safari the product-usage consent dialog opened as a 302px box on a 714px viewport with the disclosure text squeezed into a 32px strip between header and footer. The dialog is a native laid out as a flex column with only a max-height, so its own height is indefinite, and the disclosure region used `flex-1`, which is `flex: 1 1 0%`. WebKit resolves that 0% basis against the indefinite container height as zero: the region's hypothetical size is zero, the dialog sizes to header plus footer, and the max-height never comes into play. Chromium treats the same basis as `content` and was fine. An `auto` basis with min-height 0 sizes the region from its content and lets it shrink to the max-height: measured in iOS Safari, 641px dialog and a 371px scrolling region, footer on screen, matching Chromium. Only the is affected. The div-based modals that use the same `flex-1 overflow-y-auto` pattern inside a max-height column were measured in the same WebKit and size correctly, so they stay as they are. A test pins the classes with the reasoning, since jsdom cannot see the layout. --- .../settings/__tests__/ProductUsageTab.test.tsx | 17 +++++++++++++++++ .../features/settings/tabs/ProductUsageTab.tsx | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx index 3699e6cb..a6b677c4 100644 --- a/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx +++ b/frontend/src/features/settings/__tests__/ProductUsageTab.test.tsx @@ -305,6 +305,23 @@ it('returns focus to the control that opened the consent dialog', async () => { ); }); +// WebKit sizing (Safari, desktop and iOS). The consent dialog is a native +// laid out as a flex column with only a max-height, so its height is +// indefinite. `flex-1` is `flex: 1 1 0%`, and WebKit resolves that 0% basis +// against the indefinite height as zero — the disclosure's hypothetical size +// becomes zero and the dialog shrinks to header plus footer, leaving the +// text in a 32px strip. Chromium treats the same basis as `content`. Measured +// in iOS Safari: 302px dialog / 32px region before, 641px / 371px after. An +// `auto` basis with min-height 0 sizes from content and then shrinks to fit. +// The div-based modals elsewhere are unaffected; only collapses. +it('sizes the consent disclosure from its content, so WebKit does not collapse the dialog', async () => { + mount(); + fireEvent.click(await screen.findByText('productUsage.review')); + const region = await screen.findByRole('group', { name: 'productUsage.consentTitle' }); + expect(region.className.split(' ')).toEqual(expect.arrayContaining(['flex-auto', 'min-h-0', 'overflow-y-auto'])); + expect(region.className.split(' ')).not.toContain('flex-1'); +}); + // A security property, not a nicety: the consent dialog is where an operator // decides whether to open a connection at all, so it has to say which way that // connection runs. UsageService makes exactly two outbound POSTs and reads diff --git a/frontend/src/features/settings/tabs/ProductUsageTab.tsx b/frontend/src/features/settings/tabs/ProductUsageTab.tsx index 54647ab3..bf9b1687 100644 --- a/frontend/src/features/settings/tabs/ProductUsageTab.tsx +++ b/frontend/src/features/settings/tabs/ProductUsageTab.tsx @@ -124,7 +124,7 @@ function ConsentDialog({ tabIndex={0} role="group" aria-label={t('productUsage.consentTitle') as string} - className="flex-1 overflow-y-auto border-y border-neutral-200 dark:border-neutral-700 px-6 py-4 space-y-4 focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary-400" + className="min-h-0 flex-auto overflow-y-auto border-y border-neutral-200 dark:border-neutral-700 px-6 py-4 space-y-4 focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary-400" > {DISCLOSURE.map(({ key, heading, Icon }) => (