Skip to content

Instantly share code, notes, and snippets.

@marcospgp
Created September 22, 2025 13:07
Show Gist options
  • Save marcospgp/9ae67de36dcc212a76ec1755b3c97c72 to your computer and use it in GitHub Desktop.
Save marcospgp/9ae67de36dcc212a76ec1755b3c97c72 to your computer and use it in GitHub Desktop.
BaseUI dialog manager implementation
import { Dialog as D } from "@base-ui-components/react/dialog";
import {
type ComponentProps,
type ReactNode,
createContext,
useEffect,
useRef,
useState,
} from "react";
import { Button } from "./Button";
import { Field } from "./Field";
import { Form } from "./Form";
type DialogProps = {
title: string;
renderTrigger?: ComponentProps<typeof D.Trigger>["render"];
render: (close: () => void) => ReactNode;
/** Used to modify or reset state after CSS transitions have completed. */
onOpenChangeComplete?: (open: boolean) => void;
fitContent?: boolean;
};
/**
* Internal dialog component that can optionally render without a trigger.
* When renderTrigger is not provided, the dialog opens immediately.
* This is used internally by imperative dialog functions.
*/
function InternalDialog(props: Readonly<DialogProps>) {
const [isOpen, setIsOpen] = useState(!props.renderTrigger);
const [key, setKey] = useState(0);
return (
<D.Root
key={key}
open={isOpen}
onOpenChange={setIsOpen}
onOpenChangeComplete={(open) => {
if (!open) setKey((prev) => prev + 1); // Force remount.
props.onOpenChangeComplete?.(open);
}}
>
{props.renderTrigger && <D.Trigger render={props.renderTrigger} />}
<D.Portal>
<div className="fixed inset-0 z-10 flex items-center justify-center">
{/* Backdrop must not be covered so it remains clickable. */}
<D.Backdrop className="absolute inset-0 z-20 cursor-pointer bg-black opacity-50 transition data-[ending-style]:opacity-0 data-[starting-style]:opacity-0" />
{/* Dialog */}
<D.Popup
className={`${!props.fitContent && "w-lg"} bg-bg px-md py-sm bevel relative z-30 flex max-w-[min(var(--container-lg),80vw)] flex-col transition data-[ending-style]:scale-90 data-[starting-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0`}
>
<D.Title render={(ps) => <h1 {...ps}>{props.title}</h1>} />
{props.render(() => {
setIsOpen(false);
})}
</D.Popup>
</div>
</D.Portal>
</D.Root>
);
}
type PromptConfig = {
title: string;
description?: string;
label?: string;
placeholder?: string;
positiveAction?: string;
negativeAction?: string;
danger?: boolean;
onSubmit: (value: string) => void | Promise<void>;
onCancel?: () => void | Promise<void>;
};
type ConfirmConfig = {
title: string;
description?: string;
positiveAction?: string;
negativeAction?: string;
danger?: boolean;
onConfirm: () => void | Promise<void>;
onCancel?: () => void | Promise<void>;
};
type AlertConfig = {
title: string;
description?: string;
positiveAction?: string;
onClose: () => void | Promise<void>;
};
type CustomDialogConfig = {
title: string;
children: ReactNode | ((close: () => void) => ReactNode);
onClose?: () => void | Promise<void>;
fitContent?: boolean;
};
// Component-specific prop types (config + component handlers)
type PromptProps = PromptConfig & {
renderTrigger?: ComponentProps<typeof D.Trigger>["render"];
};
type DialogVariant = "prompt" | "confirm" | "alert";
function InternalPromptDialog(
props: {
variant: DialogVariant;
onOpenChangeComplete?: (open: boolean) => void;
onCancel?: () => void; // Add cancel handler
} & PromptProps,
) {
const inputRef = useRef<HTMLInputElement>(null);
const isAlert = props.variant === "alert";
const isPrompt = props.variant === "prompt";
const positiveAction = props.positiveAction ?? (isAlert ? "Got it" : "Ok");
const negativeAction = isAlert ? undefined : (props.negativeAction ?? "Cancel");
return (
<InternalDialog
title={props.title}
renderTrigger={props.renderTrigger}
onOpenChangeComplete={(open) => {
props.onOpenChangeComplete?.(open);
// Handle cancellation when dialog closes without submission
if (!open && !isAlert && props.onCancel) {
props.onCancel();
}
}}
render={(close) => (
<Form.Root
onSubmit={async () => {
if (isAlert) {
close();
return;
}
await props.onSubmit(inputRef.current?.value ?? "");
close();
}}
>
<div className="mb-sm gap-md flex flex-col">
{props.description && (
<D.Description className="text-text-muted" render={<div>{props.description}</div>} />
)}
{isPrompt && (
<Field
name="input"
label={props.label}
ref={inputRef}
placeholder={props.placeholder}
autoComplete="off"
autoFocus
grow
validate={(value: unknown) => {
const valid = typeof value === "string" && value.trim().length > 0;
return valid ? null : "Please enter a value";
}}
/** Temporary fix to avoid flash of error message when
* dismissing dialog with empty input, which triggers onBlur.
* https://github.com/mui/base-ui/issues/2803
*/
validationMode="onChange"
/>
)}
</div>
<div className="mt-md gap-xs flex justify-end">
{!isAlert && negativeAction && (
<D.Close
onClick={() => {
if (props.onCancel) props.onCancel();
close();
}}
render={<Button />}
className=""
>
{negativeAction}
</D.Close>
)}
<Form.Submit autoFocus={!isPrompt} danger={!isAlert && props.danger}>
{positiveAction}
</Form.Submit>
</div>
</Form.Root>
)}
/>
);
}
function PromptDialog(props: PromptProps) {
const [key, setKey] = useState(0);
return (
<InternalPromptDialog
key={key}
variant="prompt"
onOpenChangeComplete={(open) => {
if (!open) {
// Force remount.
setKey((x) => x + 1);
}
}}
{...props}
/>
);
}
function ConfirmDialog(
props: ConfirmConfig & { onClose: (confirmed: boolean) => void | Promise<void> },
) {
const { onClose, onConfirm, onCancel, ...restProps } = props;
return (
<InternalPromptDialog
variant="confirm"
onSubmit={() => onClose(true)}
onCancel={() => onClose(false)}
{...restProps}
/>
);
}
function AlertDialog(props: AlertConfig & { onOpenChangeComplete?: (open: boolean) => void }) {
return (
<InternalPromptDialog
variant="alert"
onSubmit={() => {}}
onOpenChangeComplete={props.onOpenChangeComplete}
{...props}
/>
);
}
function CustomDialog(
props: CustomDialogConfig & { onOpenChangeComplete?: (open: boolean) => void },
) {
const [key, setKey] = useState(0);
return (
<InternalDialog
key={key}
title={props.title}
fitContent={props.fitContent}
onOpenChangeComplete={(open) => {
if (!open) {
// Force remount to reset custom component state.
setKey((prev) => prev + 1);
if (props.onClose) {
void Promise.resolve(props.onClose());
}
}
props.onOpenChangeComplete?.(open);
}}
render={(close) =>
typeof props.children === "function" ? props.children(close) : props.children
}
/>
);
}
// Imperative dialog state management
type DialogState =
| { id: number; type: "prompt"; props: PromptProps }
| {
id: number;
type: "confirm";
props: ConfirmConfig & { onClose: (confirmed: boolean) => void | Promise<void> };
}
| {
id: number;
type: "alert";
props: AlertConfig & { onOpenChangeComplete?: (open: boolean) => void };
}
| {
id: number;
type: "custom";
props: CustomDialogConfig & { onOpenChangeComplete?: (open: boolean) => void };
};
const dialogs = new Map<number, DialogState>();
let listener: ((dialogs: DialogState[]) => void) | null = null;
let dialogId = 0;
const notify = () => {
if (listener) {
const dialogArray = Array.from(dialogs.values());
listener(dialogArray);
}
};
const subscribe = (newListener: (dialogs: DialogState[]) => void) => {
listener = newListener;
return () => {
listener = null;
};
};
const removeDialog = (id: number) => {
dialogs.delete(id);
notify();
};
// Helper function to get next dialog ID and prevent infinite growth
const getNextDialogId = () => {
const id = ++dialogId;
// Reset ID counter to prevent infinite growth (simple approach)
if (dialogId > 1000) dialogId = 0;
return id;
};
/**
* Imperative dialog manager for programmatically controlling dialogs.
*
* Why imperative API (dialogManager.prompt()) over declarative
* (<PromptDialog />):
*
* - auto state reset on dialog close
* - can trigger dialog without rendering a button
*/
export const dialogManager = {
prompt: (config: PromptConfig) => {
const id = getNextDialogId();
let isResolved = false;
dialogs.set(id, {
id,
type: "prompt",
props: {
...config,
onSubmit: (result: string) => {
if (!isResolved) {
isResolved = true;
removeDialog(id);
void Promise.resolve(config.onSubmit(result));
}
},
onCancel: () => {
if (!isResolved) {
isResolved = true;
removeDialog(id);
if (config.onCancel) {
void Promise.resolve(config.onCancel());
}
}
},
},
});
notify();
},
confirm: (config: ConfirmConfig) => {
const id = getNextDialogId();
dialogs.set(id, {
id,
type: "confirm",
props: {
...config,
onClose: (confirmed: boolean) => {
removeDialog(id);
if (confirmed) {
void Promise.resolve(config.onConfirm());
} else if (config.onCancel) {
void Promise.resolve(config.onCancel());
}
},
},
});
notify();
},
alert: (config: AlertConfig) => {
const id = getNextDialogId();
dialogs.set(id, {
id,
type: "alert",
props: {
...config,
onOpenChangeComplete: (open: boolean) => {
if (!open) {
removeDialog(id);
void Promise.resolve(config.onClose());
}
},
},
});
notify();
},
custom: (config: CustomDialogConfig) => {
const id = getNextDialogId();
dialogs.set(id, {
id,
type: "custom",
props: {
...config,
onOpenChangeComplete: (open: boolean) => {
if (!open) {
removeDialog(id);
if (config.onClose) {
void Promise.resolve(config.onClose());
}
}
},
},
});
notify();
},
};
// Context for provider
const DialogContext = createContext(null);
// Provider component
export function DialogProvider(props: Readonly<{ children: ReactNode }>) {
const [dialogList, setDialogList] = useState<DialogState[]>([]);
useEffect(() => {
const unsubscribe = subscribe(setDialogList);
return unsubscribe;
}, []);
return (
<DialogContext value={null}>
{props.children}
{/* Render active dialogs */}
{dialogList.map((dialog) => {
if (dialog.type === "prompt") {
return <PromptDialog key={dialog.id} {...dialog.props} />;
}
if (dialog.type === "confirm") {
return <ConfirmDialog key={dialog.id} {...dialog.props} />;
}
if (dialog.type === "custom") {
return <CustomDialog key={dialog.id} {...dialog.props} />;
}
return <AlertDialog key={dialog.id} {...dialog.props} />;
})}
</DialogContext>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment