Skip to content

Commit f1ba0d2

Browse files
committed
fix(webapp): make Esc close consistently across dialogs, sheets and panels
- Vercel onboarding modal no longer renders a close button and Esc hint in the marketplace flow, where dismissal is intentionally blocked - The authenticator app setup dialog can be dismissed with Esc or the close button; the recovery codes step stays deliberately non-dismissible - The dashboard chat panel closes with Esc, using the same shortcut button the other detail panels use - Every sheet gets the Esc hint from a shared close button in SheetContent instead of opting in via SheetTitle - Panel close shortcuts fire while a form field is focused - A shortcut is ignored while its button sits behind an open overlay, so one Esc no longer closes both a dialog and the panel behind it Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6449a64 commit f1ba0d2

20 files changed

Lines changed: 89 additions & 38 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Esc now closes the chat panel, the authenticator app setup dialog, and detail panels while you're typing in one of their fields, and every side sheet shows its Esc shortcut. Closing a dialog with Esc no longer also closes the panel behind it, and the Vercel setup modal no longer shows a close button it ignores.

apps/webapp/app/components/dashboard-agent/DashboardAgentHeader.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ClockIcon, PencilSquareIcon, XMarkIcon } from "@heroicons/react/20/solid";
2+
import { Button } from "~/components/primitives/Buttons";
23
import { cn } from "~/utils/cn";
34

45
export function DashboardAgentHeader({
@@ -23,7 +24,15 @@ export function DashboardAgentHeader({
2324
onClick={onToggleHistory}
2425
active={view === "history"}
2526
/>
26-
<IconButton label="Close" icon={XMarkIcon} onClick={onClose} />
27+
<Button
28+
onClick={onClose}
29+
variant="minimal/small"
30+
TrailingIcon={XMarkIcon}
31+
shortcut={{ key: "esc", enabledOnInputElements: true }}
32+
shortcutPosition="before-trailing-icon"
33+
className="pl-1"
34+
aria-label="Close"
35+
/>
2736
</div>
2837
</div>
2938
);

apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { useFetcher, useNavigate } from "@remix-run/react";
77
import { SlackIcon } from "@trigger.dev/companyicons";
88
import { Fragment, useEffect, useRef, useState } from "react";
99
import { z } from "zod";
10-
import { ExitIcon } from "~/assets/icons/ExitIcon";
1110
import { InlineCode } from "~/components/code/InlineCode";
1211
import { Button, LinkButton } from "~/components/primitives/Buttons";
1312
import { Callout, variantClasses } from "~/components/primitives/Callout";
@@ -120,14 +119,6 @@ export function ConfigureErrorAlerts({
120119
<Header2 className="flex items-center gap-2">
121120
<BellAlertIcon className="size-5 text-alerts" /> Configure alerts
122121
</Header2>
123-
<LinkButton
124-
to={closeHref}
125-
variant="minimal/small"
126-
TrailingIcon={ExitIcon}
127-
shortcut={{ key: "esc" }}
128-
shortcutPosition="before-trailing-icon"
129-
className="pl-1"
130-
/>
131122
</div>
132123

133124
<fetcher.Form method="post" action={formAction} {...getFormProps(form)} className="contents">

apps/webapp/app/components/integrations/VercelOnboardingModal.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,11 @@ export function VercelOnboardingModal({
747747
}
748748
}}
749749
>
750-
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
750+
<DialogContent
751+
className="max-w-lg"
752+
onInteractOutside={(e) => e.preventDefault()}
753+
showCloseButton={!fromMarketplaceContext}
754+
>
751755
<DialogHeader>
752756
<div className="flex items-center gap-2">
753757
<VercelLogo className="size-5" />
@@ -787,7 +791,11 @@ export function VercelOnboardingModal({
787791
}
788792
}}
789793
>
790-
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
794+
<DialogContent
795+
className="max-w-lg"
796+
onInteractOutside={(e) => e.preventDefault()}
797+
showCloseButton={!fromMarketplaceContext}
798+
>
791799
<DialogHeader>
792800
<div className="flex items-center gap-2">
793801
<VercelLogo className="size-5" />

apps/webapp/app/components/logs/LogDetailView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
117117
onClick={onClose}
118118
variant="minimal/small"
119119
TrailingIcon={ExitIcon}
120-
shortcut={{ key: "esc" }}
120+
shortcut={{ key: "esc", enabledOnInputElements: true }}
121121
shortcutPosition="before-trailing-icon"
122122
className="pl-1"
123123
/>
@@ -138,7 +138,7 @@ export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDet
138138
onClick={onClose}
139139
variant="minimal/small"
140140
TrailingIcon={ExitIcon}
141-
shortcut={{ key: "esc" }}
141+
shortcut={{ key: "esc", enabledOnInputElements: true }}
142142
shortcutPosition="before-trailing-icon"
143143
className="pl-1"
144144
/>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
397397
}
398398
},
399399
disabled: isDisabled || !props.shortcut,
400+
elementRef: innerRef,
400401
});
401402

402403
const buttonElement = (
@@ -467,6 +468,7 @@ export const LinkButton = ({
467468
}
468469
},
469470
disabled: disabled || !props.shortcut,
471+
elementRef: innerRef,
470472
});
471473

