Skip to content

Commit 0b52af9

Browse files
samejrclaude
andauthored
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>
1 parent f6f3b75 commit 0b52af9

3 files changed

Lines changed: 67 additions & 21 deletions

File tree

apps/webapp/app/components/primitives/Dialog.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import * as React from "react";
44
import * as DialogPrimitive from "@radix-ui/react-dialog";
55
import { cn } from "~/utils/cn";
6-
import { XMarkIcon } from "@heroicons/react/24/solid";
7-
import { ShortcutKey } from "./ShortcutKey";
6+
import { ModalCloseButton } from "./ModalCloseButton";
87

98
const Dialog = DialogPrimitive.Root;
109

@@ -56,18 +55,10 @@ const DialogContent = React.forwardRef<
5655
>
5756
<hr className="absolute left-0 top-11 w-full" />
5857
{children}
59-
{showCloseButton && (
60-
<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">
61-
<ShortcutKey
62-
shortcut={{
63-
key: "esc",
64-
}}
65-
variant="medium"
66-
/>
67-
<XMarkIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
68-
<span className="sr-only">Close</span>
69-
</DialogPrimitive.Close>
70-
)}
58+
{/* The default size-7 is the height this button had when it rendered the esc key alongside the
59+
icon, so the vertical geometry dialogs align against (the top-11 divider, absolutely
60+
positioned titles) is unchanged — it only gets narrower. */}
61+
{showCloseButton && <ModalCloseButton className="absolute right-2 top-2.25" />}
7162
</DialogPrimitive.Content>
7263
</DialogPortal>
7364
));
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as DialogPrimitive from "@radix-ui/react-dialog";
2+
import * as React from "react";
3+
import { CrossIcon } from "~/assets/icons/CrossIcon";
4+
import { cn } from "~/utils/cn";
5+
import { ShortcutKey } from "./ShortcutKey";
6+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip";
7+
8+
const CLOSE_TOOLTIP_DELAY_MS = 500;
9+
10+
/**
11+
* The close button for modal surfaces — Dialog and Sheet, which are both Radix Dialog underneath.
12+
* Pass `className` to position it, and to override the default `size-7` box where a surface needs
13+
* to keep a tighter header height.
14+
*/
15+
export function ModalCloseButton({ className }: { className?: string }) {
16+
const [open, setOpen] = React.useState(false);
17+
const openTimeout = React.useRef<ReturnType<typeof setTimeout>>();
18+
19+
const cancelOpen = () => clearTimeout(openTimeout.current);
20+
React.useEffect(() => cancelOpen, []);
21+
22+
const close = () => {
23+
cancelOpen();
24+
setOpen(false);
25+
};
26+
27+
return (
28+
<TooltipProvider>
29+
{/* The tooltip is driven by our own hover timer rather than Radix's: Radix opens tooltips
30+
instantly on focus, and these surfaces autofocus this button whenever they hold no other
31+
tabbable content, which would pop the tooltip open on mount and leave it there.
32+
Radix-initiated opens are ignored; its closes (pointer leave, blur, click) are honoured. */}
33+
<Tooltip open={open} onOpenChange={(nextOpen) => !nextOpen && close()}>
34+
<TooltipTrigger asChild>
35+
<DialogPrimitive.Close
36+
onPointerEnter={(event) => {
37+
if (event.pointerType === "touch") return;
38+
cancelOpen();
39+
openTimeout.current = setTimeout(() => setOpen(true), CLOSE_TOOLTIP_DELAY_MS);
40+
}}
41+
onPointerLeave={close}
42+
className={cn(
43+
"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",
44+
className
45+
)}
46+
>
47+
<CrossIcon className="size-4 text-text-dimmed transition group-hover:text-text-bright" />
48+
<span className="sr-only">Close</span>
49+
</DialogPrimitive.Close>
50+
</TooltipTrigger>
51+
<TooltipContent className="flex items-center py-1.5 pl-2.5 pr-2 text-xs text-text-bright">
52+
Close
53+
<ShortcutKey shortcut={{ key: "esc" }} variant="medium" />
54+
</TooltipContent>
55+
</Tooltip>
56+
</TooltipProvider>
57+
);
58+
}

apps/webapp/app/components/primitives/SheetV3.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { XMarkIcon } from "@heroicons/react/20/solid";
21
import * as SheetPrimitive from "@radix-ui/react-dialog";
32
import { cva, type VariantProps } from "class-variance-authority";
43
import * as React from "react";
54
import { cn } from "~/utils/cn";
6-
import { ShortcutKey } from "./ShortcutKey";
5+
import { ModalCloseButton } from "./ModalCloseButton";
76

87
const Sheet = SheetPrimitive.Root;
98

@@ -91,11 +90,9 @@ const SheetTitle = React.forwardRef<
9190
{...props}
9291
>
9392
{children}
94-
<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">
95-
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
96-
<XMarkIcon className="size-4 text-text-dimmed" />
97-
<span className="sr-only">Close</span>
98-
</SheetPrimitive.Close>
93+
{/* size-6 rather than the default size-7 keeps this header row at the height it had when the
94+
button rendered the esc key alongside the icon — it only gets narrower. */}
95+
<ModalCloseButton className="size-6" />
9996
</SheetPrimitive.Title>
10097
));
10198
SheetTitle.displayName = SheetPrimitive.Title.displayName;

0 commit comments

Comments
 (0)