mirror of
https://github.com/tale/headplane.git
synced 2026-07-27 08:08:56 +00:00
2584a4ef55
#556 added overflow-y-auto to the dialog content container. Per the CSS spec, when one overflow axis is non-visible the other resets from visible to auto, so overflow-x was being silently clamped too — clipping the ~2px focus ring on inputs/buttons against the left edge of the container. Add px-1/-mx-1 so the layout width is unchanged while focus rings get room to render. Amp-Thread-ID: https://ampcode.com/threads/T-019e7ae4-6862-760c-a3e7-239350eab71d Co-authored-by: Amp <amp@ampcode.com>
134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import { AlertDialog } from "@base-ui/react/alert-dialog";
|
|
import React, { cloneElement, useEffect, useRef } from "react";
|
|
import { Form, type HTMLFormMethod } from "react-router";
|
|
|
|
import Button, { type ButtonProps } from "~/components/button";
|
|
import cn from "~/utils/cn";
|
|
import { useLiveData } from "~/utils/live-data";
|
|
|
|
export interface DialogProps {
|
|
children:
|
|
| [React.ReactElement<ButtonProps>, React.ReactElement<DialogPanelProps>]
|
|
| React.ReactElement<DialogPanelProps>;
|
|
isOpen?: boolean;
|
|
onOpenChange?: (isOpen: boolean) => void;
|
|
}
|
|
|
|
function Dialog(props: DialogProps) {
|
|
const { pause, resume } = useLiveData();
|
|
const { isOpen, onOpenChange } = props;
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
pause();
|
|
} else {
|
|
resume();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
if (Array.isArray(props.children)) {
|
|
const [button, panel] = props.children;
|
|
return (
|
|
<AlertDialog.Root open={isOpen} onOpenChange={(open) => onOpenChange?.(open)}>
|
|
<AlertDialog.Trigger render={cloneElement(button)} />
|
|
<DialogOverlay>{panel}</DialogOverlay>
|
|
</AlertDialog.Root>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AlertDialog.Root open={isOpen} onOpenChange={(open) => onOpenChange?.(open)}>
|
|
<DialogOverlay>{props.children}</DialogOverlay>
|
|
</AlertDialog.Root>
|
|
);
|
|
}
|
|
|
|
export interface DialogPanelProps {
|
|
children: React.ReactNode;
|
|
variant?: "normal" | "destructive" | "unactionable";
|
|
onSubmit?: React.FormEventHandler<HTMLFormElement>;
|
|
method?: HTMLFormMethod;
|
|
isDisabled?: boolean;
|
|
}
|
|
|
|
function Panel(props: DialogPanelProps) {
|
|
const { children, onSubmit, isDisabled, variant, method = "POST" } = props;
|
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
|
|
return (
|
|
<AlertDialog.Popup
|
|
className={cn(
|
|
"flex w-full max-w-lg flex-col rounded-xl p-4",
|
|
"max-h-[90dvh]",
|
|
"outline-hidden",
|
|
"bg-white dark:bg-mist-900",
|
|
"border border-mist-200 dark:border-mist-800",
|
|
"shadow-overlay",
|
|
)}
|
|
>
|
|
<Form
|
|
method={method ?? "POST"}
|
|
className="flex min-h-0 flex-1 flex-col"
|
|
onSubmit={(event) => {
|
|
if (onSubmit) {
|
|
onSubmit(event);
|
|
}
|
|
|
|
if (!event.defaultPrevented) {
|
|
closeRef.current?.click();
|
|
}
|
|
}}
|
|
>
|
|
{/* px-1 -mx-1 gives focus rings on inputs/buttons room to render
|
|
without being clipped by overflow-y-auto (which implicitly forces
|
|
overflow-x: auto per CSS spec). */}
|
|
<div className="-mx-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-1">
|
|
{children}
|
|
</div>
|
|
<div className="mt-5 flex shrink-0 justify-end gap-3">
|
|
{variant === "unactionable" ? (
|
|
<AlertDialog.Close render={<Button>Close</Button>} />
|
|
) : (
|
|
<>
|
|
<AlertDialog.Close render={<Button>Cancel</Button>} />
|
|
<AlertDialog.Close ref={closeRef} className="hidden" aria-hidden tabIndex={-1} />
|
|
<Button
|
|
disabled={isDisabled}
|
|
type="submit"
|
|
variant={variant === "destructive" ? "danger" : "heavy"}
|
|
>
|
|
Confirm
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Form>
|
|
</AlertDialog.Popup>
|
|
);
|
|
}
|
|
|
|
function DialogOverlay({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<AlertDialog.Portal>
|
|
<AlertDialog.Backdrop
|
|
className={cn(
|
|
"fixed inset-0 z-20 h-screen w-screen",
|
|
"bg-mist-900/30 dark:bg-mist-950/60",
|
|
"transition-opacity duration-100",
|
|
)}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
"fixed inset-0 z-20 h-screen w-screen",
|
|
"flex items-center justify-center p-4",
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
</AlertDialog.Portal>
|
|
);
|
|
}
|
|
|
|
export { Panel as DialogPanel };
|
|
export default Dialog;
|