diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-open-linux.png deleted file mode 100644 index e36b6b5c7a2..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-linux.png deleted file mode 100644 index 15e6d5852fb..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-open-linux.png deleted file mode 100644 index bd676876386..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-dimmed-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-linux.png deleted file mode 100644 index e4cdc12a1ef..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-open-linux.png deleted file mode 100644 index f76d26b24b0..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-open-linux.png deleted file mode 100644 index 3ab67fb9f55..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-open-linux.png deleted file mode 100644 index 3ab67fb9f55..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-dark-tritanopia-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-open-linux.png deleted file mode 100644 index f9183dc7602..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-linux.png deleted file mode 100644 index 14c00a9b278..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-open-linux.png deleted file mode 100644 index 251f630f4a4..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-open-linux.png deleted file mode 100644 index a082cfc9e25..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-open-linux.png deleted file mode 100644 index a082cfc9e25..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Confirmation-Dialog-light-tritanopia-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-open-linux.png deleted file mode 100644 index 54679e020ff..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-linux.png deleted file mode 100644 index 15e6d5852fb..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-open-linux.png deleted file mode 100644 index ae452818bf2..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-dimmed-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-linux.png deleted file mode 100644 index e4cdc12a1ef..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-open-linux.png deleted file mode 100644 index ec45bf5f92e..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-open-linux.png deleted file mode 100644 index c796266fe2c..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-open-linux.png deleted file mode 100644 index d681c8b46cb..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-dark-tritanopia-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-open-linux.png deleted file mode 100644 index ca12c0ea3db..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-linux.png deleted file mode 100644 index 14c00a9b278..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-open-linux.png deleted file mode 100644 index 68a4d671782..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-open-linux.png deleted file mode 100644 index e9ec32de38f..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-open-linux.png deleted file mode 100644 index 2cddc730547..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Basic-Dialog-light-tritanopia-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-open-linux.png deleted file mode 100644 index 52cf69ace23..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-linux.png deleted file mode 100644 index 15e6d5852fb..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-open-linux.png deleted file mode 100644 index 39f42ada002..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-dimmed-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-linux.png deleted file mode 100644 index e4cdc12a1ef..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-open-linux.png deleted file mode 100644 index c136e00cc79..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-open-linux.png deleted file mode 100644 index 52cf69ace23..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-linux.png deleted file mode 100644 index 7491948ec9a..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-open-linux.png deleted file mode 100644 index 52cf69ace23..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-dark-tritanopia-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-open-linux.png deleted file mode 100644 index 06482d06421..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-colorblind-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-linux.png deleted file mode 100644 index 14c00a9b278..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-open-linux.png deleted file mode 100644 index 050ed0b3a66..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-high-contrast-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-open-linux.png deleted file mode 100644 index 06482d06421..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-open-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-linux.png deleted file mode 100644 index a0cac7f272b..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-open-linux.png b/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-open-linux.png deleted file mode 100644 index 06482d06421..00000000000 Binary files a/.playwright/snapshots/components/DialogV2.test.ts-snapshots/DialogV2-Default-light-tritanopia-open-linux.png and /dev/null differ diff --git a/docs/content/Dialog.mdx b/docs/content/Dialog.mdx index d724ff7f835..1b3c828e3f5 100644 --- a/docs/content/Dialog.mdx +++ b/docs/content/Dialog.mdx @@ -3,7 +3,7 @@ title: Dialog status: Alpha --- -import data from '../../src/Dialog/Dialog.docs.json' +import data from '../../src/Dialog.docs.json' The dialog component is used for all modals. It renders on top of the rest of the app with an overlay. diff --git a/docs/content/drafts/Dialog2.mdx b/docs/content/drafts/Dialog.mdx similarity index 97% rename from docs/content/drafts/Dialog2.mdx rename to docs/content/drafts/Dialog.mdx index 0d89fa0e764..3db183706d0 100644 --- a/docs/content/drafts/Dialog2.mdx +++ b/docs/content/drafts/Dialog.mdx @@ -1,14 +1,10 @@ --- title: Dialog v2 -componentId: dialog_2 +componentId: dialog status: Draft -a11yReviewed: false -description: Use an underlined nav to allow tab like navigation with overflow behaviour in your UI. -source: https://github.com/primer/react/tree/main/src/Dialog2 -storybook: '/react/storybook/?path=/story/drafts-components-dialog--default' --- -import data from '../../../src/Dialog2/Dialog.docs.json' +import data from '../../../src/Dialog/Dialog.docs.json' ```js import {Dialog} from '@primer/react/drafts' diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 82605d53b56..da1d837ce2b 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -159,7 +159,7 @@ - title: Drafts children: - title: Dialog v2 - url: /drafts/Dialog2 + url: /drafts/Dialog - title: InlineAutocomplete url: /drafts/InlineAutocomplete - title: MarkdownEditor diff --git a/e2e/components/DialogV2.test.ts b/e2e/components/DialogV2.test.ts deleted file mode 100644 index f982ba9e610..00000000000 --- a/e2e/components/DialogV2.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import {test, expect} from '@playwright/test' -import {visit} from '../test-helpers/storybook' -import {themes} from '../test-helpers/themes' - -test.describe('DialogV2', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog--default', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Default.${theme}.png`) - // Open Dialog - await page.getByRole('button', {name: 'Show dialog'}).click() - // wait for dialog to open - await page.getByRole('dialog', {name: 'Dialog'}).waitFor() - // Open state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Default.${theme}.open.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Basic Dialog', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog-features--basic-dialog', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Basic Dialog.${theme}.png`) - // Open Dialog - await page.getByRole('button', {name: 'Show dialog'}).click() - // wait for dialog to open - await page.getByRole('dialog', {name: 'Dialog'}).waitFor() - // Open state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Basic Dialog.${theme}.open.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog-features--basic-dialog', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Basic Confirmation Dialog', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog-features--basic-confirmation-dialog', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Basic Confirmation Dialog.${theme}.png`) - // Open Dialog - await page.getByRole('button', {name: 'Show dialog'}).click() - // wait for dialog to open - await page.getByRole('alertdialog').waitFor() - // Open state - expect( - await page.screenshot({ - animations: 'disabled', - }), - ).toMatchSnapshot(`DialogV2.Basic Confirmation Dialog.${theme}.open.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'drafts-components-dialog-features--basic-confirmation-dialog', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) -}) diff --git a/generated/components.json b/generated/components.json index 11ce9edb1b2..33808f80b37 100644 --- a/generated/components.json +++ b/generated/components.json @@ -122,6 +122,60 @@ ], "subcomponents": [] }, + "dialog": { + "id": "dialog", + "name": "Dialog", + "status": "alpha", + "a11yReviewed": false, + "stories": [], + "props": [ + { + "name": "isOpen", + "type": "boolean", + "description": "Whether or not the dialog is open" + }, + { + "name": "onDismiss", + "type": "() => void", + "description": "Function that will be called when the dialog is closed" + }, + { + "name": "returnFocusRef", + "type": " React.RefObject", + "description": "The element to restore focus back to after the `Dialog` is closed" + }, + { + "name": "initialFocusRef", + "type": " React.RefObject", + "description": "Element inside of the `Dialog` you'd like to be focused when the Dialog is opened. If nothing is passed to `initialFocusRef` the close button is focused." + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Pass an id to use for the aria-label. Use either a `aria-label` or an `aria-labelledby` but not both." + }, + { + "name": "aria-label", + "type": "string", + "description": "Pass a label to be used to describe the Dialog. Use either a `aria-label` or an `aria-labelledby` but not both." + }, + { + "name": "sx", + "type": "SystemStyleObject" + } + ], + "subcomponents": [ + { + "name": "Dialog.Header", + "props": [ + { + "name": "sx", + "type": "SystemStyleObject" + } + ] + } + ] + }, "filter_list": { "id": "filter_list", "name": "FilterList", @@ -2053,76 +2107,12 @@ ], "subcomponents": [] }, - "dialog": { - "id": "dialog", - "name": "Dialog", - "status": "alpha", - "a11yReviewed": false, - "stories": [ - { - "id": "components-dialog--default", - "code": "() => {\n const [isOpen, setIsOpen] = useState(false)\n const returnFocusRef = React.useRef(null)\n return (\n <>\n \n setIsOpen(false)}\n aria-labelledby=\"header-id\"\n >\n Title\n some content\n \n \n )\n}" - } - ], - "props": [ - { - "name": "isOpen", - "type": "boolean", - "description": "Whether or not the dialog is open" - }, - { - "name": "onDismiss", - "type": "() => void", - "description": "Function that will be called when the dialog is closed" - }, - { - "name": "returnFocusRef", - "type": " React.RefObject", - "description": "The element to restore focus back to after the `Dialog` is closed" - }, - { - "name": "initialFocusRef", - "type": " React.RefObject", - "description": "Element inside of the `Dialog` you'd like to be focused when the Dialog is opened. If nothing is passed to `initialFocusRef` the close button is focused." - }, - { - "name": "aria-labelledby", - "type": "string", - "description": "Pass an id to use for the aria-label. Use either a `aria-label` or an `aria-labelledby` but not both." - }, - { - "name": "aria-label", - "type": "string", - "description": "Pass a label to be used to describe the Dialog. Use either a `aria-label` or an `aria-labelledby` but not both." - }, - { - "name": "sx", - "type": "SystemStyleObject" - } - ], - "subcomponents": [ - { - "name": "Dialog.Header", - "props": [ - { - "name": "sx", - "type": "SystemStyleObject" - } - ] - } - ] - }, "drafts_dialog": { "id": "drafts_dialog", "name": "Dialog", "status": "draft", "a11yReviewed": false, - "stories": [ - { - "id": "components-dialog--default", - "code": "() => {\n const [isOpen, setIsOpen] = useState(false)\n const buttonRef = useRef(null)\n const onDialogClose = useCallback(() => setIsOpen(false), [])\n return (\n <>\n \n {isOpen && Dialog Content}\n \n )\n}" - } - ], + "stories": [], "props": [ { "name": "title", diff --git a/script/generate-e2e-tests.js b/script/generate-e2e-tests.js index 3338f7a9fab..d7372e486c5 100644 --- a/script/generate-e2e-tests.js +++ b/script/generate-e2e-tests.js @@ -336,25 +336,6 @@ const components = new Map([ ], }, ], - [ - 'DialogV2', - { - stories: [ - { - id: 'drafts-components-dialog--default', - name: 'Default', - }, - { - id: 'drafts-components-dialog-features--basic-dialog', - name: 'Basic Dialog', - }, - { - id: 'drafts-components-dialog-features--basic-confirmation-dialog', - name: 'Basic Confirmation Dialog', - }, - ], - }, - ], [ 'Flash', { diff --git a/src/Dialog.docs.json b/src/Dialog.docs.json new file mode 100644 index 00000000000..593f3688b3f --- /dev/null +++ b/src/Dialog.docs.json @@ -0,0 +1,54 @@ +{ + "id": "dialog", + "name": "Dialog", + "status": "alpha", + "a11yReviewed": false, + "stories": [], + "props": [ + { + "name": "isOpen", + "type": "boolean", + "description": "Whether or not the dialog is open" + }, + { + "name": "onDismiss", + "type": "() => void", + "description": "Function that will be called when the dialog is closed" + }, + { + "name": "returnFocusRef", + "type": " React.RefObject", + "description": "The element to restore focus back to after the `Dialog` is closed" + }, + { + "name": "initialFocusRef", + "type": " React.RefObject", + "description": "Element inside of the `Dialog` you'd like to be focused when the Dialog is opened. If nothing is passed to `initialFocusRef` the close button is focused." + }, + { + "name": "aria-labelledby", + "type": "string", + "description": "Pass an id to use for the aria-label. Use either a `aria-label` or an `aria-labelledby` but not both." + }, + { + "name": "aria-label", + "type": "string", + "description": "Pass a label to be used to describe the Dialog. Use either a `aria-label` or an `aria-labelledby` but not both." + }, + { + "name": "sx", + "type": "SystemStyleObject" + } + ], + "subcomponents": [ + { + "name": "Dialog.Header", + "props": [ + { + "name": "sx", + "type": "SystemStyleObject" + } + ] + } + ] +} diff --git a/src/Dialog.tsx b/src/Dialog.tsx new file mode 100644 index 00000000000..2df90952959 --- /dev/null +++ b/src/Dialog.tsx @@ -0,0 +1,142 @@ +import React, {forwardRef, useRef} from 'react' +import styled from 'styled-components' +import ButtonClose from './deprecated/Button/ButtonClose' +import {get} from './constants' +import Box from './Box' +import useDialog from './hooks/useDialog' +import sx, {SxProp} from './sx' +import Text from './Text' +import {ComponentProps} from './utils/types' +import {useRefObjectAsForwardedRef} from './hooks/useRefObjectAsForwardedRef' + +const noop = () => null + +type StyledDialogBaseProps = { + narrow?: boolean + wide?: boolean +} & SxProp + +const DialogBase = styled.div` + box-shadow: ${get('shadows.shadow.large')}; + border-radius: ${get('radii.2')}; + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + max-height: 80vh; + z-index: 999; + margin: 10vh auto; + background-color: ${get('colors.canvas.default')}; + width: ${props => (props.narrow ? '320px' : props.wide ? '640px' : '440px')}; + outline: none; + + @media screen and (max-width: 750px) { + width: 100vw; + margin: 0; + border-radius: 0; + height: 100vh; + } + + ${sx}; +` + +const DialogHeaderBase = styled(Box)` + border-radius: ${get('radii.2')} ${get('radii.2')} 0px 0px; + border-bottom: 1px solid ${get('colors.border.default')}; + display: flex; + + @media screen and (max-width: 750px) { + border-radius: 0px; + } + + ${sx}; +` +export type DialogHeaderProps = ComponentProps + +function DialogHeader({theme, children, backgroundColor = 'canvas.subtle', ...rest}: DialogHeaderProps) { + if (React.Children.toArray(children).every(ch => typeof ch === 'string')) { + children = ( + + {children} + + ) + } + + return ( + + {children} + + ) +} + +const Overlay = styled.span` + &:before { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: block; + cursor: default; + content: ' '; + background: transparent; + z-index: 99; + background: ${get('colors.primer.canvas.backdrop')}; + } +` + +type InternalDialogProps = { + isOpen?: boolean + onDismiss?: () => void + initialFocusRef?: React.RefObject + returnFocusRef?: React.RefObject +} & ComponentProps + +const Dialog = forwardRef( + ({children, onDismiss = noop, isOpen, initialFocusRef, returnFocusRef, ...props}, forwardedRef) => { + const overlayRef = useRef(null) + const modalRef = useRef(null) + useRefObjectAsForwardedRef(forwardedRef, modalRef) + const closeButtonRef = useRef(null) + + const onCloseClick = () => { + onDismiss() + if (returnFocusRef && returnFocusRef.current) { + returnFocusRef.current.focus() + } + } + + const {getDialogProps} = useDialog({ + modalRef, + onDismiss: onCloseClick, + isOpen, + initialFocusRef, + closeButtonRef, + returnFocusRef, + overlayRef, + }) + return isOpen ? ( + <> + + + + {children} + + + ) : null + }, +) + +DialogHeader.propTypes = { + ...Box.propTypes, +} + +DialogHeader.displayName = 'Dialog.Header' +Dialog.displayName = 'Dialog' + +export type DialogProps = ComponentProps +export default Object.assign(Dialog, {Header: DialogHeader}) diff --git a/src/Dialog2/ConfirmationDialog.tsx b/src/Dialog/ConfirmationDialog.tsx similarity index 99% rename from src/Dialog2/ConfirmationDialog.tsx rename to src/Dialog/ConfirmationDialog.tsx index a4fcfaa09b9..2fa2259849e 100644 --- a/src/Dialog2/ConfirmationDialog.tsx +++ b/src/Dialog/ConfirmationDialog.tsx @@ -5,7 +5,7 @@ import Box from '../Box' import {ThemeProvider, useTheme, ThemeProviderProps} from '../ThemeProvider' import {FocusKeys} from '@primer/behaviors' import {get} from '../constants' -import {Dialog, DialogProps, DialogHeaderProps, DialogButtonProps} from './Dialog' +import {Dialog, DialogProps, DialogHeaderProps, DialogButtonProps} from '../Dialog/Dialog' import {useFocusZone} from '../hooks/useFocusZone' /** diff --git a/src/Dialog/Dialog.docs.json b/src/Dialog/Dialog.docs.json index 593f3688b3f..5b803588341 100644 --- a/src/Dialog/Dialog.docs.json +++ b/src/Dialog/Dialog.docs.json @@ -1,54 +1,58 @@ { - "id": "dialog", + "id": "drafts_dialog", "name": "Dialog", - "status": "alpha", + "status": "draft", "a11yReviewed": false, "stories": [], "props": [ { - "name": "isOpen", - "type": "boolean", - "description": "Whether or not the dialog is open" + "name": "title", + "type": "React.ReactNode", + "description": "Title of the Dialog. Also serves as the aria-label for this Dialog." }, { - "name": "onDismiss", - "type": "() => void", - "description": "Function that will be called when the dialog is closed" + "name": "subtitle", + "type": "React.ReactNode", + "description": "The Dialog's subtitle. Optional. Rendered below the title in smaller type with less contrast. Also serves as the aria-describedby for this Dialog." }, { - "name": "returnFocusRef", - "type": " React.RefObject", - "description": "The element to restore focus back to after the `Dialog` is closed" + "name": "renderHeader", + "type": "React.FunctionComponent>", + "description": "Provide a custom renderer for the dialog header. This content is rendered directly into the dialog body area, full bleed from edge to edge, top to the start of the body element. Warning: using a custom renderer may violate Primer UX principles." }, { - "name": "initialFocusRef", - "type": " React.RefObject", - "description": "Element inside of the `Dialog` you'd like to be focused when the Dialog is opened. If nothing is passed to `initialFocusRef` the close button is focused." + "name": "renderBody", + "type": "React.FunctionComponent>", + "description": "Provide a custom render function for the dialog body. This content is rendered directly into the dialog body area, full bleed from edge to edge, header to footer. Warning: using a custom renderer may violate Primer UX principles." }, { - "name": "aria-labelledby", - "type": "string", - "description": "Pass an id to use for the aria-label. Use either a `aria-label` or an `aria-labelledby` but not both." + "name": "renderFooter", + "type": "React.FunctionComponent>", + "description": "Provide a custom render function for the dialog footer. This content is rendered directly into the dialog footer area, full bleed from edge to edge, end of the body element to bottom. Warning: using a custom renderer may violate Primer UX principles." }, { - "name": "aria-label", - "type": "string", - "description": "Pass a label to be used to describe the Dialog. Use either a `aria-label` or an `aria-labelledby` but not both." + "name": "footerButtons", + "type": "DialogButtonProps[]", + "description": "Specifies the buttons to be rendered in the Dialog footer." }, { - "name": "sx", - "type": "SystemStyleObject" + "name": "onClose", + "type": "(gesture: 'close-button' | 'escape') => void", + "description": "This method is invoked when a gesture to close the dialog is used (either an Escape key press or clicking the 'X' in the top-right corner). The gesture argument indicates the gesture that was used to close the dialog (either 'close-button' or 'escape')." + }, + { + "name": "role", + "type": "'dialog' | 'alertdialog'", + "description": "The ARIA role to assign to this dialog." + }, + { + "name": "width", + "type": "'small' | 'medium' | 'large' | 'xlarge'" + }, + { + "name": "height", + "type": "'small' | 'large' | 'auto'" } ], - "subcomponents": [ - { - "name": "Dialog.Header", - "props": [ - { - "name": "sx", - "type": "SystemStyleObject" - } - ] - } - ] + "subcomponents": [] } diff --git a/src/Dialog/Dialog.stories.tsx b/src/Dialog/Dialog.stories.tsx deleted file mode 100644 index 434f203cf95..00000000000 --- a/src/Dialog/Dialog.stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, {useState} from 'react' -import {Meta} from '@storybook/react' - -import {BaseStyles, ThemeProvider} from '..' -import {Button} from '../Button' -import Dialog from './Dialog' - -export default { - title: 'Components/Dialog', - component: Dialog, - decorators: [ - Story => { - // Since portal roots are registered globally, we need this line so that each storybook - // story works in isolation. - return ( - - - - - - ) - }, - ], -} as Meta - -export const Default = () => { - const [isOpen, setIsOpen] = useState(false) - const returnFocusRef = React.useRef(null) - - return ( - <> - - setIsOpen(false)} - aria-labelledby="header-id" - > - Title - some content - - - ) -} diff --git a/src/Dialog/Dialog.tsx b/src/Dialog/Dialog.tsx index 6fc57e2c20f..be9fa186567 100644 --- a/src/Dialog/Dialog.tsx +++ b/src/Dialog/Dialog.tsx @@ -1,142 +1,454 @@ -import React, {forwardRef, useRef} from 'react' +import React, {useCallback, useEffect, useRef, useState} from 'react' import styled from 'styled-components' -import ButtonClose from '../deprecated/Button/ButtonClose' -import {get} from '../constants' +import Button, {ButtonPrimary, ButtonDanger, ButtonProps} from '../deprecated/Button' import Box from '../Box' -import useDialog from '../hooks/useDialog' +import {get} from '../constants' +import {useOnEscapePress, useProvidedRefOrCreate} from '../hooks' +import {useFocusTrap} from '../hooks/useFocusTrap' import sx, {SxProp} from '../sx' -import Text from '../Text' -import {ComponentProps} from '../utils/types' +import StyledOcticon from '../StyledOcticon' +import {XIcon} from '@primer/octicons-react' +import {useFocusZone} from '../hooks/useFocusZone' +import {FocusKeys} from '@primer/behaviors' +import Portal from '../Portal' import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useId} from '../hooks/useId' -const noop = () => null +const ANIMATION_DURATION = '200ms' -type StyledDialogBaseProps = { - narrow?: boolean - wide?: boolean -} & SxProp +/** + * Props that characterize a button to be rendered into the footer of + * a Dialog. + */ +export type DialogButtonProps = ButtonProps & { + /** + * The type of Button element to use + */ + buttonType?: 'normal' | 'primary' | 'danger' + + /** + * The Button's inner text + */ + content: React.ReactNode + + /** + * If true, and if this is the only button with autoFocus set to true, + * focus this button automatically when the dialog appears. + */ + autoFocus?: boolean + + /** + * A reference to the rendered Button’s DOM node, used together with + * `autoFocus` for `focusTrap`’s `initialFocus`. + */ + ref?: React.RefObject +} + +/** + * Props to customize the rendering of the Dialog. + */ +export interface DialogProps extends SxProp { + /** + * Title of the Dialog. Also serves as the aria-label for this Dialog. + */ + title?: React.ReactNode + + /** + * The Dialog's subtitle. Optional. Rendered below the title in smaller + * type with less contrast. Also serves as the aria-describedby for this + * Dialog. + */ + subtitle?: React.ReactNode + + /** + * Provide a custom renderer for the dialog header. This content is + * rendered directly into the dialog body area, full bleed from edge + * to edge, top to the start of the body element. + * + * Warning: using a custom renderer may violate Primer UX principles. + */ + renderHeader?: React.FunctionComponent> + + /** + * Provide a custom render function for the dialog body. This content is + * rendered directly into the dialog body area, full bleed from edge to + * edge, header to footer. + * + * Warning: using a custom renderer may violate Primer UX principles. + */ + renderBody?: React.FunctionComponent> + + /** + * Provide a custom render function for the dialog footer. This content is + * rendered directly into the dialog footer area, full bleed from edge to + * edge, end of the body element to bottom. + * + * Warning: using a custom renderer may violate Primer UX principles. + */ + renderFooter?: React.FunctionComponent> + + /** + * Specifies the buttons to be rendered in the Dialog footer. + */ + footerButtons?: DialogButtonProps[] + + /** + * This method is invoked when a gesture to close the dialog is used (either + * an Escape key press or clicking the "X" in the top-right corner). The + * gesture argument indicates the gesture that was used to close the dialog + * (either 'close-button' or 'escape'). + */ + onClose: (gesture: 'close-button' | 'escape') => void + + /** + * Default: "dialog". The ARIA role to assign to this dialog. + * @see https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal + * @see https://www.w3.org/TR/wai-aria-practices-1.1/#alertdialog + */ + role?: 'dialog' | 'alertdialog' + + /** + * The width of the dialog. + * small: 296px + * medium: 320px + * large: 480px + * xlarge: 640px + */ + width?: DialogWidth + + /** + * The height of the dialog. + * small: 296x480 + * large: 480x640 + * auto: variable based on contents + */ + height?: DialogHeight +} -const DialogBase = styled.div` - box-shadow: ${get('shadows.shadow.large')}; - border-radius: ${get('radii.2')}; +/** + * Props that are passed to a component that serves as a dialog header + */ +export interface DialogHeaderProps extends DialogProps { + /** + * ID of the element that will be used as the `aria-labelledby` attribute on the + * dialog. This ID should be set to the element that renders the dialog's title. + */ + dialogLabelId: string + + /** + * ID of the element that will be used as the `aria-describedby` attribute on the + * dialog. This ID should be set to the element that renders the dialog's subtitle. + */ + dialogDescriptionId: string +} + +const Backdrop = styled('div')` position: fixed; top: 0; - left: 50%; - transform: translateX(-50%); - max-height: 80vh; - z-index: 999; - margin: 10vh auto; - background-color: ${get('colors.canvas.default')}; - width: ${props => (props.narrow ? '320px' : props.wide ? '640px' : '440px')}; - outline: none; - - @media screen and (max-width: 750px) { - width: 100vw; - margin: 0; - border-radius: 0; - height: 100vh; - } + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.4); + animation: dialog-backdrop-appear ${ANIMATION_DURATION} ${get('animation.easeOutCubic')}; - ${sx}; + @keyframes dialog-backdrop-appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } ` -const DialogHeaderBase = styled(Box)` - border-radius: ${get('radii.2')} ${get('radii.2')} 0px 0px; - border-bottom: 1px solid ${get('colors.border.default')}; +const heightMap = { + small: '480px', + large: '640px', + auto: 'auto', +} as const + +const widthMap = { + small: '296px', + medium: '320px', + large: '480px', + xlarge: '640px', +} as const + +export type DialogWidth = keyof typeof widthMap +export type DialogHeight = keyof typeof heightMap + +type StyledDialogProps = { + width?: DialogWidth + height?: DialogHeight +} & SxProp + +const StyledDialog = styled.div` display: flex; + flex-direction: column; + background-color: ${get('colors.canvas.overlay')}; + box-shadow: ${get('shadows.overlay.shadow')}; + min-width: 296px; + max-width: calc(100vw - 64px); + max-height: calc(100vh - 64px); + width: ${props => widthMap[props.width ?? ('xlarge' as const)]}; + height: ${props => heightMap[props.height ?? ('auto' as const)]}; + border-radius: 12px; + opacity: 1; + animation: overlay--dialog-appear ${ANIMATION_DURATION} ${get('animation.easeOutCubic')}; - @media screen and (max-width: 750px) { - border-radius: 0px; + @keyframes overlay--dialog-appear { + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } } ${sx}; ` -export type DialogHeaderProps = ComponentProps - -function DialogHeader({theme, children, backgroundColor = 'canvas.subtle', ...rest}: DialogHeaderProps) { - if (React.Children.toArray(children).every(ch => typeof ch === 'string')) { - children = ( - - {children} - - ) - } +const DefaultHeader: React.FC> = ({ + dialogLabelId, + title, + subtitle, + dialogDescriptionId, + onClose, +}) => { + const onCloseClick = useCallback(() => { + onClose('close-button') + }, [onClose]) return ( - - {children} - + + + + {title ?? 'Dialog'} + {subtitle && {subtitle}} + + + + ) } +const DefaultBody: React.FC> = ({children}) => { + return {children} +} +const DefaultFooter: React.FC> = ({footerButtons}) => { + const {containerRef: footerRef} = useFocusZone({ + bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.Tab, + focusInStrategy: 'closest', + }) + return footerButtons ? ( + }> + + + ) : null +} -const Overlay = styled.span` - &:before { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: block; - cursor: default; - content: ' '; - background: transparent; - z-index: 99; - background: ${get('colors.primer.canvas.backdrop')}; +const _Dialog = React.forwardRef>((props, forwardedRef) => { + const { + title = 'Dialog', + subtitle = '', + renderHeader, + renderBody, + renderFooter, + onClose, + role = 'dialog', + width = 'xlarge', + height = 'auto', + footerButtons = [], + sx, + } = props + const dialogLabelId = useId() + const dialogDescriptionId = useId() + const autoFocusedFooterButtonRef = useRef(null) + for (const footerButton of footerButtons) { + if (footerButton.autoFocus) { + footerButton.ref = autoFocusedFooterButtonRef + } } + const defaultedProps = {...props, title, subtitle, role, dialogLabelId, dialogDescriptionId} + + const dialogRef = useRef(null) + useRefObjectAsForwardedRef(forwardedRef, dialogRef) + const backdropRef = useRef(null) + useFocusTrap({containerRef: dialogRef, restoreFocusOnCleanUp: true, initialFocusRef: autoFocusedFooterButtonRef}) + + useOnEscapePress( + (event: KeyboardEvent) => { + onClose('escape') + event.preventDefault() + }, + [onClose], + ) + + const header = (renderHeader ?? DefaultHeader)(defaultedProps) + const body = (renderBody ?? DefaultBody)(defaultedProps) + const footer = (renderFooter ?? DefaultFooter)(defaultedProps) + + return ( + <> + + + + {header} + {body} + {footer} + + + + + ) +}) +_Dialog.displayName = 'Dialog' + +const Header = styled.div` + box-shadow: 0 1px 0 ${get('colors.border.default')}; + padding: ${get('space.2')}; + z-index: 1; + flex-shrink: 0; ` -type InternalDialogProps = { - isOpen?: boolean - onDismiss?: () => void - initialFocusRef?: React.RefObject - returnFocusRef?: React.RefObject -} & ComponentProps - -const Dialog = forwardRef( - ({children, onDismiss = noop, isOpen, initialFocusRef, returnFocusRef, ...props}, forwardedRef) => { - const overlayRef = useRef(null) - const modalRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, modalRef) - const closeButtonRef = useRef(null) - - const onCloseClick = () => { - onDismiss() - if (returnFocusRef && returnFocusRef.current) { - returnFocusRef.current.focus() - } +const Title = styled.h1` + font-size: ${get('fontSizes.1')}; + font-weight: ${get('fontWeights.bold')}; + margin: 0; /* override default margin */ + ${sx}; +` + +const Subtitle = styled.h2` + font-size: ${get('fontSizes.0')}; + color: ${get('colors.fg.muted')}; + margin: 0; /* override default margin */ + margin-top: ${get('space.1')}; + + ${sx}; +` + +const Body = styled.div` + flex-grow: 1; + overflow: auto; + padding: ${get('space.3')}; + + ${sx}; +` + +const Footer = styled.div` + box-shadow: 0 -1px 0 ${get('colors.border.default')}; + padding: ${get('space.3')}; + display: flex; + flex-flow: wrap; + justify-content: flex-end; + z-index: 1; + flex-shrink: 0; + + button { + margin-left: ${get('space.1')}; + &:first-child { + margin-left: 0; } + } + + ${sx}; +` - const {getDialogProps} = useDialog({ - modalRef, - onDismiss: onCloseClick, - isOpen, - initialFocusRef, - closeButtonRef, - returnFocusRef, - overlayRef, - }) - return isOpen ? ( - <> - - - - {children} - - - ) : null - }, -) - -DialogHeader.propTypes = { - ...Box.propTypes, +const buttonTypes = { + normal: Button, + primary: ButtonPrimary, + danger: ButtonDanger, } +const Buttons: React.FC> = ({buttons}) => { + const autoFocusRef = useProvidedRefOrCreate(buttons.find(button => button.autoFocus)?.ref) + let autoFocusCount = 0 + const [hasRendered, setHasRendered] = useState(0) + useEffect(() => { + // hack to work around dialogs originating from other focus traps. + if (hasRendered === 1) { + autoFocusRef.current?.focus() + } else { + setHasRendered(hasRendered + 1) + } + }, [autoFocusRef, hasRendered]) -DialogHeader.displayName = 'Dialog.Header' -Dialog.displayName = 'Dialog' + return ( + <> + {buttons.map((dialogButtonProps, index) => { + const {content, buttonType = 'normal', autoFocus = false, ...buttonProps} = dialogButtonProps + const ButtonElement = buttonTypes[buttonType] + return ( + + {content} + + ) + })} + + ) +} +const DialogCloseButton = styled(Button)` + border-radius: 4px; + background: transparent; + border: 0; + vertical-align: middle; + color: ${get('colors.fg.muted')}; + padding: ${get('space.2')}; + align-self: flex-start; + line-height: normal; + box-shadow: none; +` +const CloseButton: React.FC void}>> = ({onClose}) => { + return ( + + + + ) +} -export type DialogProps = ComponentProps -export default Object.assign(Dialog, {Header: DialogHeader}) +/** + * A dialog is a type of overlay that can be used for confirming actions, asking + * for disambiguation, and presenting small forms. They generally allow the user + * to focus on a quick task without having to navigate to a different page. + * + * Dialogs appear in the page after a direct user interaction. Don't show dialogs + * on page load or as system alerts. + * + * Dialogs appear centered in the page, with a visible backdrop that dims the rest + * of the window for focus. + * + * All dialogs have a title and a close button. + * + * Dialogs are modal. Dialogs can be dismissed by clicking on the close button, + * pressing the escape key, or by interacting with another button in the dialog. + * To avoid losing information and missing important messages, clicking outside + * of the dialog will not close it. + * + * The sub components provided (e.g. Header, Title, etc.) are available for custom + * renderers only. They are not intended to be used otherwise. + */ +export const Dialog = Object.assign(_Dialog, { + Header, + Title, + Subtitle, + Body, + Footer, + Buttons, + CloseButton, +}) diff --git a/src/Dialog/index.ts b/src/Dialog/index.ts deleted file mode 100644 index c9e2bb007dd..00000000000 --- a/src/Dialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default, DialogProps, DialogHeaderProps} from './Dialog' diff --git a/src/Dialog2/Dialog.docs.json b/src/Dialog2/Dialog.docs.json deleted file mode 100644 index 5b803588341..00000000000 --- a/src/Dialog2/Dialog.docs.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "id": "drafts_dialog", - "name": "Dialog", - "status": "draft", - "a11yReviewed": false, - "stories": [], - "props": [ - { - "name": "title", - "type": "React.ReactNode", - "description": "Title of the Dialog. Also serves as the aria-label for this Dialog." - }, - { - "name": "subtitle", - "type": "React.ReactNode", - "description": "The Dialog's subtitle. Optional. Rendered below the title in smaller type with less contrast. Also serves as the aria-describedby for this Dialog." - }, - { - "name": "renderHeader", - "type": "React.FunctionComponent>", - "description": "Provide a custom renderer for the dialog header. This content is rendered directly into the dialog body area, full bleed from edge to edge, top to the start of the body element. Warning: using a custom renderer may violate Primer UX principles." - }, - { - "name": "renderBody", - "type": "React.FunctionComponent>", - "description": "Provide a custom render function for the dialog body. This content is rendered directly into the dialog body area, full bleed from edge to edge, header to footer. Warning: using a custom renderer may violate Primer UX principles." - }, - { - "name": "renderFooter", - "type": "React.FunctionComponent>", - "description": "Provide a custom render function for the dialog footer. This content is rendered directly into the dialog footer area, full bleed from edge to edge, end of the body element to bottom. Warning: using a custom renderer may violate Primer UX principles." - }, - { - "name": "footerButtons", - "type": "DialogButtonProps[]", - "description": "Specifies the buttons to be rendered in the Dialog footer." - }, - { - "name": "onClose", - "type": "(gesture: 'close-button' | 'escape') => void", - "description": "This method is invoked when a gesture to close the dialog is used (either an Escape key press or clicking the 'X' in the top-right corner). The gesture argument indicates the gesture that was used to close the dialog (either 'close-button' or 'escape')." - }, - { - "name": "role", - "type": "'dialog' | 'alertdialog'", - "description": "The ARIA role to assign to this dialog." - }, - { - "name": "width", - "type": "'small' | 'medium' | 'large' | 'xlarge'" - }, - { - "name": "height", - "type": "'small' | 'large' | 'auto'" - } - ], - "subcomponents": [] -} diff --git a/src/Dialog2/Dialog.stories.tsx b/src/Dialog2/Dialog.stories.tsx deleted file mode 100644 index b3a206f2e6e..00000000000 --- a/src/Dialog2/Dialog.stories.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {useState, useRef, useCallback} from 'react' -import {Meta} from '@storybook/react' - -import {BaseStyles, ThemeProvider} from '..' -import {Button} from '../Button' -import {Dialog} from './Dialog' - -export default { - title: 'Drafts/Components/Dialog', - component: Dialog, - decorators: [ - Story => { - // Since portal roots are registered globally, we need this line so that each storybook - // story works in isolation. - return ( - - - - - - ) - }, - ], -} as Meta - -export const Default = () => { - const [isOpen, setIsOpen] = useState(false) - const buttonRef = useRef(null) - const onDialogClose = useCallback(() => setIsOpen(false), []) - - return ( - <> - - {isOpen && Dialog Content} - - ) -} diff --git a/src/Dialog2/Dialog.tsx b/src/Dialog2/Dialog.tsx deleted file mode 100644 index be9fa186567..00000000000 --- a/src/Dialog2/Dialog.tsx +++ /dev/null @@ -1,454 +0,0 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' -import styled from 'styled-components' -import Button, {ButtonPrimary, ButtonDanger, ButtonProps} from '../deprecated/Button' -import Box from '../Box' -import {get} from '../constants' -import {useOnEscapePress, useProvidedRefOrCreate} from '../hooks' -import {useFocusTrap} from '../hooks/useFocusTrap' -import sx, {SxProp} from '../sx' -import StyledOcticon from '../StyledOcticon' -import {XIcon} from '@primer/octicons-react' -import {useFocusZone} from '../hooks/useFocusZone' -import {FocusKeys} from '@primer/behaviors' -import Portal from '../Portal' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' -import {useId} from '../hooks/useId' - -const ANIMATION_DURATION = '200ms' - -/** - * Props that characterize a button to be rendered into the footer of - * a Dialog. - */ -export type DialogButtonProps = ButtonProps & { - /** - * The type of Button element to use - */ - buttonType?: 'normal' | 'primary' | 'danger' - - /** - * The Button's inner text - */ - content: React.ReactNode - - /** - * If true, and if this is the only button with autoFocus set to true, - * focus this button automatically when the dialog appears. - */ - autoFocus?: boolean - - /** - * A reference to the rendered Button’s DOM node, used together with - * `autoFocus` for `focusTrap`’s `initialFocus`. - */ - ref?: React.RefObject -} - -/** - * Props to customize the rendering of the Dialog. - */ -export interface DialogProps extends SxProp { - /** - * Title of the Dialog. Also serves as the aria-label for this Dialog. - */ - title?: React.ReactNode - - /** - * The Dialog's subtitle. Optional. Rendered below the title in smaller - * type with less contrast. Also serves as the aria-describedby for this - * Dialog. - */ - subtitle?: React.ReactNode - - /** - * Provide a custom renderer for the dialog header. This content is - * rendered directly into the dialog body area, full bleed from edge - * to edge, top to the start of the body element. - * - * Warning: using a custom renderer may violate Primer UX principles. - */ - renderHeader?: React.FunctionComponent> - - /** - * Provide a custom render function for the dialog body. This content is - * rendered directly into the dialog body area, full bleed from edge to - * edge, header to footer. - * - * Warning: using a custom renderer may violate Primer UX principles. - */ - renderBody?: React.FunctionComponent> - - /** - * Provide a custom render function for the dialog footer. This content is - * rendered directly into the dialog footer area, full bleed from edge to - * edge, end of the body element to bottom. - * - * Warning: using a custom renderer may violate Primer UX principles. - */ - renderFooter?: React.FunctionComponent> - - /** - * Specifies the buttons to be rendered in the Dialog footer. - */ - footerButtons?: DialogButtonProps[] - - /** - * This method is invoked when a gesture to close the dialog is used (either - * an Escape key press or clicking the "X" in the top-right corner). The - * gesture argument indicates the gesture that was used to close the dialog - * (either 'close-button' or 'escape'). - */ - onClose: (gesture: 'close-button' | 'escape') => void - - /** - * Default: "dialog". The ARIA role to assign to this dialog. - * @see https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal - * @see https://www.w3.org/TR/wai-aria-practices-1.1/#alertdialog - */ - role?: 'dialog' | 'alertdialog' - - /** - * The width of the dialog. - * small: 296px - * medium: 320px - * large: 480px - * xlarge: 640px - */ - width?: DialogWidth - - /** - * The height of the dialog. - * small: 296x480 - * large: 480x640 - * auto: variable based on contents - */ - height?: DialogHeight -} - -/** - * Props that are passed to a component that serves as a dialog header - */ -export interface DialogHeaderProps extends DialogProps { - /** - * ID of the element that will be used as the `aria-labelledby` attribute on the - * dialog. This ID should be set to the element that renders the dialog's title. - */ - dialogLabelId: string - - /** - * ID of the element that will be used as the `aria-describedby` attribute on the - * dialog. This ID should be set to the element that renders the dialog's subtitle. - */ - dialogDescriptionId: string -} - -const Backdrop = styled('div')` - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - background-color: rgba(0, 0, 0, 0.4); - animation: dialog-backdrop-appear ${ANIMATION_DURATION} ${get('animation.easeOutCubic')}; - - @keyframes dialog-backdrop-appear { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } -` - -const heightMap = { - small: '480px', - large: '640px', - auto: 'auto', -} as const - -const widthMap = { - small: '296px', - medium: '320px', - large: '480px', - xlarge: '640px', -} as const - -export type DialogWidth = keyof typeof widthMap -export type DialogHeight = keyof typeof heightMap - -type StyledDialogProps = { - width?: DialogWidth - height?: DialogHeight -} & SxProp - -const StyledDialog = styled.div` - display: flex; - flex-direction: column; - background-color: ${get('colors.canvas.overlay')}; - box-shadow: ${get('shadows.overlay.shadow')}; - min-width: 296px; - max-width: calc(100vw - 64px); - max-height: calc(100vh - 64px); - width: ${props => widthMap[props.width ?? ('xlarge' as const)]}; - height: ${props => heightMap[props.height ?? ('auto' as const)]}; - border-radius: 12px; - opacity: 1; - animation: overlay--dialog-appear ${ANIMATION_DURATION} ${get('animation.easeOutCubic')}; - - @keyframes overlay--dialog-appear { - 0% { - opacity: 0; - transform: scale(0.5); - } - 100% { - opacity: 1; - transform: scale(1); - } - } - - ${sx}; -` - -const DefaultHeader: React.FC> = ({ - dialogLabelId, - title, - subtitle, - dialogDescriptionId, - onClose, -}) => { - const onCloseClick = useCallback(() => { - onClose('close-button') - }, [onClose]) - return ( - - - - {title ?? 'Dialog'} - {subtitle && {subtitle}} - - - - - ) -} -const DefaultBody: React.FC> = ({children}) => { - return {children} -} -const DefaultFooter: React.FC> = ({footerButtons}) => { - const {containerRef: footerRef} = useFocusZone({ - bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.Tab, - focusInStrategy: 'closest', - }) - return footerButtons ? ( - }> - - - ) : null -} - -const _Dialog = React.forwardRef>((props, forwardedRef) => { - const { - title = 'Dialog', - subtitle = '', - renderHeader, - renderBody, - renderFooter, - onClose, - role = 'dialog', - width = 'xlarge', - height = 'auto', - footerButtons = [], - sx, - } = props - const dialogLabelId = useId() - const dialogDescriptionId = useId() - const autoFocusedFooterButtonRef = useRef(null) - for (const footerButton of footerButtons) { - if (footerButton.autoFocus) { - footerButton.ref = autoFocusedFooterButtonRef - } - } - const defaultedProps = {...props, title, subtitle, role, dialogLabelId, dialogDescriptionId} - - const dialogRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, dialogRef) - const backdropRef = useRef(null) - useFocusTrap({containerRef: dialogRef, restoreFocusOnCleanUp: true, initialFocusRef: autoFocusedFooterButtonRef}) - - useOnEscapePress( - (event: KeyboardEvent) => { - onClose('escape') - event.preventDefault() - }, - [onClose], - ) - - const header = (renderHeader ?? DefaultHeader)(defaultedProps) - const body = (renderBody ?? DefaultBody)(defaultedProps) - const footer = (renderFooter ?? DefaultFooter)(defaultedProps) - - return ( - <> - - - - {header} - {body} - {footer} - - - - - ) -}) -_Dialog.displayName = 'Dialog' - -const Header = styled.div` - box-shadow: 0 1px 0 ${get('colors.border.default')}; - padding: ${get('space.2')}; - z-index: 1; - flex-shrink: 0; -` - -const Title = styled.h1` - font-size: ${get('fontSizes.1')}; - font-weight: ${get('fontWeights.bold')}; - margin: 0; /* override default margin */ - ${sx}; -` - -const Subtitle = styled.h2` - font-size: ${get('fontSizes.0')}; - color: ${get('colors.fg.muted')}; - margin: 0; /* override default margin */ - margin-top: ${get('space.1')}; - - ${sx}; -` - -const Body = styled.div` - flex-grow: 1; - overflow: auto; - padding: ${get('space.3')}; - - ${sx}; -` - -const Footer = styled.div` - box-shadow: 0 -1px 0 ${get('colors.border.default')}; - padding: ${get('space.3')}; - display: flex; - flex-flow: wrap; - justify-content: flex-end; - z-index: 1; - flex-shrink: 0; - - button { - margin-left: ${get('space.1')}; - &:first-child { - margin-left: 0; - } - } - - ${sx}; -` - -const buttonTypes = { - normal: Button, - primary: ButtonPrimary, - danger: ButtonDanger, -} -const Buttons: React.FC> = ({buttons}) => { - const autoFocusRef = useProvidedRefOrCreate(buttons.find(button => button.autoFocus)?.ref) - let autoFocusCount = 0 - const [hasRendered, setHasRendered] = useState(0) - useEffect(() => { - // hack to work around dialogs originating from other focus traps. - if (hasRendered === 1) { - autoFocusRef.current?.focus() - } else { - setHasRendered(hasRendered + 1) - } - }, [autoFocusRef, hasRendered]) - - return ( - <> - {buttons.map((dialogButtonProps, index) => { - const {content, buttonType = 'normal', autoFocus = false, ...buttonProps} = dialogButtonProps - const ButtonElement = buttonTypes[buttonType] - return ( - - {content} - - ) - })} - - ) -} -const DialogCloseButton = styled(Button)` - border-radius: 4px; - background: transparent; - border: 0; - vertical-align: middle; - color: ${get('colors.fg.muted')}; - padding: ${get('space.2')}; - align-self: flex-start; - line-height: normal; - box-shadow: none; -` -const CloseButton: React.FC void}>> = ({onClose}) => { - return ( - - - - ) -} - -/** - * A dialog is a type of overlay that can be used for confirming actions, asking - * for disambiguation, and presenting small forms. They generally allow the user - * to focus on a quick task without having to navigate to a different page. - * - * Dialogs appear in the page after a direct user interaction. Don't show dialogs - * on page load or as system alerts. - * - * Dialogs appear centered in the page, with a visible backdrop that dims the rest - * of the window for focus. - * - * All dialogs have a title and a close button. - * - * Dialogs are modal. Dialogs can be dismissed by clicking on the close button, - * pressing the escape key, or by interacting with another button in the dialog. - * To avoid losing information and missing important messages, clicking outside - * of the dialog will not close it. - * - * The sub components provided (e.g. Header, Title, etc.) are available for custom - * renderers only. They are not intended to be used otherwise. - */ -export const Dialog = Object.assign(_Dialog, { - Header, - Title, - Subtitle, - Body, - Footer, - Buttons, - CloseButton, -}) diff --git a/src/Dialog2/index.ts b/src/Dialog2/index.ts deleted file mode 100644 index 8390808a2a1..00000000000 --- a/src/Dialog2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Dialog' diff --git a/src/TreeView/TreeView.tsx b/src/TreeView/TreeView.tsx index 9ddca3cc74b..d70195cfa90 100644 --- a/src/TreeView/TreeView.tsx +++ b/src/TreeView/TreeView.tsx @@ -8,7 +8,7 @@ import classnames from 'classnames' import React from 'react' import styled, {keyframes} from 'styled-components' import {get} from '../constants' -import {ConfirmationDialog} from '../Dialog2/ConfirmationDialog' +import {ConfirmationDialog} from '../Dialog/ConfirmationDialog' import {useControllableState} from '../hooks/useControllableState' import {useId} from '../hooks/useId' import useSafeTimeout from '../hooks/useSafeTimeout' diff --git a/src/Dialog2/__tests__/ConfirmationDialog.test.tsx b/src/__tests__/ConfirmationDialog.test.tsx similarity index 90% rename from src/Dialog2/__tests__/ConfirmationDialog.test.tsx rename to src/__tests__/ConfirmationDialog.test.tsx index 82c9fb48055..aa11cc0a672 100644 --- a/src/Dialog2/__tests__/ConfirmationDialog.test.tsx +++ b/src/__tests__/ConfirmationDialog.test.tsx @@ -2,15 +2,15 @@ import {render as HTMLRender, act, fireEvent} from '@testing-library/react' import {axe} from 'jest-axe' import React, {useCallback, useRef, useState} from 'react' -import {ActionMenu} from '../../deprecated/ActionMenu' -import BaseStyles from '../../BaseStyles' -import Box from '../../Box' -import Button from '../../deprecated/Button/Button' -import {ConfirmationDialog, useConfirm} from '../ConfirmationDialog' -import theme from '../../theme' -import {ThemeProvider} from '../../ThemeProvider' -import {SSRProvider} from '../../utils/ssr' -import {behavesAsComponent, checkExports} from '../../utils/testing' +import {ActionMenu} from '../deprecated/ActionMenu' +import BaseStyles from '../BaseStyles' +import Box from '../Box' +import Button from '../deprecated/Button/Button' +import {ConfirmationDialog, useConfirm} from '../Dialog/ConfirmationDialog' +import theme from '../theme' +import {ThemeProvider} from '../ThemeProvider' +import {SSRProvider} from '../utils/ssr' +import {behavesAsComponent, checkExports} from '../utils/testing' declare const REACT_VERSION_LATEST: boolean @@ -80,7 +80,7 @@ describe('ConfirmationDialog', () => { options: {skipAs: true, skipSx: true}, }) - checkExports('Dialog2/ConfirmationDialog', { + checkExports('Dialog/ConfirmationDialog', { default: undefined, useConfirm, ConfirmationDialog, diff --git a/src/Dialog/__tests__/Dialog.test.tsx b/src/__tests__/Dialog.test.tsx similarity index 96% rename from src/Dialog/__tests__/Dialog.test.tsx rename to src/__tests__/Dialog.test.tsx index 8f4abcf194b..df5f8dc02a8 100644 --- a/src/Dialog/__tests__/Dialog.test.tsx +++ b/src/__tests__/Dialog.test.tsx @@ -1,9 +1,9 @@ import React, {useState, useRef} from 'react' -import {Dialog, Box, Text} from '../..' -import {Button} from '../../deprecated' +import {Dialog, Box, Text} from '..' +import {Button} from '../deprecated' import {render as HTMLRender, act, fireEvent} from '@testing-library/react' import {axe, toHaveNoViolations} from 'jest-axe' -import {behavesAsComponent, checkExports} from '../../utils/testing' +import {behavesAsComponent, checkExports} from '../utils/testing' expect.extend(toHaveNoViolations) const comp = ( diff --git a/src/Dialog/__tests__/Dialog.types.test.tsx b/src/__tests__/Dialog.types.test.tsx similarity index 89% rename from src/Dialog/__tests__/Dialog.types.test.tsx rename to src/__tests__/Dialog.types.test.tsx index d60ce984c57..de6b44c65ea 100644 --- a/src/Dialog/__tests__/Dialog.types.test.tsx +++ b/src/__tests__/Dialog.types.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import Dialog from '..' +import Dialog from '../Dialog' export function shouldAcceptCallWithNoProps() { return diff --git a/src/Dialog2/__tests__/Dialog.types.test.tsx b/src/__tests__/Dialog2.types.test.tsx similarity index 87% rename from src/Dialog2/__tests__/Dialog.types.test.tsx rename to src/__tests__/Dialog2.types.test.tsx index 591ccb74a05..a5774427458 100644 --- a/src/Dialog2/__tests__/Dialog.types.test.tsx +++ b/src/__tests__/Dialog2.types.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Dialog} from '../Dialog' +import {Dialog} from '../Dialog/Dialog' export function shouldAcceptCallWithNoProps() { return null} /> diff --git a/src/Dialog2/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap b/src/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap similarity index 100% rename from src/Dialog2/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap rename to src/__tests__/__snapshots__/ConfirmationDialog.test.tsx.snap diff --git a/src/Dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/src/__tests__/__snapshots__/Dialog.test.tsx.snap similarity index 100% rename from src/Dialog/__tests__/__snapshots__/Dialog.test.tsx.snap rename to src/__tests__/__snapshots__/Dialog.test.tsx.snap diff --git a/src/__tests__/storybook.test.tsx b/src/__tests__/storybook.test.tsx index ba610772e58..a19118036da 100644 --- a/src/__tests__/storybook.test.tsx +++ b/src/__tests__/storybook.test.tsx @@ -21,8 +21,6 @@ const allowlist = [ 'CounterLabel', 'DataTable', 'Details', - 'Dialog', - 'Dialog2', 'Flash', 'Heading', 'IconButton', diff --git a/src/drafts/index.ts b/src/drafts/index.ts index 7b901fafd90..95ae3d8a365 100644 --- a/src/drafts/index.ts +++ b/src/drafts/index.ts @@ -21,7 +21,7 @@ export type { TableActionsProps, } from '../DataTable' -export * from '../Dialog2' +export * from '../Dialog/Dialog' export {default as InlineAutocomplete} from './InlineAutocomplete' export type { diff --git a/src/index.ts b/src/index.ts index 7b81df893d1..e6b83b39a2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,7 @@ export type {TouchOrMouseEvent} from './hooks/useOnOutsideClick' export {useOpenAndCloseFocus} from './hooks/useOpenAndCloseFocus' export {useOnEscapePress} from './hooks/useOnEscapePress' export {useOverlay} from './hooks/useOverlay' -export {useConfirm} from './Dialog2/ConfirmationDialog' +export {useConfirm} from './Dialog/ConfirmationDialog' export {useFocusTrap} from './hooks/useFocusTrap' export type {FocusTrapHookSettings} from './hooks/useFocusTrap' export {useFocusZone} from './hooks/useFocusZone' @@ -87,8 +87,8 @@ export {default as Details} from './Details' export type {DetailsProps} from './Details' export {default as Dialog} from './Dialog' export type {DialogProps, DialogHeaderProps} from './Dialog' -export type {ConfirmationDialogProps} from './Dialog2/ConfirmationDialog' -export {ConfirmationDialog} from './Dialog2/ConfirmationDialog' +export type {ConfirmationDialogProps} from './Dialog/ConfirmationDialog' +export {ConfirmationDialog} from './Dialog/ConfirmationDialog' export {default as FilteredSearch} from './FilteredSearch' export type {FilteredSearchProps} from './FilteredSearch' export {default as FilterList} from './FilterList' diff --git a/src/stories/ConfirmationDialog.stories.tsx b/src/stories/ConfirmationDialog.stories.tsx new file mode 100644 index 00000000000..6f5668a4b32 --- /dev/null +++ b/src/stories/ConfirmationDialog.stories.tsx @@ -0,0 +1,106 @@ +import React, {useState, useRef, useCallback} from 'react' +import {Meta} from '@storybook/react' +import {BaseStyles, Box, ThemeProvider, useTheme} from '..' +import {Button} from '../Button' +import {ActionMenu} from '../ActionMenu' +import {ActionList} from '../ActionList' +import {ConfirmationDialog, useConfirm} from '../Dialog/ConfirmationDialog' + +export default { + title: 'Components/ConfirmationDialog', + component: ConfirmationDialog, + decorators: [ + Story => { + // Since portal roots are registered globally, we need this line so that each storybook + // story works in isolation. + return ( + + + + + + ) + }, + ], +} as Meta + +export const BasicConfirmationDialog = () => { + const [isOpen, setIsOpen] = useState(false) + const buttonRef = useRef(null) + const onDialogClose = useCallback(() => setIsOpen(false), []) + return ( + <> + + {isOpen && ( + + Deleting the universe could have disastrous effects, including but not limited to destroying all life on + Earth. + + )} + + ) +} + +export const ShorthandHook = () => { + const confirm = useConfirm() + const {theme} = useTheme() + const onButtonClick = useCallback( + async (event: React.MouseEvent) => { + if ( + (await confirm({title: 'Are you sure?', content: 'Do you really want to turn this button green?'})) && + event.target instanceof HTMLElement + ) { + event.target.style.color = theme?.colors.success.fg ?? 'green' + event.target.textContent = "I'm green!" + } + }, + [confirm, theme], + ) + return ( + + + + + + + ) +} + +export const ShorthandHookFromActionMenu = () => { + const confirm = useConfirm() + const [text, setText] = useState('open me') + const onButtonClick = useCallback(async () => { + if (await confirm({title: 'Are you sure?', content: 'Do you really want to do a trick?'})) { + setText('tada!') + } + }, [confirm]) + + return ( + + + {text} + + + + Do a trick! + + + + + ) +} diff --git a/src/Dialog2/Dialog.features.stories.tsx b/src/stories/Dialog.stories.tsx similarity index 71% rename from src/Dialog2/Dialog.features.stories.tsx rename to src/stories/Dialog.stories.tsx index 6af68cdc7ab..9dab6838f5a 100644 --- a/src/Dialog2/Dialog.features.stories.tsx +++ b/src/stories/Dialog.stories.tsx @@ -1,15 +1,12 @@ import React, {useState, useRef, useCallback} from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, ThemeProvider, Box, useTheme} from '..' -import {Button} from '../Button' -import {ActionMenu} from '../ActionMenu' -import {ActionList} from '../ActionList' -import {ConfirmationDialog, useConfirm} from './ConfirmationDialog' -import {Dialog, DialogProps, DialogWidth, DialogHeight} from './Dialog' +import {BaseStyles, ThemeProvider, Box} from '..' +import {Button} from '../Button' +import {Dialog, DialogProps, DialogWidth, DialogHeight} from '../Dialog/Dialog' export default { - title: 'Drafts/Components/Dialog/Features', + title: 'Components/Dialog', component: Dialog, decorators: [ Story => { @@ -108,43 +105,6 @@ interface DialogStoryProps { height: DialogHeight subtitle: boolean } - -function CustomHeader({ - title, - subtitle, - dialogLabelId, - dialogDescriptionId, - onClose, -}: React.PropsWithChildren) { - const onCloseClick = useCallback(() => { - onClose('close-button') - }, [onClose]) - if (typeof title === 'string' && typeof subtitle === 'string') { - return ( - - - -

