From ea9956f21da3f7498bf947f539c6b31fa844fe96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 13 Apr 2026 15:19:56 +0100 Subject: [PATCH] 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 --- resources/css/app.css | 2 ++ resources/js/components/ui/popover.tsx | 30 ++++++++++++++++++++++++++ tests/Feature/PopoverSafeAreaTest.php | 9 ++++++++ 3 files changed, 41 insertions(+) create mode 100644 tests/Feature/PopoverSafeAreaTest.php diff --git a/resources/css/app.css b/resources/css/app.css index 6c48d4a9..93a68b8b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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); diff --git a/resources/js/components/ui/popover.tsx b/resources/js/components/ui/popover.tsx index 6d51b6ce..80ce42b3 100644 --- a/resources/js/components/ui/popover.tsx +++ b/resources/js/components/ui/popover.tsx @@ -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) { @@ -19,14 +45,18 @@ function PopoverContent({ className, align = "center", sideOffset = 4, + collisionPadding, ...props }: React.ComponentProps) { + const safeAreaTopPadding = useSafeAreaTopPadding() + return ( toContain('collisionPadding={collisionPadding ?? { top: safeAreaTopPadding, right: 8, bottom: 8, left: 8 }}') + ->and($css)->toContain('--safe-area-top: env(safe-area-inset-top, 0px);'); +});