fix: keep iOS popovers below the notch (#282)

## Summary
- add safe-area CSS variables for the viewport top and bottom insets
- apply safe-area-aware collision padding to the shared popover
primitive so iOS overlays stay below the notch
- add a focused regression test for the popover safe-area guard

## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/PopoverSafeAreaTest.php
This commit is contained in:
Víctor Falcón 2026-04-13 15:19:56 +01:00 committed by GitHub
parent 80b666836c
commit ea9956f21d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 41 additions and 0 deletions

View File

@ -71,6 +71,8 @@
}
:root {
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);

View File

@ -3,6 +3,32 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function useSafeAreaTopPadding(): number {
const [padding, setPadding] = React.useState(8)
React.useEffect(() => {
const updatePadding = () => {
const value = Number.parseFloat(
getComputedStyle(document.documentElement).getPropertyValue("--safe-area-top")
)
setPadding(Number.isNaN(value) ? 8 : Math.max(8, value + 8))
}
updatePadding()
window.addEventListener("resize", updatePadding)
window.visualViewport?.addEventListener("resize", updatePadding)
return () => {
window.removeEventListener("resize", updatePadding)
window.visualViewport?.removeEventListener("resize", updatePadding)
}
}, [])
return padding
}
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
@ -19,14 +45,18 @@ function PopoverContent({
className,
align = "center",
sideOffset = 4,
collisionPadding,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
const safeAreaTopPadding = useSafeAreaTopPadding()
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
collisionPadding={collisionPadding ?? { top: safeAreaTopPadding, right: 8, bottom: 8, left: 8 }}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className

View File

@ -0,0 +1,9 @@
<?php
test('popover content keeps a safe top collision padding', function () {
$popover = file_get_contents(resource_path('js/components/ui/popover.tsx'));
$css = file_get_contents(resource_path('css/app.css'));
expect($popover)->toContain('collisionPadding={collisionPadding ?? { top: safeAreaTopPadding, right: 8, bottom: 8, left: 8 }}')
->and($css)->toContain('--safe-area-top: env(safe-area-inset-top, 0px);');
});