feat(webapp): restyle the modal and sheet close buttons (#4603)
The close button on modals and slide-over panels is now a simpler icon-only button. The `Esc` key label moves out of the button and into a hover tooltip, delayed by 500ms. <img src="https://raw.githubusercontent.com/triggerdotdev/trigger.dev/31b781afb984e1ca36b31cd1e7d3a475f06310d1/modal-close-button.png" width="620" alt="Modal with the new square close button in the top right" /> <img src="https://raw.githubusercontent.com/triggerdotdev/trigger.dev/31b781afb984e1ca36b31cd1e7d3a475f06310d1/modal-close-button-tooltip.png" width="200" alt="Hovering the close button shows a Close tooltip with the Esc key" /> ### Verified Both surfaces, driven in a real browser: no tooltip on open despite autofocus, hidden at 300ms of hover, "Close · Esc" at 700ms, hides on pointer leave, `Escape` closes, clicking the X closes with no orphaned tooltip, and the button stays keyboard-focusable (`tabIndex 0`). The `fullscreen` dialog variant flips the tooltip below to stay on-screen. `typecheck --filter webapp` passes; `format` and `lint` are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- conductor-workspace-link --> --- [Open workspace in Conductor](https://app.conductor.build/workspace/a7368189-9fbb-4edd-891c-43c633931bcf) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<
|
||||
>
|
||||
<hr className="absolute left-0 top-11 w-full" />
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground group absolute right-2 top-2.25 flex items-center gap-1 rounded-sm p-1 py-1 pl-0 pr-1 opacity-70 transition focus-custom hover:bg-background-hover hover:opacity-100 focus-visible:focus-custom disabled:pointer-events-none">
|
||||
<ShortcutKey
|
||||
shortcut={{
|
||||
key: "esc",
|
||||
}}
|
||||
variant="medium"
|
||||
/>
|
||||
<XMarkIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.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 && <ModalCloseButton className="absolute right-2 top-2.25" />}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
|
||||
@@ -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<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const cancelOpen = () => clearTimeout(openTimeout.current);
|
||||
React.useEffect(() => cancelOpen, []);
|
||||
|
||||
const close = () => {
|
||||
cancelOpen();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
{/* 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. */}
|
||||
<Tooltip open={open} onOpenChange={(nextOpen) => !nextOpen && close()}>
|
||||
<TooltipTrigger asChild>
|
||||
<DialogPrimitive.Close
|
||||
onPointerEnter={(event) => {
|
||||
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
|
||||
)}
|
||||
>
|
||||
<CrossIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center py-1.5 pl-2.5 pr-2 text-xs text-text-bright">
|
||||
Close
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="medium" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
<SheetPrimitive.Close className="flex items-center gap-1 rounded-sm p-1 pl-0 transition hover:bg-background-hover focus-visible:focus-custom disabled:pointer-events-none">
|
||||
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
|
||||
<XMarkIcon className="size-4 text-text-dimmed" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.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. */}
|
||||
<ModalCloseButton className="size-6" />
|
||||
</SheetPrimitive.Title>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
Reference in New Issue
Block a user