From 938b5151deb4807db1b9f12715cd0b87ba324d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 21:40:27 +0200 Subject: [PATCH] fix(ui): drop the home-indicator padding of a bottom sheet on the keyboard A bottom sheet standing on the on-screen keyboard kept its safe-area inset as bottom padding, leaving about 34 px of empty space between its last row and the keys on an iPhone. --- src/__tests__/pwa-mobile-shell-guard.test.ts | 14 ++++++++++++++ src/app/globals.css | 7 +++++++ src/components/ui/responsive-sheet.tsx | 7 +++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/__tests__/pwa-mobile-shell-guard.test.ts b/src/__tests__/pwa-mobile-shell-guard.test.ts index 0daf812ef..964f7df3e 100644 --- a/src/__tests__/pwa-mobile-shell-guard.test.ts +++ b/src/__tests__/pwa-mobile-shell-guard.test.ts @@ -78,6 +78,20 @@ describe("the shell owns the safe area", () => { ); }); + it("a bottom sheet on the keyboard drops its home-indicator padding", () => { + // Standing on the keyboard, the safe-area inset left a ~34 px gap + // between the sheet's last row and the keys. + expect(code(read("src/app/globals.css"))).toMatch( + /@custom-variant keyboard-open \(html\[data-keyboard="open"\] &\);/, + ); + const sheet = code(read("src/components/ui/responsive-sheet.tsx")); + const padded = (sheet.match( + /"[^"]*pb-\[calc\(env\(safe-area-inset-bottom[^"]*"/g, + ) ?? []) as string[]; + expect(padded.length).toBe(2); + for (const cls of padded) expect(cls).toMatch(/\bkeyboard-open:pb-4\b/); + }); + it("the sheet primitive names its side for those rules", () => { expect(read("src/components/ui/sheet.tsx")).toMatch( /data-sheet-side=\{side\}/, diff --git a/src/app/globals.css b/src/app/globals.css index 6d230f1fe..fcb04514a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -76,6 +76,13 @@ } } +/* + * `keyboard-open:` — while the on-screen keyboard is up (see the keyboard + * rules further down). A bottom sheet then stands on the keyboard, not on + * the home indicator, so its safe-area padding has nothing left to clear. + */ +@custom-variant keyboard-open (html[data-keyboard="open"] &); + @custom-variant shell-mobile { @media (max-width: 47.98rem), (pointer: coarse) and (max-height: 520px) { @slot; diff --git a/src/components/ui/responsive-sheet.tsx b/src/components/ui/responsive-sheet.tsx index 3f9f67edc..17fa6f950 100644 --- a/src/components/ui/responsive-sheet.tsx +++ b/src/components/ui/responsive-sheet.tsx @@ -257,7 +257,10 @@ export function ResponsiveSheet({ // Without a footer the body is the sheet's bottom edge, so it // keeps its last row above the home indicator of an // installed app; the footer does that when there is one. - !footer && "pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]", + // Standing on the keyboard, the sheet is clear of the + // indicator already; the inset would leave a gap above it. + !footer && + "keyboard-open:pb-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]", bodyClassName, )} > @@ -266,7 +269,7 @@ export function ResponsiveSheet({ {footer ? ( {footer}