{title}

- {subtitle &&

{subtitle}

} -
- -
-
- ) - } - return null -} -function CustomBody({children}: React.PropsWithChildren) { - return {children} -} -function CustomFooter({footerButtons}: React.PropsWithChildren) { - return ( - - {footerButtons ? : null} - - ) -} - export const BasicDialog = ({width, height, subtitle}: DialogStoryProps) => { const [isOpen, setIsOpen] = useState(false) const [secondOpen, setSecondOpen] = useState(false) @@ -180,7 +140,39 @@ export const BasicDialog = ({width, height, subtitle}: DialogStoryProps) => { ) } -export const DialogWithCustomRenderers = ({width, height, subtitle}: DialogStoryProps) => { + +function CustomHeader({ + title, + subtitle, + dialogLabelId, + dialogDescriptionId, + onClose, +}: React.PropsWithChildren) { + const onCloseClick = useCallback(() => { + onClose('close-button') + }, [onClose]) + if (typeof title === 'string' && typeof subtitle === 'string') { + return ( + +

{title.toUpperCase()}

+

{subtitle.toLowerCase()}

+ +
+ ) + } + return null +} +function CustomBody({children}: React.PropsWithChildren) { + return {children} +} +function CustomFooter({footerButtons}: React.PropsWithChildren) { + return ( + + {footerButtons ? : null} + + ) +} +export const WithCustomRenderers = ({width, height, subtitle}: DialogStoryProps) => { const [isOpen, setIsOpen] = useState(false) const onDialogClose = useCallback(() => setIsOpen(false), []) return ( @@ -208,7 +200,7 @@ export const DialogWithCustomRenderers = ({width, height, subtitle}: DialogStory ) } -export const DialogWithStressTest = ({width, height, subtitle}: DialogStoryProps) => { +export const StressTest = ({width, height, subtitle}: DialogStoryProps) => { const [isOpen, setIsOpen] = useState(false) const [secondOpen, setSecondOpen] = useState(false) const buttonRef = useRef(null) @@ -249,84 +241,3 @@ export const DialogWithStressTest = ({width, height, subtitle}: DialogStoryProps ) } - -export const BasicConfirmationDialog = () => { - const [isOpen, setIsOpen] = useState(false) - const buttonRef = useRef(null) - const onDialogClose = useCallback(() => setIsOpen(false), []) - return ( - <> - - {isOpen && ( - - Deleting the universe could have disastrous effects, including but not limited to destroying all life on - Earth. - - )} - - ) -} - -export const ConfirmationDialogWithShorthandHook = () => { - const confirm = useConfirm() - const {theme} = useTheme() - const onButtonClick = useCallback( - async (event: React.MouseEvent) => { - if ( - (await confirm({title: 'Are you sure?', content: 'Do you really want to turn this button green?'})) && - event.target instanceof HTMLElement - ) { - event.target.style.color = theme?.colors.success.fg ?? 'green' - event.target.textContent = "I'm green!" - } - }, - [confirm, theme], - ) - return ( - - - - - - - ) -} - -export const ConfirmationDialogWithShorthandHookFromActionMenu = () => { - const confirm = useConfirm() - const [text, setText] = useState('open me') - const onButtonClick = useCallback(async () => { - if (await confirm({title: 'Are you sure?', content: 'Do you really want to do a trick?'})) { - setText('tada!') - } - }, [confirm]) - - return ( - - - {text} - - - - Do a trick! - - - - - ) -}