This commit is contained in:
Szymon Bludnik 2026-09-11 13:11:53 -04:00 committed by GitHub
commit e12bcbb2a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 114 additions and 4 deletions

View File

@ -3,6 +3,7 @@
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import { wasNestedPopupJustClosed } from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils";
function AlertDialog({
@ -46,6 +47,7 @@ function AlertDialogOverlay({
function AlertDialogContent({
className,
size = "default",
onEscapeKeyDown,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
size?: "default" | "sm";
@ -60,6 +62,13 @@ function AlertDialogContent({
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-6 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-lg data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
onEscapeKeyDown={(event) => {
if (wasNestedPopupJustClosed()) {
event.preventDefault();
return;
}
onEscapeKeyDown?.(event);
}}
{...props}
/>
</AlertDialogPortal>

View File

@ -2,7 +2,10 @@ import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import { wasNestedPopupJustClosed } from "@/components/ui/nested-popup-context";
import {
wasNestedPopupJustClosed,
wasWindowRecentlyBlurred,
} from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils";
function Dialog({
@ -51,6 +54,8 @@ function DialogContent({
showCloseButton = true,
onPointerDownOutside,
onEscapeKeyDown,
onInteractOutside,
onFocusOutside,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
@ -78,6 +83,20 @@ function DialogContent({
}
onEscapeKeyDown?.(event);
}}
onFocusOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onFocusOutside?.(event);
}}
onInteractOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onInteractOutside?.(event);
}}
{...props}
>
{children}

View File

@ -1,7 +1,10 @@
import { CheckIcon, ChevronRightIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import * as React from "react";
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
import type * as React from "react";
import {
markNestedPopupClosed,
wasWindowRecentlyBlurred,
} from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils";
function DropdownMenu({
@ -61,6 +64,8 @@ function DropdownMenuContent({
className,
align = "start",
sideOffset = 4,
onInteractOutside,
onFocusOutside,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
@ -73,6 +78,20 @@ function DropdownMenuContent({
"z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 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 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
onFocusOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onFocusOutside?.(event);
}}
onInteractOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onInteractOutside?.(event);
}}
{...props}
/>
</DropdownMenuPrimitive.Portal>

View File

@ -7,3 +7,19 @@ export function markNestedPopupClosed() {
export function wasNestedPopupJustClosed() {
return performance.now() - lastNestedPopupCloseAt < 100;
}
let lastWindowFocusChangeAt = 0;
if (typeof window !== "undefined") {
const markWindowFocusChange = () => {
lastWindowFocusChangeAt = performance.now();
};
window.addEventListener("blur", markWindowFocusChange);
window.addEventListener("focus", markWindowFocusChange);
document.addEventListener("visibilitychange", markWindowFocusChange);
}
export function wasWindowRecentlyBlurred() {
return performance.now() - lastWindowFocusChangeAt < 150;
}

View File

@ -2,7 +2,10 @@
import { Popover as PopoverPrimitive } from "radix-ui";
import type * as React from "react";
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
import {
markNestedPopupClosed,
wasWindowRecentlyBlurred,
} from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils";
function Popover({
@ -33,6 +36,8 @@ function PopoverContent({
className,
align = "center",
sideOffset = 4,
onInteractOutside,
onFocusOutside,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
@ -45,6 +50,20 @@ function PopoverContent({
"z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
onFocusOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onFocusOutside?.(event);
}}
onInteractOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onInteractOutside?.(event);
}}
{...props}
/>
</PopoverPrimitive.Portal>

View File

@ -2,6 +2,10 @@ import { XIcon } from "lucide-react";
import { Dialog as SheetPrimitive } from "radix-ui";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import {
wasNestedPopupJustClosed,
wasWindowRecentlyBlurred,
} from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
@ -47,6 +51,9 @@ function SheetContent({
children,
side = "right",
showCloseButton = true,
onEscapeKeyDown,
onInteractOutside,
onFocusOutside,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
@ -62,6 +69,27 @@ function SheetContent({
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className,
)}
onEscapeKeyDown={(event) => {
if (wasNestedPopupJustClosed()) {
event.preventDefault();
return;
}
onEscapeKeyDown?.(event);
}}
onFocusOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onFocusOutside?.(event);
}}
onInteractOutside={(event) => {
if (wasWindowRecentlyBlurred()) {
event.preventDefault();
return;
}
onInteractOutside?.(event);
}}
{...props}
>
{children}