472474
if (disabled) {

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,28 @@ const sheetVariants = cva(
5050
interface SheetContentProps
5151
extends
5252
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
53-
VariantProps<typeof sheetVariants> {}
53+
VariantProps<typeof sheetVariants> {
54+
showCloseButton?: boolean;
55+
}
5456

5557
const SheetContent = React.forwardRef<
5658
React.ElementRef<typeof SheetPrimitive.Content>,
5759
SheetContentProps
58-
>(({ side = "right", className, children, ...props }, ref) => (
60+
>(({ side = "right", className, children, showCloseButton = true, ...props }, ref) => (
5961
<SheetPortal>
6062
<SheetOverlay />
6163
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
64+
{showCloseButton && (
65+
// Zero-height sticky wrapper so the button stays pinned to the top right
66+
// even when the sheet itself is the scrolling element.
67+
<div className="sticky top-0 z-10 h-0">
68+
<SheetPrimitive.Close className="absolute right-2 top-2 flex items-center gap-1 rounded-sm p-1 pl-0 transition hover:bg-background-hover focus-visible:focus-custom disabled:pointer-events-none">
69+
<ShortcutKey shortcut={{ key: "esc" }} variant="small" />
70+
<XMarkIcon className="size-4 text-text-dimmed" />
71+
<span className="sr-only">Close</span>
72+
</SheetPrimitive.Close>
73+
</div>
74+
)}
6275
{children}
6376
</SheetPrimitive.Content>
6477
</SheetPortal>
@@ -85,17 +98,12 @@ const SheetTitle = React.forwardRef<
8598
<SheetPrimitive.Title
8699
ref={ref}
87100
className={cn(
88-
"sticky top-0 flex items-center justify-between border-b border-grid-bright bg-background-dimmed pb-1.5 pl-3 pr-1.5 pt-2",
101+
"sticky top-0 flex items-center justify-between border-b border-grid-bright bg-background-dimmed pb-1.5 pl-3 pr-12 pt-2",
89102
className
90103
)}
91104
{...props}
92105
>
93106
{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>
99107
</SheetPrimitive.Title>
100108
));
101109
SheetTitle.displayName = SheetPrimitive.Title.displayName;

apps/webapp/app/hooks/useShortcutKeys.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type RefObject } from "react";
12
import { useHotkeys } from "react-hotkeys-hook";
23
import { useOperatingSystem } from "~/components/primitives/OperatingSystemProvider";
34
import { useShortcuts } from "~/components/primitives/ShortcutsProvider";
@@ -23,13 +24,34 @@ type useShortcutKeysProps = {
2324
action: (event: KeyboardEvent) => void;
2425
disabled?: boolean;
2526
enabledOnInputElements?: boolean;
27+
/**
28+
* The element this shortcut belongs to. When set, the shortcut is ignored while
29+
* the element sits behind an open overlay, so one Escape can't close both a
30+
* dialog and the panel behind it.
31+
*/
32+
elementRef?: RefObject<HTMLElement | null>;
2633
};
2734

35+
/** Layered surfaces that own the keyboard while they're open. */
36+
const OVERLAY_ROLES = '[role="dialog"],[role="alertdialog"],[role="listbox"],[role="menu"]';
37+
38+
function isBlockedByOverlay(event: KeyboardEvent, element: HTMLElement | null) {
39+
// Radix marks everything outside an open modal `aria-hidden`, which covers
40+
// modals that don't move focus into themselves.
41+
if (element?.closest('[aria-hidden="true"]')) return true;
42+
43+
const target = event.target instanceof Element ? event.target : null;
44+
const overlay = target?.closest(OVERLAY_ROLES);
45+
46+
return !!overlay && (!element || !overlay.contains(element));
47+
}
48+
2849
export function useShortcutKeys({
2950
shortcut,
3051
action,
3152
disabled = false,
3253
enabledOnInputElements,
54+
elementRef,
3355
}: useShortcutKeysProps) {
3456
const { platform } = useOperatingSystem();
3557
const { areShortcutsEnabled } = useShortcuts();
@@ -44,9 +66,10 @@ export function useShortcutKeys({
4466
useHotkeys(
4567
keys,
4668
(event) => {
47-
if (!event.repeat) {
48-
action(event);
49-
}
69+
if (event.repeat) return;
70+
if (elementRef && isBlockedByOverlay(event, elementRef.current)) return;
71+
72+
action(event);
5073
},
5174
{
5275
enabled: isEnabled,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ function NewTaskPromptsPanel({ onClose }: { onClose: () => void }) {
844844
onClick={onClose}
845845
variant="minimal/small"
846846
TrailingIcon={ExitIcon}
847-
shortcut={{ key: "esc" }}
847+
shortcut={{ key: "esc", enabledOnInputElements: true }}
848848
shortcutPosition="before-trailing-icon"
849849
className="pl-1"
850850
/>

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default function Page() {
9696
to={v3BatchesPath(organization, project, environment)}
9797
variant="minimal/small"
9898
TrailingIcon={ExitIcon}
99-
shortcut={{ key: "esc" }}
99+
shortcut={{ key: "esc", enabledOnInputElements: true }}
100100
shortcutPosition="before-trailing-icon"
101101
className="pl-1"
102102
/>

0 commit comments

Comments
 (0)