diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index 3a7666eb80..b62bb01f22 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -3,8 +3,7 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { cn } from "~/utils/cn"; -import { XMarkIcon } from "@heroicons/react/24/solid"; -import { ShortcutKey } from "./ShortcutKey"; +import { ModalCloseButton } from "./ModalCloseButton"; const Dialog = DialogPrimitive.Root; @@ -56,18 +55,10 @@ const DialogContent = React.forwardRef< >
{children} - {showCloseButton && ( - - - - Close - - )} + {/* The default size-7 is the height this button had when it rendered the esc key alongside the + icon, so the vertical geometry dialogs align against (the top-11 divider, absolutely + positioned titles) is unchanged — it only gets narrower. */} + {showCloseButton && } )); diff --git a/apps/webapp/app/components/primitives/ModalCloseButton.tsx b/apps/webapp/app/components/primitives/ModalCloseButton.tsx new file mode 100644 index 0000000000..8827a702e4 --- /dev/null +++ b/apps/webapp/app/components/primitives/ModalCloseButton.tsx @@ -0,0 +1,58 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import * as React from "react"; +import { CrossIcon } from "~/assets/icons/CrossIcon"; +import { cn } from "~/utils/cn"; +import { ShortcutKey } from "./ShortcutKey"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; + +const CLOSE_TOOLTIP_DELAY_MS = 500; + +/** + * The close button for modal surfaces — Dialog and Sheet, which are both Radix Dialog underneath. + * Pass `className` to position it, and to override the default `size-7` box where a surface needs + * to keep a tighter header height. + */ +export function ModalCloseButton({ className }: { className?: string }) { + const [open, setOpen] = React.useState(false); + const openTimeout = React.useRef>(); + + const cancelOpen = () => clearTimeout(openTimeout.current); + React.useEffect(() => cancelOpen, []); + + const close = () => { + cancelOpen(); + setOpen(false); + }; + + return ( + + {/* The tooltip is driven by our own hover timer rather than Radix's: Radix opens tooltips + instantly on focus, and these surfaces autofocus this button whenever they hold no other + tabbable content, which would pop the tooltip open on mount and leave it there. + Radix-initiated opens are ignored; its closes (pointer leave, blur, click) are honoured. */} + !nextOpen && close()}> + + { + if (event.pointerType === "touch") return; + cancelOpen(); + openTimeout.current = setTimeout(() => setOpen(true), CLOSE_TOOLTIP_DELAY_MS); + }} + onPointerLeave={close} + className={cn( + "group flex size-7 items-center justify-center rounded-sm opacity-70 transition focus-custom hover:bg-background-hover hover:opacity-100 focus-visible:focus-custom disabled:pointer-events-none", + className + )} + > + + Close + + + + Close + + + + + ); +} diff --git a/apps/webapp/app/components/primitives/SheetV3.tsx b/apps/webapp/app/components/primitives/SheetV3.tsx index 922f28d5ba..5bfc14285e 100644 --- a/apps/webapp/app/components/primitives/SheetV3.tsx +++ b/apps/webapp/app/components/primitives/SheetV3.tsx @@ -1,9 +1,8 @@ -import { XMarkIcon } from "@heroicons/react/20/solid"; import * as SheetPrimitive from "@radix-ui/react-dialog"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "~/utils/cn"; -import { ShortcutKey } from "./ShortcutKey"; +import { ModalCloseButton } from "./ModalCloseButton"; const Sheet = SheetPrimitive.Root; @@ -91,11 +90,9 @@ const SheetTitle = React.forwardRef< {...props} > {children} - - - - Close - + {/* size-6 rather than the default size-7 keeps this header row at the height it had when the + button rendered the esc key alongside the icon — it only gets narrower. */} + )); SheetTitle.displayName = SheetPrimitive.Title.displayName;