-
Notifications
You must be signed in to change notification settings - Fork 1.5k
chore(site): migrate all <Dialog />s off MUI
#27506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
66c09e8
1b2dfc6
23e4814
76adb73
fd21f59
e93243b
1086bb2
e7d6f75
4ae7a3d
a0286d1
37bcd4e
073e313
6cba6a6
0e66cb6
2db38b3
0976c23
c83daeb
0e2ad1f
70dc01f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
| import { expect, fn, screen, userEvent } from "storybook/test"; | ||
| import { ConfirmDialog } from "./ConfirmDialog"; | ||
|
|
||
| const meta: Meta<typeof ConfirmDialog> = { | ||
| title: "components/Dialog/ConfirmDialog", | ||
| component: ConfirmDialog, | ||
| args: { | ||
| onClose: fn(), | ||
| onConfirm: fn(), | ||
| open: true, | ||
| title: "Confirm Dialog", | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof ConfirmDialog>; | ||
|
|
||
| export const Example: Story = { | ||
| args: { | ||
| description: "Do you really want to delete me?", | ||
| hideCancel: false, | ||
| type: "delete", | ||
| }, | ||
| play: async ({ args }) => { | ||
| const dialog = await screen.findByRole("dialog"); | ||
| await expect(dialog).toBeInTheDocument(); | ||
|
|
||
| await userEvent.click(screen.getByRole("button", { name: "Cancel" })); | ||
| await expect(args.onClose).toHaveBeenCalled(); | ||
| }, | ||
| }; | ||
|
|
||
| export const InfoDialog: Story = { | ||
| args: { | ||
| description: "Information is cool!", | ||
| hideCancel: true, | ||
| type: "info", | ||
| }, | ||
| play: async ({ args }) => { | ||
| await expect( | ||
| screen.queryByRole("button", { name: "Cancel" }), | ||
| ).not.toBeInTheDocument(); | ||
|
|
||
| await userEvent.click(screen.getByRole("button", { name: "OK" })); | ||
| await expect(args.onConfirm).toHaveBeenCalled(); | ||
| }, | ||
| }; | ||
|
|
||
| export const InfoDialogWithCancel: Story = { | ||
| args: { | ||
| description: "Information can be cool!", | ||
| hideCancel: false, | ||
| type: "info", | ||
| }, | ||
| }; | ||
|
|
||
| export const SuccessDialog: Story = { | ||
| args: { | ||
| description: "I am successful.", | ||
| hideCancel: true, | ||
| type: "success", | ||
| }, | ||
| }; | ||
|
|
||
| export const SuccessDialogWithCancel: Story = { | ||
| args: { | ||
| description: "I may be successful.", | ||
| hideCancel: false, | ||
| type: "success", | ||
| }, | ||
| }; | ||
|
|
||
| export const SuccessDialogLoading: Story = { | ||
| args: { | ||
| description: "I am successful.", | ||
| hideCancel: true, | ||
| type: "success", | ||
| confirmLoading: true, | ||
| }, | ||
| play: async () => { | ||
| // Spinner prefixes the accessible name with "Loading spinner". | ||
| await expect(screen.getByRole("button", { name: /ok/i })).toBeDisabled(); | ||
| }, | ||
| }; | ||
|
|
||
| export const ConfirmAction: Story = { | ||
| args: { | ||
| description: "Do you really want to delete me?", | ||
| hideCancel: false, | ||
| type: "delete", | ||
| confirmText: "CONFIRM", | ||
| cancelText: "CANCEL", | ||
| }, | ||
| play: async ({ args }) => { | ||
| await userEvent.click(screen.getByRole("button", { name: "CONFIRM" })); | ||
| await expect(args.onConfirm).toHaveBeenCalled(); | ||
| await expect(args.onClose).not.toHaveBeenCalled(); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import type { FC, ReactNode } from "react"; | ||
| import { | ||
| Dialog, | ||
| DialogActions, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| } from "#/components/Dialog/Dialog"; | ||
|
|
||
| type ConfirmDialogType = "delete" | "info" | "success"; | ||
|
|
||
| interface ConfirmDialogTypeConfig { | ||
| confirmText: ReactNode; | ||
| hideCancel: boolean; | ||
| } | ||
|
|
||
| const CONFIRM_DIALOG_DEFAULTS: Record< | ||
| ConfirmDialogType, | ||
| ConfirmDialogTypeConfig | ||
| > = { | ||
| delete: { | ||
| confirmText: "Delete", | ||
| hideCancel: false, | ||
| }, | ||
| info: { | ||
| confirmText: "OK", | ||
| hideCancel: true, | ||
| }, | ||
| success: { | ||
| confirmText: "OK", | ||
| hideCancel: true, | ||
| }, | ||
| }; | ||
|
|
||
| export interface ConfirmDialogProps { | ||
| readonly title: string; | ||
| readonly open: boolean; | ||
| readonly onClose: () => void; | ||
| readonly description: ReactNode; | ||
| readonly cancelText?: string; | ||
| readonly confirmText?: ReactNode; | ||
| readonly confirmLoading?: boolean; | ||
| readonly disabled?: boolean; | ||
| /** | ||
| * When omitted, `onClose` doubles as the confirm handler, so a `delete` | ||
| * dialog without `onConfirm` closes without deleting anything. | ||
| */ | ||
| readonly onConfirm?: () => void; | ||
|
jakehwll marked this conversation as resolved.
|
||
| readonly type?: ConfirmDialogType; | ||
| /** | ||
| * Defaults to shown for "delete", hidden for "info"/"success". | ||
| */ | ||
| readonly hideCancel?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Quick-use dialog for yes/no style confirmations without custom layout. | ||
| */ | ||
| export const ConfirmDialog: FC<ConfirmDialogProps> = ({ | ||
| cancelText = "Cancel", | ||
| confirmLoading = false, | ||
| confirmText, | ||
| description, | ||
| disabled = false, | ||
| hideCancel, | ||
| onClose, | ||
| onConfirm, | ||
| open = false, | ||
| title, | ||
| type = "info", | ||
| }) => { | ||
| const defaults = CONFIRM_DIALOG_DEFAULTS[type]; | ||
| const shouldHideCancel = hideCancel ?? defaults.hideCancel; | ||
| const resolvedConfirmText = confirmText ?? defaults.confirmText; | ||
| const handleConfirm = onConfirm ?? onClose; | ||
|
|
||
| return ( | ||
| <Dialog | ||
| open={open} | ||
| onOpenChange={(nextOpen) => { | ||
| if (!nextOpen) { | ||
| onClose(); | ||
| } | ||
| }} | ||
| > | ||
| <DialogContent | ||
| variant={type === "delete" ? "destructive" : "default"} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok but the annoying thing here is that we also use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed the
|
||
| data-testid="dialog" | ||
| > | ||
| <DialogHeader> | ||
| <DialogTitle>{title}</DialogTitle> | ||
| <DialogDescription asChild> | ||
| <div className="text-sm text-content-secondary font-medium [&_strong]:text-content-primary [&_p]:m-0 [&_p+p]:mt-2"> | ||
| {description} | ||
| </div> | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <DialogFooter> | ||
|
jakehwll marked this conversation as resolved.
|
||
| <DialogActions | ||
| cancelText={cancelText} | ||
| onCancel={shouldHideCancel ? undefined : onClose} | ||
| confirmText={resolvedConfirmText} | ||
| confirmLoading={confirmLoading} | ||
| confirmDisabled={disabled} | ||
| confirmVariant={type === "delete" ? "destructive" : undefined} | ||
| onConfirm={handleConfirm} | ||
| /> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
| import { expect, fn, userEvent, within } from "storybook/test"; | ||
| import { DeleteDialog } from "./DeleteDialog"; | ||
|
|
||
| const meta: Meta<typeof DeleteDialog> = { | ||
| title: "components/Dialog/DeleteDialog", | ||
| component: DeleteDialog, | ||
| args: { | ||
| onCancel: fn(), | ||
| onConfirm: fn(), | ||
| isOpen: true, | ||
| entity: "foo", | ||
| name: "MyFoo", | ||
| info: "Here's some info about the foo so you know you're deleting the right one.", | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof DeleteDialog>; | ||
|
|
||
| export const Idle: Story = { | ||
| play: async ({ canvasElement }) => { | ||
| const body = within(canvasElement.ownerDocument.body); | ||
| await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled(); | ||
| }, | ||
| }; | ||
|
|
||
| export const FilledSuccessfully: Story = { | ||
| play: async ({ canvasElement, args }) => { | ||
| const user = userEvent.setup(); | ||
| const body = within(canvasElement.ownerDocument.body); | ||
| const input = await body.findByLabelText("Name of the foo to delete"); | ||
| await user.type(input, "MyFoo"); | ||
|
|
||
| const confirmButton = body.getByRole("button", { name: "Delete" }); | ||
| await expect(confirmButton).toBeEnabled(); | ||
| await user.click(confirmButton); | ||
| await expect(args.onConfirm).toHaveBeenCalled(); | ||
| }, | ||
| }; | ||
|
|
||
| export const FilledWrong: Story = { | ||
| play: async ({ canvasElement }) => { | ||
| const user = userEvent.setup(); | ||
| const body = within(canvasElement.ownerDocument.body); | ||
| const input = await body.findByLabelText("Name of the foo to delete"); | ||
| await user.type(input, "InvalidFooName"); | ||
| // Blur so the mismatch error becomes visible. | ||
| await user.tab(); | ||
|
|
||
| await expect(body.getByRole("button", { name: "Delete" })).toBeDisabled(); | ||
| await expect( | ||
| body.getByText("InvalidFooName does not match the name of this foo"), | ||
| ).toBeVisible(); | ||
| }, | ||
| }; | ||
|
|
||
| export const Loading: Story = { | ||
| args: { | ||
| confirmLoading: true, | ||
| }, | ||
| play: async ({ canvasElement }) => { | ||
| const body = within(canvasElement.ownerDocument.body); | ||
| // Spinner prefixes the accessible name with "Loading spinner". | ||
| await expect(body.getByRole("button", { name: /delete/i })).toBeDisabled(); | ||
| await expect(body.getByRole("button", { name: "Cancel" })).toBeDisabled(); | ||
| }, | ||
| }; |
Uh oh!
There was an error while loading. Please reload this page.