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:
James Ritchie
2026-08-13 14:05:17 +01:00
committed by GitHub
parent f6f3b75547
commit 0b52af94fa
3 changed files with 67 additions and 21 deletions
@@ -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;