diff --git a/.changeset/lazy-jobs-pump.md b/.changeset/lazy-jobs-pump.md new file mode 100644 index 00000000000..d57adc23a22 --- /dev/null +++ b/.changeset/lazy-jobs-pump.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Add `loading` state to `Button` and `IconButton` diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-colorblind-linux.png index 879ddfa9027..48f797324ab 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-dimmed-linux.png index bc0be329541..e977aff9f3b 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png index aa26a849c19..c7409f43fd2 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-linux.png index 879ddfa9027..48f797324ab 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-tritanopia-linux.png index 879ddfa9027..48f797324ab 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-colorblind-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-colorblind-linux.png index 425a179a4db..3f5d9e40101 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-high-contrast-linux.png index 701417e7166..80b5cc2723a 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-linux.png index 5af933d70ac..35b40903002 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-tritanopia-linux.png index 425a179a4db..3f5d9e40101 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-colorblind-linux.png index ab37fe7cc56..805e920af9b 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-dimmed-linux.png index 3cfe44bdc0b..2e5881d93cb 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-high-contrast-linux.png index d0dde255eaf..8e369b17b17 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-linux.png index ab37fe7cc56..805e920af9b 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-tritanopia-linux.png index ab37fe7cc56..805e920af9b 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-colorblind-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-colorblind-linux.png index a28c745393a..8c4647526ac 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-high-contrast-linux.png index 76112b5689a..a12e8f611a0 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-linux.png index 927610bd04f..659cd64c846 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-tritanopia-linux.png index a28c745393a..8c4647526ac 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Single-Select-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-colorblind-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-dimmed-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-dimmed-linux.png new file mode 100644 index 00000000000..f95602f4191 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-high-contrast-linux.png new file mode 100644 index 00000000000..9599511d3f6 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-tritanopia-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-colorblind-linux.png new file mode 100644 index 00000000000..c5971cbb5f1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-linux.png new file mode 100644 index 00000000000..915404b77a1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-tritanopia-linux.png new file mode 100644 index 00000000000..c5971cbb5f1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-Custom-Announcement-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-colorblind-linux.png new file mode 100644 index 00000000000..edf5ad510c9 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-dimmed-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-dimmed-linux.png new file mode 100644 index 00000000000..333817f5f9a Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-high-contrast-linux.png new file mode 100644 index 00000000000..ab03362b29e Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-linux.png new file mode 100644 index 00000000000..c100db093f3 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-tritanopia-linux.png new file mode 100644 index 00000000000..edf5ad510c9 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-colorblind-linux.png new file mode 100644 index 00000000000..52d68822575 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-linux.png new file mode 100644 index 00000000000..f72a495ec0e Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-tritanopia-linux.png new file mode 100644 index 00000000000..52d68822575 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Leading-Visual-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-colorblind-linux.png new file mode 100644 index 00000000000..a9d2801000c Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-dimmed-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-dimmed-linux.png new file mode 100644 index 00000000000..ba56fd1422f Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-high-contrast-linux.png new file mode 100644 index 00000000000..b7203f08d54 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-linux.png new file mode 100644 index 00000000000..f62eba040a0 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-tritanopia-linux.png new file mode 100644 index 00000000000..a9d2801000c Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-colorblind-linux.png new file mode 100644 index 00000000000..76c390511a2 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-linux.png new file mode 100644 index 00000000000..a0a7ddaa426 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-tritanopia-linux.png new file mode 100644 index 00000000000..76c390511a2 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-With-Trailing-Visual-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-colorblind-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-dimmed-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-dimmed-linux.png new file mode 100644 index 00000000000..f95602f4191 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-high-contrast-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-high-contrast-linux.png new file mode 100644 index 00000000000..9599511d3f6 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-tritanopia-linux.png new file mode 100644 index 00000000000..2bb594dea00 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-colorblind-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-colorblind-linux.png new file mode 100644 index 00000000000..c5971cbb5f1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-linux.png new file mode 100644 index 00000000000..915404b77a1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-linux.png differ diff --git a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-tritanopia-linux.png b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-tritanopia-linux.png new file mode 100644 index 00000000000..c5971cbb5f1 Binary files /dev/null and b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-Loading-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-colorblind-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-colorblind-linux.png index 71c6f79a646..b26dd477d5c 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-colorblind-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-dimmed-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-dimmed-linux.png index f8a71e52668..91a136d81bc 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-dimmed-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-high-contrast-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-high-contrast-linux.png index 34ace65d6db..5124ba0088e 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-high-contrast-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-linux.png index 3d6431e4eee..3801959b732 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-tritanopia-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-tritanopia-linux.png index 9a7ee276952..1bf1e6a9f45 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-tritanopia-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-colorblind-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-colorblind-linux.png index 6a02bd322e8..87824094f91 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-colorblind-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-high-contrast-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-high-contrast-linux.png index 5e2c2ae3695..65e0a0cc92b 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-high-contrast-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-linux.png index 4dcce520606..6ba5cbfa3c2 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-tritanopia-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-tritanopia-linux.png index 9529d66fdeb..0ae3c80fd1c 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-tritanopia-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Default-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-colorblind-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-colorblind-linux.png index f79c9c5872a..a86d583a2f5 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-colorblind-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-dimmed-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-dimmed-linux.png index 6260eb2d553..65331f6e703 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-dimmed-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-high-contrast-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-high-contrast-linux.png index 87d3cfeff15..9b96689af4d 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-high-contrast-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-linux.png index 077cb9ee5fb..057cd8ad277 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-tritanopia-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-tritanopia-linux.png index 4663e46ac7d..3aaf206b28e 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-tritanopia-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-colorblind-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-colorblind-linux.png index 51a9ee57deb..d13fa0b6687 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-colorblind-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-high-contrast-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-high-contrast-linux.png index 9537583c804..67768df956b 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-high-contrast-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-linux.png index 3b8af491224..dcd0ebc512e 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-linux.png differ diff --git a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-tritanopia-linux.png b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-tritanopia-linux.png index 72397be86a9..b62ae78af5f 100644 Binary files a/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-tritanopia-linux.png and b/.playwright/snapshots/components/Dialog.test.ts-snapshots/Dialog-Stress-Test-light-tritanopia-linux.png differ diff --git a/e2e/components/Button.test.ts b/e2e/components/Button.test.ts index cc8eee590ef..dec01cb8726 100644 --- a/e2e/components/Button.test.ts +++ b/e2e/components/Button.test.ts @@ -479,6 +479,148 @@ test.describe('Button', () => { } }) + test.describe('Loading', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`Button.Loading.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) + + test.describe('Loading Custom Announcement', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-custom-announcement', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `Button.Loading Custom Announcement.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-custom-announcement', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) + + test.describe('Loading With Leading Visual', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-with-leading-visual', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `Button.Loading With Leading Visual.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-with-leading-visual', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) + + test.describe('Loading With Trailing Visual', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-with-trailing-visual', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot( + `Button.Loading With Trailing Visual.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-button-features--loading-with-trailing-visual', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) + test.describe('Dev: Invisible Variants', () => { for (const theme of themes) { test.describe(theme, () => { diff --git a/package-lock.json b/package-lock.json index 974f0105db8..44498363551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,6 @@ { "name": "primer", + "version": "36.4.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/packages/react/src/Button/Button.docs.json b/packages/react/src/Button/Button.docs.json index aa88b20fb32..01da829510f 100644 --- a/packages/react/src/Button/Button.docs.json +++ b/packages/react/src/Button/Button.docs.json @@ -19,15 +19,9 @@ "description": "For counter buttons, the number to display." }, { - "name": "variant", - "type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'", - "defaultValue": "'default'", - "description": "Change the visual style of the button." - }, - { - "name": "size", - "type": "'small'\n| 'medium'\n| 'large'", - "defaultValue": "'medium'" + "name": "inactive", + "type": "boolean", + "description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence." }, { "name": "leadingIcon", @@ -40,6 +34,22 @@ "type": "React.ElementType", "description": "A visual to display before the button text." }, + { + "name": "loading", + "type": "boolean", + "description": "When true, the button is in a loading state." + }, + { + "name": "loadingAnnouncement", + "type": "string", + "description": "The content to announce to screen readers when loading. This requires `loading` prop to be true" + }, + + { + "name": "size", + "type": "'small'\n| 'medium'\n| 'large'", + "defaultValue": "'medium'" + }, { "name": "trailingIcon", "type": "React.ComponentType", @@ -52,9 +62,10 @@ "description": "A visual to display after the button text." }, { - "name": "inactive", - "type": "boolean", - "description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence." + "name": "variant", + "type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'", + "defaultValue": "'default'", + "description": "Change the visual style of the button." }, { "name": "as", diff --git a/packages/react/src/Button/Button.examples.stories.tsx b/packages/react/src/Button/Button.examples.stories.tsx new file mode 100644 index 00000000000..b2da8e29079 --- /dev/null +++ b/packages/react/src/Button/Button.examples.stories.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import {Button} from '.' +import {DownloadIcon} from '@primer/octicons-react' +import {VisuallyHidden} from '../internal/components/VisuallyHidden' + +const meta: Meta = { + title: 'Components/Button/Examples', +} as Meta + +export default meta + +export const LoadingStatusAnnouncementSuccessful = () => { + const [loading, setLoading] = React.useState(false) + const [success, setSuccess] = React.useState(false) + + const resolveAction = async () => { + setLoading(true) + await new Promise(resolve => setTimeout(resolve, 1500)) + setLoading(false) + + return await true + } + + const onClick = (resolveType: 'error' | 'success') => async () => { + const actionResult = await resolveAction() + + if (resolveType === 'error') { + setSuccess(!actionResult) + return + } + + setSuccess(actionResult) + } + + return ( + <> + {!loading && success ? 'Export completed' : null} + + + ) +} + +export const LoadingStatusAnnouncementError = () => { + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState(false) + + const resolveAction = async () => { + setLoading(true) + await new Promise(resolve => setTimeout(resolve, 1500)) + setLoading(false) + + return await true + } + + const onClick = (resolveType: 'error' | 'success') => async () => { + const actionResult = await resolveAction() + + if (resolveType === 'error') { + setError(actionResult) + return + } + + setError(!actionResult) + } + + return ( + <> + {!loading && error ? 'Export failed' : null} + + + + ) +} diff --git a/packages/react/src/Button/Button.features.stories.tsx b/packages/react/src/Button/Button.features.stories.tsx index 313b249c093..1caf453d217 100644 --- a/packages/react/src/Button/Button.features.stories.tsx +++ b/packages/react/src/Button/Button.features.stories.tsx @@ -1,4 +1,4 @@ -import {EyeIcon, TriangleDownIcon, HeartIcon} from '@primer/octicons-react' +import {EyeIcon, TriangleDownIcon, HeartIcon, DownloadIcon} from '@primer/octicons-react' import React, {useState} from 'react' import {Button} from '.' @@ -96,3 +96,37 @@ export const Small = () => export const Medium = () => export const Large = () => + +export const Loading = () => + +export const LoadingCustomAnnouncement = () => ( + +) + +export const LoadingWithLeadingVisual = () => ( + +) + +export const LoadingWithTrailingVisual = () => ( + +) + +export const LoadingTrigger = () => { + const [isLoading, setIsLoading] = useState(false) + + const handleClick = () => { + setIsLoading(true) + } + + return ( + + ) +} diff --git a/packages/react/src/Button/Button.stories.tsx b/packages/react/src/Button/Button.stories.tsx index 793d5884195..5b410d8efe4 100644 --- a/packages/react/src/Button/Button.stories.tsx +++ b/packages/react/src/Button/Button.stories.tsx @@ -48,6 +48,16 @@ Playground.argTypes = { type: 'boolean', }, }, + loading: { + control: { + type: 'boolean', + }, + }, + count: { + control: { + type: 'number', + }, + }, leadingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), trailingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), trailingAction: OcticonArgType([TriangleDownIcon]), @@ -59,6 +69,7 @@ Playground.args = { inactive: false, variant: 'default', alignContent: 'center', + loading: false, trailingVisual: null, leadingVisual: null, trailingAction: null, diff --git a/packages/react/src/Button/ButtonBase.tsx b/packages/react/src/Button/ButtonBase.tsx index 746a26dfe0b..8458ecd8ad5 100644 --- a/packages/react/src/Button/ButtonBase.tsx +++ b/packages/react/src/Button/ButtonBase.tsx @@ -10,7 +10,23 @@ import {StyledButton} from './types' import {getVariantStyles, getButtonStyles, getAlignContentSize} from './styles' import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' import {defaultSxProp} from '../utils/defaultSxProp' +import {VisuallyHidden} from '../internal/components/VisuallyHidden' +import Spinner from '../Spinner' import CounterLabel from '../CounterLabel' +import {useId} from '../hooks' +import {ConditionalWrapper} from '../internal/components/ConditionalWrapper' +import {Status} from '../internal/components/Status' + +const iconWrapStyles = { + display: 'flex', + pointerEvents: 'none', +} + +const renderVisual = (Visual: React.ElementType, loading: boolean, visualName: string) => ( + + {loading ? : } + +) const ButtonBase = forwardRef( ({children, as: Component = 'button', sx: sxProp = defaultSxProp, ...props}, forwardedRef): JSX.Element => { @@ -18,13 +34,19 @@ const ButtonBase = forwardRef( leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, trailingAction: TrailingAction, + ['aria-describedby']: ariaDescribedBy, + ['aria-labelledby']: ariaLabelledBy, count, icon: Icon, + id, variant = 'default', size = 'medium', alignContent = 'center', block = false, + loading = false, + loadingAnnouncement = 'Loading', inactive, + onClick, ...rest } = props @@ -38,10 +60,9 @@ const ButtonBase = forwardRef( const sxStyles = useMemo(() => { return merge(baseStyles, sxProp) }, [baseStyles, sxProp]) - const iconWrapStyles = { - display: 'flex', - pointerEvents: 'none', - } + const uuid = useId(id) + const loadingAnnouncementID = `${uuid}-loading-announcement` + const buttonLabelID = ariaLabelledBy || `${uuid}-label` if (__DEV__) { /** @@ -64,45 +85,63 @@ const ButtonBase = forwardRef( } return ( - - {Icon ? ( - - ) : ( - <> - - {LeadingVisual && ( - - + + Boolean(descriptionID)) + .join(' ')} + // aria-labelledby is needed because the accessible name becomes unset when the button is in a loading state + aria-labelledby={buttonLabelID} + id={id} + onClick={loading ? undefined : onClick} + > + {Icon ? ( + loading ? ( + + ) : ( + + ) + ) : ( + <> + + {loading && !LeadingVisual && !TrailingVisual && renderVisual(Spinner, loading, 'loadingSpinner')} + {LeadingVisual && renderVisual(LeadingVisual, loading, 'leadingVisual')} + {children && ( + + {children} + {count !== undefined && !TrailingVisual && ( + + {count} + + )} + + )} + {TrailingVisual && renderVisual(TrailingVisual, loading && !LeadingVisual, 'trailingVisual')} + + {TrailingAction && ( + + )} - {children && {children}} - {count !== undefined && !TrailingVisual ? ( - - {count} - - ) : TrailingVisual ? ( - - - - ) : null} - - {TrailingAction && ( - - - - )} - + + )} + + {loading && ( + + {loadingAnnouncement} + )} - + ) }, ) as PolymorphicForwardRefComponent<'button' | 'a', ButtonProps> diff --git a/packages/react/src/Button/IconButton.features.stories.tsx b/packages/react/src/Button/IconButton.features.stories.tsx index 0f27d91c397..1a892635479 100644 --- a/packages/react/src/Button/IconButton.features.stories.tsx +++ b/packages/react/src/Button/IconButton.features.stories.tsx @@ -1,5 +1,5 @@ -import {HeartIcon} from '@primer/octicons-react' -import React from 'react' +import {HeartIcon, DownloadIcon} from '@primer/octicons-react' +import React, {useState} from 'react' import {IconButton} from '.' export default { @@ -18,4 +18,19 @@ export const Small = () => -export const Large = () => +export const Large = () => + +export const Loading = () => + +export const LoadingTrigger = () => { + const [isLoading, setIsLoading] = useState(false) + + const handleClick = () => { + setIsLoading(true) + setTimeout(() => { + setIsLoading(false) + }, 3000) + } + + return +} diff --git a/packages/react/src/Button/LinkButton.features.stories.tsx b/packages/react/src/Button/LinkButton.features.stories.tsx index 025437e0509..97c2c69fcac 100644 --- a/packages/react/src/Button/LinkButton.features.stories.tsx +++ b/packages/react/src/Button/LinkButton.features.stories.tsx @@ -1,4 +1,4 @@ -import {EyeIcon, ChevronRightIcon, HeartIcon} from '@primer/octicons-react' +import {EyeIcon, ChevronRightIcon, HeartIcon, DownloadIcon} from '@primer/octicons-react' import React, {forwardRef} from 'react' import {Button} from '.' @@ -82,3 +82,23 @@ export const WithReactRouter = () => ( Default ) + +export const Loading = () => + +export const LoadingCustomAnnouncement = () => ( + +) + +export const LoadingWithLeadingVisual = () => ( + +) + +export const LoadingWithTrailingVisual = () => ( + +) diff --git a/packages/react/src/Button/LinkButton.stories.tsx b/packages/react/src/Button/LinkButton.stories.tsx index 2ef1d371c5a..0a4c6828a3d 100644 --- a/packages/react/src/Button/LinkButton.stories.tsx +++ b/packages/react/src/Button/LinkButton.stories.tsx @@ -41,6 +41,11 @@ Playground.argTypes = { trailingIcon: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]), trailingAction: OcticonArgType([ChevronRightIcon]), href: {control: 'text'}, + loading: { + control: { + type: 'boolean', + }, + }, } Playground.args = { block: false, @@ -50,6 +55,7 @@ Playground.args = { trailingIcon: null, leadingIcon: null, href: '/', + loading: false, } export const Default = () => ( diff --git a/packages/react/src/Button/__tests__/Button.test.tsx b/packages/react/src/Button/__tests__/Button.test.tsx index 186f4a029a7..97a29b04c90 100644 --- a/packages/react/src/Button/__tests__/Button.test.tsx +++ b/packages/react/src/Button/__tests__/Button.test.tsx @@ -3,13 +3,35 @@ import {render, screen, fireEvent} from '@testing-library/react' import {axe} from 'jest-axe' import React from 'react' import {IconButton, Button} from '../../Button' +import type {ButtonProps} from '../../Button' import {behavesAsComponent} from '../../utils/testing' +type StatefulLoadingButtonProps = { + children?: React.ReactNode + id?: string + ['aria-describedby']?: string + loadingAnnouncement?: string +} + +const TestButton = (props: ButtonProps) => ) + const container = render() const button = container.getByRole('button') expect(button.textContent).toEqual('Default') }) @@ -29,7 +51,11 @@ describe('Button', () => { }) it('respects block prop', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) @@ -48,31 +74,51 @@ describe('Button', () => { }) it('respects the small size prop', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) it('respects the large size prop', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) it('styles primary button appropriately', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) it('styles invisible button appropriately', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) it('styles danger button appropriately', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) @@ -84,7 +130,11 @@ describe('Button', () => { }) it('respects the alignContent prop', () => { - const container = render() + const container = render( + , + ) const button = container.getByRole('button') expect(button).toMatchSnapshot() }) @@ -113,4 +163,90 @@ describe('Button', () => { const position = screen.getByText('content').compareDocumentPosition(screen.getByTestId('trailingVisual')) expect(position).toBe(Node.DOCUMENT_POSITION_FOLLOWING) }) + + it('should describe the button with a default loading announcement, and only when the button is in a loading state', () => { + const buttonId = 'loading-button' + const container = render( + + content + , + ) + const buttonNode = container.getByRole('button') + + expect(buttonNode.getAttribute('aria-describedby')).toBe(`${buttonId}-loading-announcement`) + + expect(buttonNode).not.toHaveAccessibleDescription('Loading') + + fireEvent.click(buttonNode) + + // not sure why, but we need to wait a tick for the the loading state to actually be set + setTimeout(() => { + expect(buttonNode).toHaveAccessibleDescription('Loading') + }, 0) + }) + + it('should render a custom loading announcement, and only when the button is in a loading state', () => { + const buttonId = 'loading-button' + const container = render( + + content + , + ) + const buttonNode = container.getByRole('button') + + expect(buttonNode.getAttribute('aria-describedby')).toBe(`${buttonId}-loading-announcement`) + + expect(buttonNode).not.toHaveAccessibleDescription('Action loading') + + fireEvent.click(buttonNode) + + // not sure why, but we need to wait a tick for the the loading state to actually be set + setTimeout(() => { + expect(buttonNode).toHaveAccessibleDescription('Action loading') + }, 0) + }) + + it('should be described by loading announcement AND whatever is passed to aria-describedby', () => { + const buttonDescriptionId = 'button-description' + const buttonId = 'loading-button' + const container = render( + + content + , + ) + const buttonDescribedBy = container.getByRole('button').getAttribute('aria-describedby') + const loadingAnnouncementId = `${buttonId}-loading-announcement` + + expect(buttonDescribedBy).toContain(loadingAnnouncementId) + + expect(buttonDescribedBy).toContain(buttonDescriptionId) + }) + + it('should only set aria-disabled to "true" when the button is in a loading state', () => { + const container = render( + + content + , + ) + const buttonNode = container.getByRole('button') + + expect(buttonNode.getAttribute('aria-disabled')).not.toBe('true') + + // not sure why, but we need to wait a tick for the the loading state to actually be set + setTimeout(() => { + expect(buttonNode.getAttribute('aria-disabled')).toBe('true') + }, 0) + }) + + it('allows the consumer to override `aria-disabled`', () => { + const container = render() + + expect(container.getByRole('button')).toHaveAttribute('aria-disabled', 'true') + }) + + it('should preserve the accessible button name when the button is in a loading state', () => { + const container = render() + + expect(container.getByRole('button')).toHaveAccessibleName('content') + }) }) diff --git a/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap b/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap index bdb67a2bd0e..d01040717b0 100644 --- a/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap +++ b/packages/react/src/Button/__tests__/__snapshots__/Button.test.tsx.snap @@ -95,7 +95,8 @@ exports[`Button respects block prop 1`] = ` cursor: auto; } -.c0:disabled { +.c0:disabled, +.c0[aria-disabled][data-loading="false"] { cursor: not-allowed; box-shadow: none; color: primer.fg.disabled; @@ -103,7 +104,8 @@ exports[`Button respects block prop 1`] = ` background-color: var(--button-default-bgColor-disabled,undefined); } -.c0:disabled [data-component=ButtonCounter] { +.c0:disabled [data-component=ButtonCounter], +.c0[aria-disabled][data-loading="false"] [data-component=ButtonCounter] { color: inherit; } @@ -178,6 +180,14 @@ exports[`Button respects block prop 1`] = ` } .c0 [data-component="text"] { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; grid-area: text; line-height: calc(20/14); white-space: nowrap; @@ -211,12 +221,22 @@ exports[`Button respects block prop 1`] = ` margin-right: 8px; } -.c0:hover:not([disabled]):not([data-inactive]) { +.c0 [data-component="loadingSpinner"] { + grid-area: text; + margin-right: 0px !important; + place-self: center; +} + +.c0 [data-component="loadingSpinner"] + [data-component="text"] { + visibility: hidden; +} + +.c0:hover:not([disabled]):not([aria-disabled][data-loading="false"]):not([data-inactive]) { background-color: btn.hoverBg; border-color: var(--button-default-borderColor-hover,undefined); } -.c0:active:not([disabled]):not([data-inactive]) { +.c0:active:not([disabled]):not([aria-disabled][data-loading="false"]):not([data-inactive]) { background-color: btn.activeBg; border-color: var(--button-default-borderColor-active,undefined); } @@ -243,9 +263,13 @@ exports[`Button respects block prop 1`] = ` } + + + + ) +} diff --git a/packages/react/src/ButtonGroup/ButtonGroup.tsx b/packages/react/src/ButtonGroup/ButtonGroup.tsx index 251045b990a..0ba399c9ad3 100644 --- a/packages/react/src/ButtonGroup/ButtonGroup.tsx +++ b/packages/react/src/ButtonGroup/ButtonGroup.tsx @@ -8,7 +8,7 @@ const ButtonGroup = styled.div` vertical-align: middle; isolation: isolate; - && > * { + && > *:not([data-loading-wrapper]) { margin-inline-end: -1px; position: relative; border-radius: 0; @@ -30,6 +30,37 @@ const ButtonGroup = styled.div` } } + // if child is loading button + [data-loading-wrapper] { + :first-child { + button, + a { + border-top-left-radius: ${get('radii.2')}; + border-bottom-left-radius: ${get('radii.2')}; + } + } + + :last-child { + button, + a { + border-top-right-radius: ${get('radii.2')}; + border-bottom-right-radius: ${get('radii.2')}; + } + } + } + + [data-loading-wrapper] > * { + margin-inline-end: -1px; + position: relative; + border-radius: 0; + + :focus, + :active, + :hover { + z-index: 1; + } + } + ${sx}; ` diff --git a/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx b/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx index 2369e0be618..7654e6ac3aa 100644 --- a/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx +++ b/packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx @@ -57,7 +57,7 @@ describe('Tooltip', () => { const {getByRole, getByText} = HTMLRender() const triggerEL = getByRole('button') const tooltipEl = getByText('Tooltip text') - expect(triggerEL).toHaveAttribute('aria-describedby', tooltipEl.id) + expect(triggerEL.getAttribute('aria-describedby')).toContain(tooltipEl.id) }) it('should render the tooltip element with role="tooltip" when the tooltip type is description (by default)', () => { const {getByText} = HTMLRender() @@ -74,7 +74,7 @@ describe('Tooltip', () => { ) const menuButton = getByRole('button') const tooltip = getByText('Additional context about the menu button') - expect(menuButton).toHaveAttribute('aria-describedby', tooltip.id) + expect(menuButton.getAttribute('aria-describedby')).toContain(tooltip.id) expect(menuButton).toHaveAttribute('aria-haspopup', 'true') }) @@ -90,7 +90,7 @@ describe('Tooltip', () => { ) const menuButton = getByRole('button') const tooltip = getByText('Additional context about the menu button') - expect(menuButton).toHaveAttribute('aria-describedby', tooltip.id) + expect(menuButton.getAttribute('aria-describedby')).toContain(tooltip.id) expect(menuButton).toHaveAttribute('aria-haspopup', 'true') }) it('should use the custom tooltip id (if present) to label the trigger element', () => { @@ -109,6 +109,6 @@ describe('Tooltip', () => { , ) const triggerEL = getByRole('button') - expect(triggerEL).toHaveAttribute('aria-describedby', 'custom-tooltip-id') + expect(triggerEL.getAttribute('aria-describedby')).toContain('custom-tooltip-id') }) }) diff --git a/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap index 2cfb98c9dff..ffa9a3040f7 100644 --- a/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap @@ -327,6 +327,11 @@ exports[`snapshots renders a loading state 1`] = ` justify-content: center; } +.c2 { + -webkit-animation: rotate-keyframes 1s linear infinite; + animation: rotate-keyframes 1s linear infinite; +} + .c0 { position: absolute; width: 1px; @@ -340,11 +345,6 @@ exports[`snapshots renders a loading state 1`] = ` border-width: 0; } -.c2 { - -webkit-animation: rotate-keyframes 1s linear infinite; - animation: rotate-keyframes 1s linear infinite; -} - @media (min-width:768px) { } diff --git a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap index 6a0f2b1bdf8..0612f23b7e4 100644 --- a/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/packages/react/src/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -1757,15 +1757,19 @@ exports[`TextInput renders trailingAction icon button 1`] = ` cursor: auto; } -.c4:disabled { +.c4:disabled, +.c4[aria-disabled][data-loading="false"] { cursor: not-allowed; box-shadow: none; color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } .c4:disabled [data-component=ButtonCounter], +.c4[aria-disabled][data-loading="false"] [data-component=ButtonCounter], .c4:disabled [data-component="leadingVisual"], -.c4:disabled [data-component="trailingAction"] { +.c4[aria-disabled][data-loading="false"] [data-component="leadingVisual"], +.c4:disabled [data-component="trailingAction"], +.c4[aria-disabled][data-loading="false"] [data-component="trailingAction"] { color: inherit; } @@ -1840,6 +1844,14 @@ exports[`TextInput renders trailingAction icon button 1`] = ` } .c4 [data-component="text"] { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; grid-area: text; line-height: calc(20/14); white-space: nowrap; @@ -1874,11 +1886,21 @@ exports[`TextInput renders trailingAction icon button 1`] = ` margin-right: 8px; } -.c4:hover:not([disabled]) { +.c4 [data-component="loadingSpinner"] { + grid-area: text; + margin-right: 0px !important; + place-self: center; +} + +.c4 [data-component="loadingSpinner"] + [data-component="text"] { + visibility: hidden; +} + +.c4:hover:not([disabled]):not([aria-disabled][data-loading="false"]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); } -.c4:active:not([disabled]) { +.c4:active:not([disabled]):not([aria-disabled][data-loading="false"]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); } @@ -1898,11 +1920,13 @@ exports[`TextInput renders trailingAction icon button 1`] = ` color: var(--button-default-fgColor-rest,var(--color-btn-text,#24292f)); } -.c4:disabled[data-no-visuals] { +.c4:disabled[data-no-visuals], +.c4[aria-disabled][data-loading="false"][data-no-visuals] { color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } -.c4:disabled[data-no-visuals] [data-component=ButtonCounter] { +.c4:disabled[data-no-visuals] [data-component=ButtonCounter], +.c4[aria-disabled][data-loading="false"][data-no-visuals] [data-component=ButtonCounter] { color: inherit; } @@ -2176,10 +2200,12 @@ exports[`TextInput renders trailingAction icon button 1`] = ` className="c3 TextInput-action" >