Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/web/src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
DIALOG_BACKDROP_CLASS,
DIALOG_MOBILE_SHEET_CLASS,
DIALOG_POPUP_CLASS,
resolveDialogBackdropStyle,
} from "~/components/ui/dialog-styles";

const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
Expand All @@ -19,12 +20,15 @@ function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}

function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
function AlertDialogBackdrop({ className, style, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
forceRender
className={cn(DIALOG_BACKDROP_CLASS, className)}
data-slot="alert-dialog-backdrop"
style={(state) =>
resolveDialogBackdropStyle(state.open, typeof style === "function" ? style(state) : style)
}
{...props}
/>
);
Expand Down
15 changes: 13 additions & 2 deletions apps/web/src/components/ui/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import {
AutocompleteList,
AutocompleteSeparator,
} from "~/components/ui/autocomplete";
import { DIALOG_BACKDROP_CLASS, DIALOG_POPUP_CLASS } from "~/components/ui/dialog-styles";
import {
DIALOG_BACKDROP_CLASS,
DIALOG_POPUP_CLASS,
resolveDialogBackdropStyle,
} from "~/components/ui/dialog-styles";
import { Button } from "~/components/ui/button";

const CommandDialog = CommandDialogPrimitive.Root;
Expand All @@ -28,11 +32,18 @@ function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
return <CommandDialogPrimitive.Trigger data-slot="command-dialog-trigger" {...props} />;
}

function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.Backdrop.Props) {
function CommandDialogBackdrop({
className,
style,
...props
}: CommandDialogPrimitive.Backdrop.Props) {
return (
<CommandDialogPrimitive.Backdrop
className={cn(DIALOG_BACKDROP_CLASS, className)}
data-slot="command-dialog-backdrop"
style={(state) =>
resolveDialogBackdropStyle(state.open, typeof style === "function" ? style(state) : style)
}
{...props}
/>
);
Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/components/ui/dialog-styles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vite-plus/test";

import { resolveDialogBackdropStyle } from "./dialog-styles";

describe("dialog backdrop interaction states", () => {
it("stops intercepting pointer input while the dialog finishes closing", () => {
expect(resolveDialogBackdropStyle(false, { pointerEvents: "auto" })).toMatchObject({
pointerEvents: "none",
});
});

it("preserves caller styles while the dialog is open", () => {
const style = { cursor: "wait" } as const;
expect(resolveDialogBackdropStyle(true, style)).toBe(style);
});
});
17 changes: 16 additions & 1 deletion apps/web/src/components/ui/dialog-styles.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import type { CSSProperties } from "react";

const DIALOG_BACKDROP_CLASS =
"dialog-backdrop fixed inset-0 z-50 transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0";

function resolveDialogBackdropStyle(
open: boolean,
style: CSSProperties | undefined,
): CSSProperties | undefined {
if (open) return style;
return { ...style, pointerEvents: "none" };
}

const DIALOG_POPUP_CLASS =
"dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative flex min-h-0 w-full min-w-0 scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border opacity-[calc(1-0.1*var(--nested-dialogs))] outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0";

const DIALOG_MOBILE_SHEET_CLASS =
"max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4";

export { DIALOG_BACKDROP_CLASS, DIALOG_MOBILE_SHEET_CLASS, DIALOG_POPUP_CLASS };
export {
DIALOG_BACKDROP_CLASS,
DIALOG_MOBILE_SHEET_CLASS,
DIALOG_POPUP_CLASS,
resolveDialogBackdropStyle,
};
34 changes: 34 additions & 0 deletions apps/web/src/components/ui/dialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vite-plus/test";

vi.mock("@base-ui/react/dialog", () => ({
Dialog: {
createHandle: () => ({}),
Root: "div",
Portal: "div",
Trigger: "button",
Close: "button",
Backdrop: ({ forceRender: _forceRender, style, ...props }: Record<string, unknown>) => (
<div
{...props}
style={
typeof style === "function" ? style({ open: false, transitionStatus: "ending" }) : style
}
/>
),
Viewport: "div",
Popup: "div",
Title: "h2",
Description: "p",
},
}));

import { DialogBackdrop } from "./dialog";

describe("DialogBackdrop", () => {
it("does not intercept input when Base UI keeps it mounted while closing", () => {
const markup = renderToStaticMarkup(<DialogBackdrop style={{ pointerEvents: "auto" }} />);

expect(markup).toContain('style="pointer-events:none"');
});
});
6 changes: 5 additions & 1 deletion apps/web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DIALOG_BACKDROP_CLASS,
DIALOG_MOBILE_SHEET_CLASS,
DIALOG_POPUP_CLASS,
resolveDialogBackdropStyle,
} from "~/components/ui/dialog-styles";
import { ScrollArea } from "~/components/ui/scroll-area";

Expand All @@ -25,12 +26,15 @@ function DialogClose(props: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}

function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props) {
function DialogBackdrop({ className, style, ...props }: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
forceRender
className={cn(DIALOG_BACKDROP_CLASS, className)}
data-slot="dialog-backdrop"
style={(state) =>
resolveDialogBackdropStyle(state.open, typeof style === "function" ? style(state) : style)
}
{...props}
/>
);
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { XIcon } from "lucide-react";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import { resolveDialogBackdropStyle } from "~/components/ui/dialog-styles";
import { ScrollArea } from "~/components/ui/scroll-area";

const Sheet = SheetPrimitive.Root;
Expand All @@ -18,14 +19,17 @@ function SheetClose(props: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}

function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
function SheetBackdrop({ className, style, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
className={cn(
"fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
className,
)}
data-slot="sheet-backdrop"
style={(state) =>
resolveDialogBackdropStyle(state.open, typeof style === "function" ? style(state) : style)
}
{...props}
/>
);
Expand Down