Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/docs/src/routes/components/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ function AlertDialogDocs() {
<Section id="behavior" title="Behavior">
<p>
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.
</p>
</Section>
</DocsPage>
Expand Down
15 changes: 15 additions & 0 deletions packages/dowel/src/components/alert-dialog/alert-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ describe("AlertDialog", () => {
expect(onConfirm).toHaveBeenCalledOnce();
});

it("dismisses on a backdrop click and returns focus", async () => {
render(<Example />);
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(
<ThemeProvider theme="dark">
Expand Down
36 changes: 32 additions & 4 deletions packages/dowel/src/components/alert-dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,8 +23,23 @@ function partProps(style: stylex.StyleXStyles) {
return { className: resolved.className, style: resolved.style };
}

const AlertDialogActionsContext = createContext<
RefObject<AlertDialogRootActions | null> | undefined
>(undefined);

export const AlertDialog = {
Root: BaseAlertDialog.Root,
Root: function AlertDialogRoot<Payload>(
props: AlertDialogRootProps<Payload>,
) {
const fallbackActionsRef = useRef<AlertDialogRootActions>(null);
const actionsRef = props.actionsRef ?? fallbackActionsRef;

return (
<AlertDialogActionsContext.Provider value={actionsRef}>
<BaseAlertDialog.Root {...props} actionsRef={actionsRef} />
</AlertDialogActionsContext.Provider>
);
},

Trigger: function AlertDialogTrigger(
props: Props<typeof BaseAlertDialog.Trigger>,
Expand Down Expand Up @@ -52,11 +71,20 @@ export const AlertDialog = {
Backdrop: function AlertDialogBackdrop(
props: Props<typeof BaseAlertDialog.Backdrop>,
) {
const actionsRef = useContext(AlertDialogActionsContext);
const { onClick, ...backdropProps } = props;

return (
<BaseAlertDialog.Backdrop
{...props}
{...backdropProps}
{...partProps(styles.backdrop.root)}
data-dowel-component="alert-dialog-backdrop"
onClick={(event) => {
onClick?.(event);
if (!event.defaultPrevented && event.target === event.currentTarget) {
actionsRef?.current?.close();
}
}}
/>
);
},
Expand Down
Loading