diff --git a/apps/docs/src/routes/components/alert-dialog.tsx b/apps/docs/src/routes/components/alert-dialog.tsx index b6767d3..7492946 100644 --- a/apps/docs/src/routes/components/alert-dialog.tsx +++ b/apps/docs/src/routes/components/alert-dialog.tsx @@ -73,8 +73,9 @@ function AlertDialogDocs() {

Opening traps focus and announces both title and description. Closing - returns focus to the trigger. Use Dialog for ordinary tasks and forms; - reserve Alert Dialog for a decision that requires explicit attention. + with Escape, the backdrop, or either action returns focus to the + trigger. Use Dialog for ordinary tasks and forms; reserve Alert Dialog + for a decision that requires explicit attention.

diff --git a/packages/dowel/src/components/alert-dialog/alert-dialog.test.tsx b/packages/dowel/src/components/alert-dialog/alert-dialog.test.tsx index f200fef..58df6a1 100644 --- a/packages/dowel/src/components/alert-dialog/alert-dialog.test.tsx +++ b/packages/dowel/src/components/alert-dialog/alert-dialog.test.tsx @@ -55,6 +55,21 @@ describe("AlertDialog", () => { expect(onConfirm).toHaveBeenCalledOnce(); }); + it("dismisses on a backdrop click and returns focus", async () => { + render(); + const trigger = screen.getByRole("button", { name: "Delete repository" }); + await userEvent.click(trigger); + + const backdrop = document.querySelector( + '[data-dowel-component="alert-dialog-backdrop"]', + ); + expect(backdrop).not.toBeNull(); + await userEvent.click(backdrop!); + + await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull()); + await waitFor(() => expect(document.activeElement).toBe(trigger)); + }); + it("carries the active theme into its portal", async () => { render( diff --git a/packages/dowel/src/components/alert-dialog/index.tsx b/packages/dowel/src/components/alert-dialog/index.tsx index ca22912..561c190 100644 --- a/packages/dowel/src/components/alert-dialog/index.tsx +++ b/packages/dowel/src/components/alert-dialog/index.tsx @@ -1,7 +1,11 @@ import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog"; +import type { + AlertDialogRootActions, + AlertDialogRootProps, +} from "@base-ui/react/alert-dialog"; import * as stylex from "@stylexjs/stylex"; -import { forwardRef, useContext } from "react"; -import type { ComponentPropsWithoutRef } from "react"; +import { createContext, forwardRef, useContext, useRef } from "react"; +import type { ComponentPropsWithoutRef, RefObject } from "react"; import { DowelThemeContext, themeStyles } from "../../theme/theme-provider"; import * as styles from "../dialog/dialog.stylex"; @@ -19,8 +23,23 @@ function partProps(style: stylex.StyleXStyles) { return { className: resolved.className, style: resolved.style }; } +const AlertDialogActionsContext = createContext< + RefObject | undefined +>(undefined); + export const AlertDialog = { - Root: BaseAlertDialog.Root, + Root: function AlertDialogRoot( + props: AlertDialogRootProps, + ) { + const fallbackActionsRef = useRef(null); + const actionsRef = props.actionsRef ?? fallbackActionsRef; + + return ( + + + + ); + }, Trigger: function AlertDialogTrigger( props: Props, @@ -52,11 +71,20 @@ export const AlertDialog = { Backdrop: function AlertDialogBackdrop( props: Props, ) { + const actionsRef = useContext(AlertDialogActionsContext); + const { onClick, ...backdropProps } = props; + return ( { + onClick?.(event); + if (!event.defaultPrevented && event.target === event.currentTarget) { + actionsRef?.current?.close(); + } + }} /> ); },