diff --git a/.changeset/tidy-kings-do.md b/.changeset/tidy-kings-do.md new file mode 100644 index 0000000000..90fbd24b24 --- /dev/null +++ b/.changeset/tidy-kings-do.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': minor +--- + +Upstream the SelectPanel component from dotcom diff --git a/.eslintrc.json b/.eslintrc.json index c1060ee3d0..595325d0b1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -50,7 +50,8 @@ "filenames/match-regex": 0, "import/no-namespace": 0, "no-shadow": 0, - "no-unused-vars": [ + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ "error", { "ignoreRestSiblings": true diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark.png new file mode 100644 index 0000000000..dda9da7cb0 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_colorblind.png new file mode 100644 index 0000000000..512fd8c901 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_dimmed.png new file mode 100644 index 0000000000..0193a01faa Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_high_contrast.png new file mode 100644 index 0000000000..adf444997c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/default.png new file mode 100644 index 0000000000..eddb9738a9 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/focused.png new file mode 100644 index 0000000000..7949904795 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light.png new file mode 100644 index 0000000000..865d18e9e6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_colorblind.png new file mode 100644 index 0000000000..35c732fe11 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_high_contrast.png new file mode 100644 index 0000000000..29d86b6acc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/default/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark.png new file mode 100644 index 0000000000..500fe4888d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_colorblind.png new file mode 100644 index 0000000000..500fe4888d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_dimmed.png new file mode 100644 index 0000000000..96162bd8c4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_high_contrast.png new file mode 100644 index 0000000000..6062a23e16 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light.png new file mode 100644 index 0000000000..8aaa3d1196 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_colorblind.png new file mode 100644 index 0000000000..8aaa3d1196 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_high_contrast.png new file mode 100644 index 0000000000..2a9566d97b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark.png new file mode 100644 index 0000000000..cdf6b7b052 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_colorblind.png new file mode 100644 index 0000000000..2b12b60ced Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_dimmed.png new file mode 100644 index 0000000000..5b10e941cb Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_high_contrast.png new file mode 100644 index 0000000000..965cdaa9ef Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light.png new file mode 100644 index 0000000000..74ef321a0c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_colorblind.png new file mode 100644 index 0000000000..dafbb0b7cf Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_high_contrast.png new file mode 100644 index 0000000000..e4e89c3de7 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_initial_failure/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark.png new file mode 100644 index 0000000000..00bf7ad6dc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_colorblind.png new file mode 100644 index 0000000000..00bf7ad6dc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_dimmed.png new file mode 100644 index 0000000000..7f174fac8d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_high_contrast.png new file mode 100644 index 0000000000..b31290db11 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light.png new file mode 100644 index 0000000000..fb5ed0f49f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_colorblind.png new file mode 100644 index 0000000000..fb5ed0f49f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_high_contrast.png new file mode 100644 index 0000000000..5ba6804929 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/eventually_local_fetch_no_results/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark.png new file mode 100644 index 0000000000..80d5440582 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_colorblind.png new file mode 100644 index 0000000000..5befafdc65 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_dimmed.png new file mode 100644 index 0000000000..5e2fbaac15 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_high_contrast.png new file mode 100644 index 0000000000..49f1334352 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light.png new file mode 100644 index 0000000000..af33bccca4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_colorblind.png new file mode 100644 index 0000000000..9c24e6e3ee Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_high_contrast.png new file mode 100644 index 0000000000..ece3b4cd58 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/footer_buttons/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark.png new file mode 100644 index 0000000000..512fd8c901 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_colorblind.png new file mode 100644 index 0000000000..512fd8c901 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_dimmed.png new file mode 100644 index 0000000000..80529c4eb8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_high_contrast.png new file mode 100644 index 0000000000..adf444997c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/default.png new file mode 100644 index 0000000000..eddb9738a9 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/focused.png new file mode 100644 index 0000000000..7949904795 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light.png new file mode 100644 index 0000000000..865d18e9e6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_colorblind.png new file mode 100644 index 0000000000..865d18e9e6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_high_contrast.png new file mode 100644 index 0000000000..29d86b6acc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark.png new file mode 100644 index 0000000000..77de620f61 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_colorblind.png new file mode 100644 index 0000000000..f8a5129eab Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_dimmed.png new file mode 100644 index 0000000000..7a2783b0cd Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_high_contrast.png new file mode 100644 index 0000000000..7a06d116bc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/default.png new file mode 100644 index 0000000000..eddb9738a9 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/focused.png new file mode 100644 index 0000000000..7949904795 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light.png new file mode 100644 index 0000000000..9e1fc43327 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_colorblind.png new file mode 100644 index 0000000000..3415608d6d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_high_contrast.png new file mode 100644 index 0000000000..9edf501412 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/local_fetch_no_results/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark.png new file mode 100644 index 0000000000..f1a8ce3c51 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_colorblind.png new file mode 100644 index 0000000000..f1a8ce3c51 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_dimmed.png new file mode 100644 index 0000000000..3d27f5c25c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_high_contrast.png new file mode 100644 index 0000000000..499c8f9e5e Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/default.png new file mode 100644 index 0000000000..f8a230e07c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/focused.png new file mode 100644 index 0000000000..0db9a13fd4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light.png new file mode 100644 index 0000000000..fb32f0d9ef Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_colorblind.png new file mode 100644 index 0000000000..fb32f0d9ef Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_high_contrast.png new file mode 100644 index 0000000000..0c9a168c39 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/multiselect/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark.png new file mode 100644 index 0000000000..500fe4888d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_colorblind.png new file mode 100644 index 0000000000..500fe4888d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_dimmed.png new file mode 100644 index 0000000000..96162bd8c4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_high_contrast.png new file mode 100644 index 0000000000..6062a23e16 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light.png new file mode 100644 index 0000000000..8aaa3d1196 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_colorblind.png new file mode 100644 index 0000000000..8aaa3d1196 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_high_contrast.png new file mode 100644 index 0000000000..2a9566d97b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark.png new file mode 100644 index 0000000000..72078c652b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_colorblind.png new file mode 100644 index 0000000000..72078c652b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_dimmed.png new file mode 100644 index 0000000000..96d7ac3e83 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_high_contrast.png new file mode 100644 index 0000000000..b47ab2dcf1 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light.png new file mode 100644 index 0000000000..1fb5df39d3 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_colorblind.png new file mode 100644 index 0000000000..1fb5df39d3 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_high_contrast.png new file mode 100644 index 0000000000..845ffd3003 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_filter_failure/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark.png new file mode 100644 index 0000000000..cdf6b7b052 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_colorblind.png new file mode 100644 index 0000000000..2b12b60ced Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_dimmed.png new file mode 100644 index 0000000000..5b10e941cb Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_high_contrast.png new file mode 100644 index 0000000000..965cdaa9ef Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light.png new file mode 100644 index 0000000000..74ef321a0c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_colorblind.png new file mode 100644 index 0000000000..dafbb0b7cf Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_high_contrast.png new file mode 100644 index 0000000000..e4e89c3de7 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_initial_failure/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark.png new file mode 100644 index 0000000000..00bf7ad6dc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_colorblind.png new file mode 100644 index 0000000000..00bf7ad6dc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_dimmed.png new file mode 100644 index 0000000000..7f174fac8d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_high_contrast.png new file mode 100644 index 0000000000..b31290db11 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/default.png new file mode 100644 index 0000000000..1dedef72e8 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/focused.png new file mode 100644 index 0000000000..b50383fbcc Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light.png new file mode 100644 index 0000000000..fb5ed0f49f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_colorblind.png new file mode 100644 index 0000000000..fb5ed0f49f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_high_contrast.png new file mode 100644 index 0000000000..5ba6804929 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/remote_fetch_no_results/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark.png new file mode 100644 index 0000000000..620e44f915 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_colorblind.png new file mode 100644 index 0000000000..f4f277c65b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_dimmed.png new file mode 100644 index 0000000000..6a5e147647 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_high_contrast.png new file mode 100644 index 0000000000..74867a96f9 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/default.png new file mode 100644 index 0000000000..f8a230e07c Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/focused.png new file mode 100644 index 0000000000..0db9a13fd4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light.png new file mode 100644 index 0000000000..7bdcbc8857 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_colorblind.png new file mode 100644 index 0000000000..7bdcbc8857 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_high_contrast.png new file mode 100644 index 0000000000..f738211f82 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/single_select/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark.png new file mode 100644 index 0000000000..0f71993ef6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_colorblind.png new file mode 100644 index 0000000000..0f71993ef6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_dimmed.png new file mode 100644 index 0000000000..ec84a87396 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_high_contrast.png new file mode 100644 index 0000000000..877aadb303 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/default.png new file mode 100644 index 0000000000..3ae93f5c6b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/focused.png new file mode 100644 index 0000000000..180edcc41d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light.png new file mode 100644 index 0000000000..002ba2fb26 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_colorblind.png new file mode 100644 index 0000000000..b76c748281 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_high_contrast.png new file mode 100644 index 0000000000..45093d50a6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_avatar_items/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark.png new file mode 100644 index 0000000000..08f62bccd7 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_colorblind.png new file mode 100644 index 0000000000..036d29cb4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_dimmed.png new file mode 100644 index 0000000000..afc1833f21 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_high_contrast.png new file mode 100644 index 0000000000..4ea8ddf206 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light.png new file mode 100644 index 0000000000..e2d50fbd2d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_colorblind.png new file mode 100644 index 0000000000..e2d50fbd2d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_high_contrast.png new file mode 100644 index 0000000000..9d24a3d9b6 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark.png new file mode 100644 index 0000000000..036d29cb4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_colorblind.png new file mode 100644 index 0000000000..036d29cb4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_dimmed.png new file mode 100644 index 0000000000..efcbb14f63 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_high_contrast.png new file mode 100644 index 0000000000..4ea8ddf206 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light.png new file mode 100644 index 0000000000..e2d50fbd2d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_colorblind.png new file mode 100644 index 0000000000..54dc04386d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_high_contrast.png new file mode 100644 index 0000000000..bbaf6456a5 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_dynamic_label_and_aria_prefix/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark.png new file mode 100644 index 0000000000..e17a7319dd Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_colorblind.png new file mode 100644 index 0000000000..e17a7319dd Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_dimmed.png new file mode 100644 index 0000000000..b8f6485d12 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_high_contrast.png new file mode 100644 index 0000000000..1b13acbf4d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light.png new file mode 100644 index 0000000000..c412798dcf Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_colorblind.png new file mode 100644 index 0000000000..c412798dcf Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_high_contrast.png new file mode 100644 index 0000000000..2f1d8a87f9 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_leading_icons/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark.png new file mode 100644 index 0000000000..dd3d5d1004 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_colorblind.png new file mode 100644 index 0000000000..dd3d5d1004 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_dimmed.png new file mode 100644 index 0000000000..04e764ea0b Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_high_contrast.png new file mode 100644 index 0000000000..e9b271fa8a Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light.png new file mode 100644 index 0000000000..ff76e6faa3 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_colorblind.png new file mode 100644 index 0000000000..ff76e6faa3 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_high_contrast.png new file mode 100644 index 0000000000..8e1e403dda Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_subtitle/light_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark.png new file mode 100644 index 0000000000..92fedd41c4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_colorblind.png new file mode 100644 index 0000000000..92fedd41c4 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_dimmed.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_dimmed.png new file mode 100644 index 0000000000..c4b9738e64 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_dimmed.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_high_contrast.png new file mode 100644 index 0000000000..147b503ff2 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/dark_high_contrast.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/default.png new file mode 100644 index 0000000000..6144594f1f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/focused.png new file mode 100644 index 0000000000..0808d38e9f Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/focused.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light.png new file mode 100644 index 0000000000..de73af8466 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_colorblind.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_colorblind.png new file mode 100644 index 0000000000..de73af8466 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_colorblind.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_high_contrast.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_high_contrast.png new file mode 100644 index 0000000000..4507790c00 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/select_panel/with_trailing_icons/light_high_contrast.png differ diff --git a/app/components/primer/alpha/action_menu.rb b/app/components/primer/alpha/action_menu.rb index a731d8cf11..b9c0e8cf8b 100644 --- a/app/components/primer/alpha/action_menu.rb +++ b/app/components/primer/alpha/action_menu.rb @@ -131,9 +131,7 @@ module Alpha # # ### JavaScript API # - # `ActionList`s render an `` custom element that exposes behavior to the client. For all these methods, - # `itemId` refers to the value of the `item_id:` argument (see below) that is used to populate the `data-item-id` HTML - # attribute. + # `ActionMenu`s render an `` custom element that exposes behavior to the client. # # #### Query methods # @@ -142,6 +140,15 @@ module Alpha # * `isItemHidden(item: Element): boolean`: Returns `true` if the item is hidden, `false` otherwise. # * `isItemDisabled(item: Element): boolean`: Returns `true` if the item is disabled, `false` otherwise. # + # NOTE: Item IDs are special values provided by the user that are attached to `ActionMenu` items as the `data-item-id` + # HTML attribute. Item IDs can be provided by passing an `item_id:` attribute when adding items to the list, eg: + # + # ```erb + # <%= render(Primer::Alpha::ActionMenu.new) do |menu| %> + # <% menu.with_item(item_id: "my-id") %> + # <% end %> + # ``` + # # #### State methods # # * `showItem(item: Element)`: Shows the item, i.e. makes it visible. @@ -156,9 +163,9 @@ module Alpha # The `` element fires an `itemActivated` event whenever an item is activated (eg. clicked) via the mouse or keyboard. # # ```typescript - # document.querySelector("action-menu").addEventListener("itemActivated", (event: ItemActivatedEvent) => { - # event.item // Element: the
  • item that was activated - # event.checked // boolean: whether or not the result of the activation checked the item + # document.querySelector("action-menu").addEventListener("itemActivated", (event: CustomEvent) => { + # event.detail.item // Element: the
  • item that was activated + # event.detail.checked // boolean: whether or not the result of the activation checked the item # }) # ``` class ActionMenu < Primer::Component diff --git a/app/components/primer/alpha/action_menu/action_menu_element.ts b/app/components/primer/alpha/action_menu/action_menu_element.ts index 714bce4abc..085edb08b6 100644 --- a/app/components/primer/alpha/action_menu/action_menu_element.ts +++ b/app/components/primer/alpha/action_menu/action_menu_element.ts @@ -12,17 +12,6 @@ type SelectedItem = { const validSelectors = ['[role="menuitem"]', '[role="menuitemcheckbox"]', '[role="menuitemradio"]'] const menuItemSelectors = validSelectors.map(selector => `:not([hidden]) > ${selector}`) -export type ItemActivatedEvent = { - item: Element - checked: boolean -} - -declare global { - interface HTMLElementEventMap { - itemActivated: CustomEvent - } -} - @controller export class ActionMenuElement extends HTMLElement { @target diff --git a/app/components/primer/alpha/select_panel.html.erb b/app/components/primer/alpha/select_panel.html.erb new file mode 100644 index 0000000000..56fc23869e --- /dev/null +++ b/app/components/primer/alpha/select_panel.html.erb @@ -0,0 +1,100 @@ +<%= render Primer::BaseComponent.new(**@system_arguments) do %> + + <%= show_button %> + <%= render(@dialog) do %> + <%= render Primer::Alpha::Dialog::Header.new(id: "#{@panel_id}-dialog", title: @title) do |header| %> + <% if subtitle? %> + <% header.with_subtitle do %> + <%= subtitle %> + <% end %> + <% end %> + <% if show_filter? %> + <% header.with_filter do %> + <%= render(Primer::BaseComponent.new( + tag: :"remote-input", + aria: { owns: @body_id }, + **(@fetch_strategy == :remote ? { src: @src } : {}), + data: { + target: "select-panel.remoteInput" + } + )) do %> + <%= render(Primer::Alpha::TextField.new( + id: "#{@panel_id}-filter", + name: :filter, + label: "Filter", + type: :search, + leading_visual: { icon: :search }, + leading_spinner: true, + autofocus: true, + visually_hide_label: true, + data: { target: "select-panel.filterInputTextField" }, + label_arguments: { + position: :absolute + } + )) %> + <% end %> + <% end %> + <% end %> + <% end %> + + <%= render Primer::Alpha::Dialog::Body.new(mt: 2, p: 0) do %> + +
    + <%= render(Primer::BaseComponent.new( + tag: :div, + data: { + fetch_strategy: @fetch_strategy, + target: "select-panel.list" + } + )) do %> +
    + <% if @src.present? %> + <%= render(Primer::ConditionalWrapper.new(condition: @fetch_strategy == :eventually_local, tag: "include-fragment", data: { target: "select-panel.includeFragment" }, src: @src, loading: preload? ? "eager" : "lazy", accept: "text/fragment+html")) do %> + <%= render(Primer::BaseComponent.new( + tag: :div, + id: "#{@panel_id}-list", + mt: 2, + mb: 2, + aria: { disabled: true, busy: true }, + display: :flex, + align_items: :center, + justify_content: :center, + text_align: :center + )) do %> +
    + <%= render Primer::Beta::Spinner.new(aria: { label: "Loading content..." }, data: { target: "select-panel.bodySpinner" }) %> +
    + + <% end %> + <% end %> + <% else %> + <%= render(@list) %> + <% end %> +
    + + <% end %> +
    + <% end %> + <%= footer %> + <% end %> +
    +<% end %> diff --git a/app/components/primer/alpha/select_panel.rb b/app/components/primer/alpha/select_panel.rb new file mode 100644 index 0000000000..4b303254bd --- /dev/null +++ b/app/components/primer/alpha/select_panel.rb @@ -0,0 +1,475 @@ +# frozen_string_literal: true + +module Primer + module Alpha + # Select panels allow for selecting from a large number of options and can be thought of as a more capable + # version of the traditional HTML `` boxes behave, and + # play nicely with Rails' built-in form mechanisms. Pass arguments via the `form_arguments:` argument, including + # the Rails form builder object and the name of the field: + # + # ```erb + # <% form_with(model: Address.new) do |f| %> + # <%= render(Primer::Alpha::SelectPanel.new(form_arguments: { builder: f, name: "country" })) do |menu| %> + # <% countries.each do |country| + # <% menu.with_item(label: country.name, data: { value: country.code }) %> + # <% end %> + # <% end %> + # <% end %> + # ``` + # + # The value of the `data: { value: ... }` argument is sent to the server on submit, keyed using the name provided above + # (eg. `"country"`). If no value is provided for an item, the value of that item is the item's label. Here's the + # corresponding `AddressesController` that might be written to handle the form above: + # + # ```ruby + # class AddressesController < ApplicationController + # def create + # puts "You chose #{address_params[:country]} as your country" + # end + # + # private + # + # def address_params + # params.require(:address).permit(:country) + # end + # end + # ``` + # + # If items are provided dynamically, things become a bit more complicated. The `form_for` or `form_with` method call + # happens in the view that renders the `SelectPanel`, which means the form builder object but isn't available in the + # view that renders the list items. In such a case, it can be useful to create an instance of the form builder maually: + # + # ```erb + # <% builder = ActionView::Helpers::FormBuilder.new( + # "address", # the name of the model, used to wrap input names, eg 'address[country_code]' + # nil, # object (eg. the Address instance, which we can omit) + # self, # template + # {} # options + # ) %> + # <%= render(Primer::Alpha::SelectPanel::ItemList.new( + # form_arguments: { builder: builder, name: "country" } + # )) do |list| %> + # <% countries.each do |country| %> + # <% menu.with_item(label: country.name, data: { value: country.code }) %> + # <% end %> + # <% end %> + # ``` + # + # ### JavaScript API + # + # `SelectPanel`s render a `` custom element that exposes behavior to the client. + # + # #### Utility methods + # + # * `show()`: Manually open the panel. Under normal circumstances, a show button is used to show the panel, but this method exists to support unusual use-cases. + # * `hide()`: Manually hides (closes) the panel. + # + # #### Query methods + # + # * `getItemById(itemId: string): Element`: Returns the item's HTML `
  • ` element. The return value can be passed as the `item` argument to the other methods listed below. + # * `isItemChecked(item: Element): boolean`: Returns `true` if the item is checked, `false` otherwise. + # * `isItemHidden(item: Element): boolean`: Returns `true` if the item is hidden, `false` otherwise. + # * `isItemDisabled(item: Element): boolean`: Returns `true` if the item is disabled, `false` otherwise. + # + # NOTE: Item IDs are special values provided by the user that are attached to `SelectPanel` list items as the `data-item-id` + # HTML attribute. Item IDs can be provided by passing an `item_id:` attribute when adding items to the panel, eg: + # + # ```erb + # <%= render(Primer::Alpha::SelectPanel.new) do |panel| %> + # <% panel.with_item(item_id: "my-id") %> + # <% end %> + # ``` + # + # The same is true when rendering `ItemList`s: + # + # ```erb + # <%= render(Primer::Alpha::SelectPanel::ItemList.new) do |list| %> + # <% list.with_item(item_id: "my-id") %> + # <% end %> + # ``` + # + # #### State methods + # + # * `showItem(item: Element)`: Shows the item, i.e. makes it visible. + # * `hideItem(item: Element)`: Hides the item, i.e. makes it invisible. + # * `enableItem(item: Element)`: Enables the item, i.e. makes it clickable by the mouse and keyboard. + # * `disableItem(item: Element)`: Disables the item, i.e. makes it unclickable by the mouse and keyboard. + # * `checkItem(item: Element)`: Checks the item. Only has an effect in single- and multi-select modes. + # * `uncheckItem(item: Element)`: Unchecks the item. Only has an effect in multi-select mode, since items cannot be unchecked in single-select mode. + # + # #### Events + # + # |Name |Type |Bubbles |Cancelable | + # |:--------------------|:------------------------------------------|:-------|:----------| + # |`itemActivated` |`CustomEvent` |Yes |No | + # |`beforeItemActivated`|`CustomEvent` |Yes |Yes | + # |`dialog:open` |`CustomEvent<{dialog: HTMLDialogElement}>` |No |No | + # |`panelClosed` |`CustomEvent<{panel: SelectPanelElement}>` |Yes |No | + # + # _Item activation_ + # + # The `` element fires an `itemActivated` event whenever an item is activated (eg. clicked) via the mouse or keyboard. + # + # ```typescript + # document.querySelector("select-panel").addEventListener( + # "itemActivated", + # (event: CustomEvent) => { + # event.detail.item // Element: the
  • item that was activated + # event.detail.checked // boolean: whether or not the result of the activation checked the item + # } + # ) + # ``` + # + # The `beforeItemActivated` event fires before an item is activated. Canceling this event will prevent the item + # from being activated. + # + # ```typescript + # document.querySelector("select-panel").addEventListener( + # "beforeItemActivated", + # (event: CustomEvent) => { + # event.detail.item // Element: the
  • item that was activated + # event.detail.checked // boolean: whether or not the result of the activation checked the item + # event.preventDefault() // Cancel the event to prevent activation (eg. checking/unchecking) + # } + # ) + # ``` + class SelectPanel < Primer::Component + status :alpha + + # The component that should be used to render the list of items in the body of a SelectPanel. + ItemList = Primer::Alpha::ActionList + + DEFAULT_PRELOAD = false + + DEFAULT_FETCH_STRATEGY = :remote + FETCH_STRATEGIES = [ + DEFAULT_FETCH_STRATEGY, + :eventually_local, + :local + ] + + DEFAULT_SELECT_VARIANT = :single + SELECT_VARIANT_OPTIONS = [ + DEFAULT_SELECT_VARIANT, + :multiple, + :none, + ].freeze + + # The URL to fetch search results from. + # + # @return [String] + attr_reader :src + + # The unique ID of the panel. + # + # @return [String] + attr_reader :panel_id + + # The unique ID of the panel body. + # + # @return [String] + attr_reader :body_id + + # <%= one_of(Primer::Alpha::ActionMenu::SELECT_VARIANT_OPTIONS) %> + # + # @return [Symbol] + attr_reader :select_variant + + # <%= one_of(Primer::Alpha::SelectPanel::FETCH_STRATEGIES) %> + # + # @return [Symbol] + attr_reader :fetch_strategy + + # Whether to preload search results when the page loads. If this option is false, results are loaded when the panel is opened. + # + # @return [Boolean] + attr_reader :preload + + alias preload? preload + + # Whether or not to show the filter input. + # + # @return [Boolean] + attr_reader :show_filter + + alias show_filter? show_filter + + # @param src [String] The URL to fetch search results from. + # @param title [String] The title that appears at the top of the panel. + # @param id [String] The unique ID of the panel. + # @param size [Symbol] The size of the panel. <%= one_of(Primer::Alpha::Overlay::SIZE_OPTIONS) %> + # @param select_variant [Symbol] <%= one_of(Primer::Alpha::ActionList::SELECT_VARIANT_OPTIONS) %> + # @param fetch_strategy [Symbol] <%= one_of(Primer::Alpha::SelectPanel::FETCH_STRATEGIES) %> + # @param no_results_label [String] The label to display when no results are found. + # @param preload [Boolean] Whether to preload search results when the page loads. If this option is false, results are loaded when the panel is opened. + # @param dynamic_label [Boolean] Whether or not to display the text of the currently selected item in the show button. + # @param dynamic_label_prefix [String] If provided, the prefix is prepended to the dynamic label and displayed in the show button. + # @param dynamic_aria_label_prefix [String] If provided, the prefix is prepended to the dynamic label and set as the value of the `aria-label` attribute on the show button. + # @param body_id [String] The unique ID of the panel body. If not provided, the body ID will be set to the panel ID with a "-body" suffix. + # @param list_arguments [Hash] Arguments to pass to the underlying <%= link_to_component(Primer::Alpha::ActionList) %> component. Only has an effect for the local fetch strategy. + # @param form_arguments [Hash] Form arguments to pass to the underlying <%= link_to_component(Primer::Alpha::ActionList) %> component. Only has an effect for the local fetch strategy. + # @param show_filter [Boolean] Whether or not to show the filter input. + # @param open_on_load [Boolean] Open the panel when the page loads. + # @param anchor_align [Symbol] The anchor alignment of the Overlay. <%= one_of(Primer::Alpha::Overlay::ANCHOR_ALIGN_OPTIONS) %> + # @param anchor_side [Symbol] The side to anchor the Overlay to. <%= one_of(Primer::Alpha::Overlay::ANCHOR_SIDE_OPTIONS) %> + def initialize( + src: nil, + title: "Menu", + id: self.class.generate_id, + size: :small, + select_variant: DEFAULT_SELECT_VARIANT, + fetch_strategy: DEFAULT_FETCH_STRATEGY, + no_results_label: "No results found", + preload: DEFAULT_PRELOAD, + dynamic_label: false, + dynamic_label_prefix: nil, + dynamic_aria_label_prefix: nil, + body_id: nil, + list_arguments: {}, + form_arguments: {}, + show_filter: true, + open_on_load: false, + anchor_align: Primer::Alpha::Overlay::DEFAULT_ANCHOR_ALIGN, + anchor_side: Primer::Alpha::Overlay::DEFAULT_ANCHOR_SIDE, + **system_arguments + ) + if src.present? + url = URI(src) + query = url.query || "" + url.query = query.split("&").push("experimental=1").join("&") + @src = url + end + + @panel_id = id + @body_id = body_id || "#{@panel_id}-body" + @preload = fetch_or_fallback_boolean(preload, DEFAULT_PRELOAD) + @select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT) + @fetch_strategy = fetch_or_fallback(FETCH_STRATEGIES, fetch_strategy, DEFAULT_FETCH_STRATEGY) + @no_results_label = no_results_label + @show_filter = show_filter + @dynamic_label = dynamic_label + @dynamic_label_prefix = dynamic_label_prefix + @dynamic_aria_label_prefix = dynamic_aria_label_prefix + + @system_arguments = deny_tag_argument(**system_arguments) + @system_arguments[:id] = @panel_id + @system_arguments[:"anchor-align"] = fetch_or_fallback(Primer::Alpha::Overlay::ANCHOR_ALIGN_OPTIONS, anchor_align, Primer::Alpha::Overlay::DEFAULT_ANCHOR_ALIGN) + @system_arguments[:"anchor-side"] = Primer::Alpha::Overlay::ANCHOR_SIDE_MAPPINGS[fetch_or_fallback(Primer::Alpha::Overlay::ANCHOR_SIDE_OPTIONS, anchor_side, Primer::Alpha::Overlay::DEFAULT_ANCHOR_SIDE)] + + @title = title + @system_arguments[:tag] = :"select-panel" + @system_arguments[:preload] = true if @src.present? && preload? + + @system_arguments[:data] = merge_data( + system_arguments, { + data: { select_variant: @select_variant, fetch_strategy: @fetch_strategy, open_on_load: open_on_load }.tap do |data| + data[:dynamic_label] = dynamic_label if dynamic_label + data[:dynamic_label_prefix] = dynamic_label_prefix if dynamic_label_prefix.present? + data[:dynamic_aria_label_prefix] = dynamic_aria_label_prefix if dynamic_aria_label_prefix.present? + end + } + ) + + @dialog = Primer::BaseComponent.new( + id: "#{@panel_id}-dialog", + tag: :dialog, + data: { target: "select-panel.dialog" }, + classes: class_names( + "Overlay", + "Overlay-whenNarrow", + Primer::Alpha::Dialog::SIZE_MAPPINGS[ + fetch_or_fallback(Primer::Alpha::Dialog::SIZE_OPTIONS, size, Primer::Alpha::Dialog::DEFAULT_SIZE) + ], + ), + style: "position: absolute;", + ) + + @list = Primer::Alpha::SelectPanel::ItemList.new( + **list_arguments, + form_arguments: form_arguments, + id: "#{@panel_id}-list", + select_variant: @select_variant, + body_id: @body_id, + role: "listbox", + aria_selection_variant: @select_variant == :multiple ? :checked : :selected, + aria: { + label: "#{title} options" + }, + p: 2 + ) + end + + # @!parse + # # Adds an item to the list. Note that this method only has an effect for the local fetch strategy. + # # + # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot. + # def with_item(**system_arguments) + # end + # + # # Adds an avatar item to the list. Note that this method only has an effect for the local fetch strategy. + # + # # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>'s `item` slot. + # def with_avatar_item + # end + + delegate :with_item, :with_avatar_item, to: :@list + + # Renders content in a footer region below the list of items. + # + # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Dialog::Footer) %>. + renders_one :footer, Primer::Alpha::Dialog::Footer + + # Renders content underneath the title at the top of the panel. + # + # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::Dialog::Header) %>'s `subtitle` slot. + renders_one :subtitle + + # Adds a show button (i.e. a button) that will open the panel when clicked. + # + # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::Button) %>. + renders_one :show_button, lambda { |**system_arguments| + system_arguments[:id] = "#{@panel_id}-button" + + system_arguments[:aria] = merge_aria( + system_arguments, + { aria: { controls: "#{@panel_id}-dialog" } } + ) + + Primer::Beta::Button.new(**system_arguments) + } + + # Customizable content for the error message that appears when items are fetched for the first time. This message + # appears in place of the list of items. + # For more information, see the [documentation regarding SelectPanel error messaging](/components/selectpanel#errorwarning). + renders_one :preload_error_content + + # Customizable content for the error message that appears when items are fetched as the result of a filter + # operation. This message appears as a banner above the previously fetched list of items. + # For more information, see the [documentation regarding SelectPanel error messaging](/components/selectpanel#errorwarning). + renders_one :error_content + + private + + def before_render + content + end + end + end +end diff --git a/app/components/primer/alpha/select_panel_element.ts b/app/components/primer/alpha/select_panel_element.ts new file mode 100644 index 0000000000..f3d91bfbee --- /dev/null +++ b/app/components/primer/alpha/select_panel_element.ts @@ -0,0 +1,1032 @@ +import {getAnchoredPosition} from '@primer/behaviors' +import {controller, target} from '@github/catalyst' +import {announceFromElement, announce} from '../aria_live' +import {IncludeFragmentElement} from '@github/include-fragment-element' +import type {PrimerTextFieldElement} from 'lib/primer/forms/primer_text_field' +import type {AnchorAlignment, AnchorSide} from '@primer/behaviors' +import '@oddbird/popover-polyfill' + +type SelectVariant = 'none' | 'single' | 'multiple' | null +type SelectedItem = { + label: string | null | undefined + value: string | null | undefined + inputName: string | null | undefined + element: SelectPanelItem +} + +const validSelectors = ['[role="option"]'] +const menuItemSelectors = validSelectors.join(',') +const visibleMenuItemSelectors = validSelectors.map(selector => `:not([hidden]) > ${selector}`).join(',') + +export type SelectPanelItem = HTMLLIElement + +enum FetchStrategy { + REMOTE, + EVENTUALLY_LOCAL, + LOCAL, +} + +enum ErrorStateType { + BODY, + BANNER, +} + +export type FilterFn = (item: SelectPanelItem, query: string) => boolean + +const updateWhenVisible = (() => { + const anchors = new Set() + let resizeObserver: ResizeObserver | null = null + function updateVisibleAnchors() { + for (const anchor of anchors) { + anchor.updateAnchorPosition() + } + } + return (el: SelectPanelElement) => { + // eslint-disable-next-line github/prefer-observers + window.addEventListener('resize', updateVisibleAnchors) + // eslint-disable-next-line github/prefer-observers + window.addEventListener('scroll', updateVisibleAnchors) + + resizeObserver ||= new ResizeObserver(() => { + for (const anchor of anchors) { + anchor.updateAnchorPosition() + } + }) + resizeObserver.observe(el.ownerDocument.documentElement) + el.addEventListener('dialog:close', () => { + anchors.delete(el) + }) + el.addEventListener('dialog:open', () => { + anchors.add(el) + }) + } +})() + +@controller +export class SelectPanelElement extends HTMLElement { + @target includeFragment: IncludeFragmentElement + @target dialog: HTMLDialogElement + @target filterInputTextField: HTMLInputElement + @target remoteInput: HTMLElement + @target list: HTMLElement + @target ariaLiveContainer: HTMLElement + @target noResults: HTMLElement + @target fragmentErrorElement: HTMLElement + @target errorBannerElement: HTMLElement + @target bodySpinner: HTMLElement + + filterFn?: FilterFn + + #dialogIntersectionObserver: IntersectionObserver + #abortController: AbortController + #originalLabel = '' + #inputName = '' + #selectedItems: Map = new Map() + #loadingDelayTimeoutId: number | null = null + #loadingAnnouncementTimeoutId: number | null = null + + get open(): boolean { + return this.dialog.open + } + + get selectVariant(): SelectVariant { + return this.getAttribute('data-select-variant') as SelectVariant + } + + get ariaSelectionType(): string { + return this.selectVariant === 'multiple' ? 'aria-checked' : 'aria-selected' + } + + set selectVariant(variant: SelectVariant) { + if (variant) { + this.setAttribute('data-select-variant', variant) + } else { + this.removeAttribute('variant') + } + } + + get dynamicLabelPrefix(): string { + const prefix = this.getAttribute('data-dynamic-label-prefix') + if (!prefix) return '' + return `${prefix}:` + } + + get dynamicAriaLabelPrefix(): string { + const prefix = this.getAttribute('data-dynamic-aria-label-prefix') + if (!prefix) return '' + return `${prefix}:` + } + + set dynamicLabelPrefix(value: string) { + this.setAttribute('data-dynamic-label', value) + } + + get dynamicLabel(): boolean { + return this.hasAttribute('data-dynamic-label') + } + + set dynamicLabel(value: boolean) { + this.toggleAttribute('data-dynamic-label', value) + } + + get invokerElement(): HTMLButtonElement | null { + const id = this.querySelector('dialog')?.id + if (!id) return null + for (const el of this.querySelectorAll(`[aria-controls]`)) { + if (el.getAttribute('aria-controls') === id) { + return el as HTMLButtonElement + } + } + return null + } + + get closeButton(): HTMLButtonElement | null { + return this.querySelector('button[data-close-dialog-id]') + } + + get invokerLabel(): HTMLElement | null { + if (!this.invokerElement) return null + return this.invokerElement.querySelector('.Button-label') + } + + get selectedItems(): SelectedItem[] { + return Array.from(this.#selectedItems.values()) + } + + get align(): AnchorAlignment { + return (this.getAttribute('anchor-align') || 'start') as AnchorAlignment + } + + get side(): AnchorSide { + return (this.getAttribute('anchor-side') || 'outside-bottom') as AnchorSide + } + + updateAnchorPosition() { + // If the selectPanel is removed from the screen on resize close the dialog + if (this && this.offsetParent === null) { + this.dialog.close() + } + + if (this.invokerElement) { + const {top, left} = getAnchoredPosition(this.dialog, this.invokerElement, { + align: this.align, + side: this.side, + anchorOffset: 4, + }) + this.dialog.style.top = `${top}px` + this.dialog.style.left = `${left}px` + this.dialog.style.bottom = 'auto' + this.dialog.style.right = 'auto' + } + } + + connectedCallback() { + const {signal} = (this.#abortController = new AbortController()) + this.addEventListener('keydown', this, {signal}) + this.addEventListener('click', this, {signal}) + this.addEventListener('mousedown', this, {signal}) + this.addEventListener('input', this, {signal}) + this.addEventListener('remote-input-success', this, {signal}) + this.addEventListener('remote-input-error', this, {signal}) + this.addEventListener('loadstart', this, {signal}) + this.#setDynamicLabel() + this.#updateInput() + this.#softDisableItems() + updateWhenVisible(this) + + if (this.remoteInput) { + this.remoteInput.addEventListener('loadstart', this, {signal}) + this.remoteInput.addEventListener('loadend', this, {signal}) + } else { + const mutationObserver = new MutationObserver(() => { + if (this.remoteInput) { + this.remoteInput.addEventListener('loadstart', this, {signal}) + this.remoteInput.addEventListener('loadend', this, {signal}) + mutationObserver.disconnect() + } + }) + + mutationObserver.observe(this, {childList: true, subtree: true}) + } + + if (this.includeFragment) { + this.includeFragment.addEventListener('include-fragment-replaced', this, {signal}) + this.includeFragment.addEventListener('error', this, {signal}) + this.includeFragment.addEventListener('loadend', this, {signal}) + } else { + const mutationObserver = new MutationObserver(() => { + if (this.includeFragment) { + this.includeFragment.addEventListener('include-fragment-replaced', this, {signal}) + this.includeFragment.addEventListener('error', this, {signal}) + this.includeFragment.addEventListener('loadend', this, {signal}) + mutationObserver.disconnect() + } + }) + + mutationObserver.observe(this, {childList: true, subtree: true}) + } + + this.#dialogIntersectionObserver = new IntersectionObserver(entries => { + for (const entry of entries) { + const elem = entry.target + if (entry.isIntersecting && elem === this.dialog) { + this.updateAnchorPosition() + + if (this.#fetchStrategy === FetchStrategy.LOCAL) { + this.#updateItemVisibility() + } + } + } + }) + + if (this.dialog) { + if (this.getAttribute('data-open-on-load') === 'true') { + this.show() + } + + this.#dialogIntersectionObserver.observe(this.dialog) + } else { + const mutationObserver = new MutationObserver(() => { + if (this.dialog) { + if (this.getAttribute('data-open-on-load') === 'true') { + this.show() + } + + this.#dialogIntersectionObserver.observe(this.dialog) + mutationObserver.disconnect() + } + }) + + mutationObserver.observe(this, {childList: true, subtree: true}) + } + } + + disconnectedCallback() { + this.#abortController.abort() + } + + #softDisableItems() { + const {signal} = this.#abortController + + for (const item of this.querySelectorAll(validSelectors.join(','))) { + item.addEventListener('click', this.#potentiallyDisallowActivation.bind(this), {signal}) + item.addEventListener('keydown', this.#potentiallyDisallowActivation.bind(this), {signal}) + } + } + + // If there is an active item in single-select mode, set its tabindex to 0. Otherwise, set the + // first visible item's tabindex to 0. All other items should have a tabindex of -1. + #updateTabIndices() { + let setZeroTabIndex = false + + if (this.selectVariant === 'single') { + for (const item of this.items) { + const itemContent = this.#getItemContent(item) + if (!itemContent) continue + + if (!this.isItemHidden(item) && this.isItemChecked(item) && !setZeroTabIndex) { + itemContent.setAttribute('tabindex', '0') + setZeroTabIndex = true + } else { + itemContent.setAttribute('tabindex', '-1') + } + + //
  • elements should not themselves be tabbable + item.setAttribute('tabindex', '-1') + } + } else { + for (const item of this.items) { + const itemContent = this.#getItemContent(item) + if (!itemContent) continue + + if (!this.isItemHidden(item) && !setZeroTabIndex) { + setZeroTabIndex = true + } else { + itemContent.setAttribute('tabindex', '-1') + } + + //
  • elements should not themselves be tabbable + item.setAttribute('tabindex', '-1') + } + } + + if (!setZeroTabIndex && this.#firstItem) { + this.#getItemContent(this.#firstItem)?.setAttribute('tabindex', '0') + } + } + + // returns true if activation was prevented + #potentiallyDisallowActivation(event: Event): boolean { + if (!this.#isActivation(event)) return false + + const item = (event.target as HTMLElement).closest(visibleMenuItemSelectors) + if (!item) return false + + if (item.getAttribute('aria-disabled')) { + event.preventDefault() + + // eslint-disable-next-line no-restricted-syntax + event.stopPropagation() + + // eslint-disable-next-line no-restricted-syntax + event.stopImmediatePropagation() + return true + } + + return false + } + + #isAnchorActivationViaSpace(event: Event): boolean { + return ( + event.target instanceof HTMLAnchorElement && + event instanceof KeyboardEvent && + event.type === 'keydown' && + !(event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) && + event.key === ' ' + ) + } + + #isActivation(event: Event): boolean { + // Some browsers fire MouseEvents (Firefox) and others fire PointerEvents (Chrome). Activating an item via + // enter or space counterintuitively fires one of these rather than a KeyboardEvent. Since PointerEvent + // inherits from MouseEvent, it is enough to check for MouseEvent here. + return (event instanceof MouseEvent && event.type === 'click') || this.#isAnchorActivationViaSpace(event) + } + + #checkSelectedItems() { + for (const item of this.items) { + const value = item.getAttribute('data-value') + + if (value) { + if (this.#selectedItems.has(value)) { + item.setAttribute(this.ariaSelectionType, 'true') + } + } + } + this.#updateInput() + } + + #addSelectedItem(item: SelectPanelItem) { + const button = item.querySelector('button')! + const value = item.getAttribute('data-value') + + if (value) { + this.#selectedItems.set(value, { + value, + label: button.querySelector('.ActionListItem-label')?.textContent?.trim(), + inputName: button.getAttribute('data-input-name'), + element: item, + }) + } + } + + #removeSelectedItem(item: Element) { + const value = item.getAttribute('data-value') + + if (value) { + this.#selectedItems.delete(value) + } + } + + #setTextFieldLoadingSpinnerTimer() { + if (this.#loadingDelayTimeoutId) clearTimeout(this.#loadingDelayTimeoutId) + if (this.#loadingAnnouncementTimeoutId) clearTimeout(this.#loadingAnnouncementTimeoutId) + + this.#loadingAnnouncementTimeoutId = setTimeout(() => { + announce('Loading', {element: this.ariaLiveContainer}) + }, 2000) as unknown as number + + this.#loadingDelayTimeoutId = setTimeout(() => { + this.#filterInputTextFieldElement.showLeadingSpinner() + }, 1000) as unknown as number + } + + handleEvent(event: Event) { + if (event.target === this.filterInputTextField) { + this.#handleSearchFieldEvent(event) + return + } + + if (event.target === this.remoteInput) { + this.#handleRemoteInputEvent(event) + return + } + + const targetIsInvoker = this.invokerElement?.contains(event.target as HTMLElement) + const targetIsCloseButton = this.closeButton?.contains(event.target as HTMLElement) + const eventIsActivation = this.#isActivation(event) + + if (targetIsInvoker && event.type === 'mousedown') { + return + } + + if (event.type === 'mousedown' && event.target instanceof HTMLInputElement) { + return + } + + // Prevent safari bug that dismisses menu on mousedown instead of allowing + // the click event to propagate to the button + if (event.type === 'mousedown') { + event.preventDefault() + return + } + + if (targetIsInvoker && eventIsActivation) { + this.#handleInvokerActivated(event) + return + } + + if (targetIsCloseButton && eventIsActivation) { + // #hide will automatically be called by dialog event triggered from `data-close-dialog-id` + return + } + + const item = (event.target as Element).closest(visibleMenuItemSelectors)?.parentElement as + | SelectPanelItem + | null + | undefined + + const targetIsItem = item !== null && item !== undefined + + if (targetIsItem && eventIsActivation) { + if (this.#potentiallyDisallowActivation(event)) return + + const dialogInvoker = item.closest('[data-show-dialog-id]') + + if (dialogInvoker) { + const dialog = this.ownerDocument.getElementById(dialogInvoker.getAttribute('data-show-dialog-id') || '') + + if (dialog && this.contains(dialogInvoker) && this.contains(dialog)) { + this.#handleDialogItemActivated(event, dialog) + return + } + } + + // Pressing the space key on a link will cause the page to scroll unless preventDefault() is called. + // We then click it manually to navigate. + if (this.#isAnchorActivationViaSpace(event)) { + event.preventDefault() + this.#getItemContent(item)?.click() + } + + this.#handleItemActivated(item) + + return + } + + if (event.type === 'click') { + const rect = this.dialog.getBoundingClientRect() + + const clickWasInsideDialog = + rect.top <= (event as MouseEvent).clientY && + (event as MouseEvent).clientY <= rect.top + rect.height && + rect.left <= (event as MouseEvent).clientX && + (event as MouseEvent).clientX <= rect.left + rect.width + + if (!clickWasInsideDialog) { + this.hide() + } + } + + // The include fragment will have been removed from the DOM by the time + // the include-fragment-replaced event has been dispatched, so we have to + // check for the type of the event target manually, since this.includeFragment + // will be null. + if (event.target instanceof IncludeFragmentElement) { + this.#handleIncludeFragmentEvent(event) + } + } + + #handleIncludeFragmentEvent(event: Event) { + switch (event.type) { + case 'include-fragment-replaced': { + this.#updateItemVisibility() + break + } + + case 'loadstart': { + this.#toggleIncludeFragmentElements(false) + break + } + + case 'loadend': { + this.dispatchEvent(new CustomEvent('loadend')) + break + } + + case 'error': { + this.#toggleIncludeFragmentElements(true) + + const errorElement = this.fragmentErrorElement + // check if the errorElement is visible in the dom + if (errorElement && !errorElement.hasAttribute('hidden')) { + announceFromElement(errorElement, {element: this.ariaLiveContainer, assertive: true}) + return + } + + break + } + } + } + + #toggleIncludeFragmentElements(showError: boolean) { + for (const el of this.includeFragment.querySelectorAll('[data-show-on-error]')) { + if (el instanceof HTMLElement) el.hidden = !showError + } + for (const el of this.includeFragment.querySelectorAll('[data-hide-on-error]')) { + if (el instanceof HTMLElement) el.hidden = showError + } + } + + #handleRemoteInputEvent(event: Event) { + switch (event.type) { + case 'remote-input-success': { + this.#clearErrorState() + this.#updateItemVisibility() + this.#checkSelectedItems() + break + } + + case 'remote-input-error': { + this.bodySpinner?.setAttribute('hidden', '') + + if (this.includeFragment || this.visibleItems.length === 0) { + this.#setErrorState(ErrorStateType.BODY) + } else { + this.#setErrorState(ErrorStateType.BANNER) + } + + break + } + + case 'loadstart': { + if (!this.#performFilteringLocally()) { + this.#clearErrorState() + this.bodySpinner?.removeAttribute('hidden') + + if (this.bodySpinner) break + this.#setTextFieldLoadingSpinnerTimer() + } + + break + } + + case 'loadend': { + this.#filterInputTextFieldElement.hideLeadingSpinner() + if (this.#loadingAnnouncementTimeoutId) clearTimeout(this.#loadingAnnouncementTimeoutId) + if (this.#loadingDelayTimeoutId) clearTimeout(this.#loadingDelayTimeoutId) + this.dispatchEvent(new CustomEvent('loadend')) + break + } + } + } + + #defaultFilterFn(item: HTMLElement, query: string) { + const text = (item.getAttribute('data-filter-string') || item.textContent || '').toLowerCase() + return text.indexOf(query.toLowerCase()) > -1 + } + + #handleSearchFieldEvent(event: Event) { + if (event.type === 'keydown' && (event as KeyboardEvent).key === 'ArrowDown') { + if (this.focusableItem) { + this.focusableItem.focus() + event.preventDefault() + } + } + if (event.type !== 'input') return + + // remote-input-element does not trigger another loadstart event if a request is + // already in-flight, so we use the input event on the text field to reset the + // loading spinner timer instead + if (!this.bodySpinner && !this.#performFilteringLocally()) { + this.#setTextFieldLoadingSpinnerTimer() + } + + if (this.#fetchStrategy === FetchStrategy.LOCAL || this.#fetchStrategy === FetchStrategy.EVENTUALLY_LOCAL) { + if (this.includeFragment) { + this.includeFragment.refetch() + return + } + + this.#updateItemVisibility() + } + } + + #updateItemVisibility() { + if (!this.list) return + + let atLeastOneResult = false + + if (this.#performFilteringLocally()) { + const query = this.filterInputTextField?.value ?? '' + const filter = this.filterFn || this.#defaultFilterFn + + for (const item of this.items) { + if (filter(item, query)) { + this.showItem(item) + atLeastOneResult = true + } else { + this.hideItem(item) + } + } + } else { + atLeastOneResult = this.items.length > 0 + } + + this.#updateTabIndices() + this.#maybeAnnounce() + + for (const item of this.items) { + const value = item.getAttribute('data-value') + + if (value && !this.#selectedItems.has(value) && this.isItemChecked(item)) { + this.#addSelectedItem(item) + } + } + + if (!this.noResults) return + + if (this.#inErrorState()) { + this.noResults.setAttribute('hidden', '') + return + } + + if (atLeastOneResult) { + this.noResults.setAttribute('hidden', '') + // TODO can we change this to search for `@panelId-list` + this.list?.querySelector('.ActionListWrap')?.removeAttribute('hidden') + } else { + this.list?.querySelector('.ActionListWrap')?.setAttribute('hidden', '') + this.noResults.removeAttribute('hidden') + } + } + + #inErrorState(): boolean { + if (this.fragmentErrorElement && !this.fragmentErrorElement.hasAttribute('hidden')) { + return true + } + + return !this.errorBannerElement.hasAttribute('hidden') + } + + #setErrorState(type: ErrorStateType) { + let errorElement = this.fragmentErrorElement + + if (type === ErrorStateType.BODY) { + this.fragmentErrorElement?.removeAttribute('hidden') + this.errorBannerElement.setAttribute('hidden', '') + } else { + errorElement = this.errorBannerElement + this.errorBannerElement?.removeAttribute('hidden') + this.fragmentErrorElement?.setAttribute('hidden', '') + } + + // check if the errorElement is visible in the dom + if (errorElement && !errorElement.hasAttribute('hidden')) { + announceFromElement(errorElement, {element: this.ariaLiveContainer, assertive: true}) + return + } + } + + #clearErrorState() { + this.fragmentErrorElement?.setAttribute('hidden', '') + this.errorBannerElement.setAttribute('hidden', '') + } + + #maybeAnnounce() { + if (this.open && this.list) { + const items = this.items + + if (items.length > 0) { + const instructions = 'tab for results' + announce(`${items.length} result${items.length === 1 ? '' : 's'} ${instructions}`, { + element: this.ariaLiveContainer, + }) + } else { + const noResultsEl = this.noResults + if (noResultsEl) { + announceFromElement(noResultsEl, {element: this.ariaLiveContainer}) + } + } + } + } + + get #fetchStrategy(): FetchStrategy { + if (!this.list) return FetchStrategy.REMOTE + + switch (this.list.getAttribute('data-fetch-strategy')) { + case 'local': + return FetchStrategy.LOCAL + case 'eventually_local': + return FetchStrategy.EVENTUALLY_LOCAL + default: + return FetchStrategy.REMOTE + } + } + + get #filterInputTextFieldElement(): PrimerTextFieldElement { + return this.filterInputTextField.closest('primer-text-field') as PrimerTextFieldElement + } + + #performFilteringLocally(): boolean { + return this.#fetchStrategy === FetchStrategy.LOCAL || this.#fetchStrategy === FetchStrategy.EVENTUALLY_LOCAL + } + + #handleInvokerActivated(event: Event) { + event.preventDefault() + + // eslint-disable-next-line no-restricted-syntax + event.stopPropagation() + + if (this.open) { + this.hide() + } else { + this.show() + } + } + + #handleDialogItemActivated(event: Event, dialog: HTMLElement) { + this.querySelector('.ActionListWrap')!.style.display = 'none' + const dialog_controller = new AbortController() + const {signal} = dialog_controller + const handleDialogClose = () => { + dialog_controller.abort() + this.querySelector('.ActionListWrap')!.style.display = '' + if (this.open) { + this.hide() + } + const activeElement = this.ownerDocument.activeElement + const lostFocus = this.ownerDocument.activeElement === this.ownerDocument.body + const focusInClosedMenu = this.contains(activeElement) + if (lostFocus || focusInClosedMenu) { + setTimeout(() => this.invokerElement?.focus(), 0) + } + } + // a modal element will close all popovers + dialog.addEventListener('close', handleDialogClose, {signal}) + dialog.addEventListener('cancel', handleDialogClose, {signal}) + } + + #handleItemActivated(item: SelectPanelItem) { + // Hide popover after current event loop to prevent changes in focus from + // altering the target of the event. Not doing this specifically affects + // tags. It causes the event to be sent to the currently focused element + // instead of the anchor, which effectively prevents navigation, i.e. it + // appears as if hitting enter does nothing. Curiously, clicking instead + // works fine. + if (this.selectVariant !== 'multiple') { + setTimeout(() => { + if (this.open) { + this.hide() + } + }) + } + + // The rest of the code below deals with single/multiple selection behavior, and should not + // interfere with events fired by menu items whose behavior is specified outside the library. + if (this.selectVariant !== 'multiple' && this.selectVariant !== 'single') return + + const checked = !this.isItemChecked(item) + + const activationSuccess = this.dispatchEvent( + new CustomEvent('beforeItemActivated', { + bubbles: true, + detail: {item, checked}, + cancelable: true, + }), + ) + + if (!activationSuccess) return + + const itemContent = this.#getItemContent(item) + + if (this.selectVariant === 'single') { + // Only check, never uncheck here. Single-select mode does not allow unchecking a checked item. + if (checked) { + this.#addSelectedItem(item) + itemContent?.setAttribute(this.ariaSelectionType, 'true') + } + + for (const checkedItem of this.querySelectorAll(`[${this.ariaSelectionType}]`)) { + if (checkedItem !== itemContent) { + this.#removeSelectedItem(checkedItem) + checkedItem.setAttribute(this.ariaSelectionType, 'false') + } + } + + this.#setDynamicLabel() + } else { + // multi-select mode allows unchecking a checked item + itemContent?.setAttribute(this.ariaSelectionType, `${checked}`) + + if (checked) { + this.#addSelectedItem(item) + } else { + this.#removeSelectedItem(item) + } + } + + this.#updateInput() + + this.dispatchEvent( + new CustomEvent('itemActivated', { + bubbles: true, + detail: {item, checked}, + }), + ) + } + + show() { + this.updateAnchorPosition() + this.dialog.showModal() + const event = new CustomEvent('dialog:open', { + detail: {dialog: this.dialog}, + }) + this.dispatchEvent(event) + } + + hide() { + this.dialog.close() + this.dispatchEvent( + new CustomEvent('panelClosed', { + detail: {panel: this}, + bubbles: true, + }), + ) + } + + #setDynamicLabel() { + if (!this.dynamicLabel) return + const invokerLabel = this.invokerLabel + if (!invokerLabel) return + this.#originalLabel ||= invokerLabel.textContent || '' + const itemLabel = + this.querySelector(`[${this.ariaSelectionType}=true] .ActionListItem-label`)?.textContent || this.#originalLabel + if (itemLabel) { + const prefixSpan = document.createElement('span') + prefixSpan.classList.add('color-fg-muted') + const contentSpan = document.createElement('span') + prefixSpan.textContent = `${this.dynamicLabelPrefix} ` + contentSpan.textContent = itemLabel + invokerLabel.replaceChildren(prefixSpan, contentSpan) + + if (this.dynamicAriaLabelPrefix) { + this.invokerElement?.setAttribute('aria-label', `${this.dynamicAriaLabelPrefix} ${itemLabel.trim()}`) + } + } else { + invokerLabel.textContent = this.#originalLabel + } + } + + #updateInput() { + if (this.selectVariant === 'single') { + const input = this.querySelector(`[data-list-inputs=true] input`) as HTMLInputElement + if (!input) return + + const selectedItem = this.selectedItems[0] + + if (selectedItem) { + input.value = (selectedItem.value || selectedItem.label || '').trim() + if (selectedItem.inputName) input.name = selectedItem.inputName + input.removeAttribute('disabled') + } else { + input.setAttribute('disabled', 'disabled') + } + } else if (this.selectVariant !== 'none') { + // multiple select variant + const inputList = this.querySelector('[data-list-inputs=true]') + if (!inputList) return + + const inputs = inputList.querySelectorAll('input') + + if (inputs.length > 0) { + this.#inputName ||= (inputs[0] as HTMLInputElement).name + } + + for (const selectedItem of this.selectedItems) { + const newInput = document.createElement('input') + newInput.setAttribute('data-list-input', 'true') + newInput.type = 'hidden' + newInput.autocomplete = 'off' + newInput.name = selectedItem.inputName || this.#inputName + newInput.value = (selectedItem.value || selectedItem.label || '').trim() + + inputList.append(newInput) + } + + for (const input of inputs) { + input.remove() + } + } + } + + get #firstItem(): SelectPanelItem | null { + return (this.querySelector(visibleMenuItemSelectors)?.parentElement || null) as SelectPanelItem | null + } + + get visibleItems(): SelectPanelItem[] { + return Array.from(this.querySelectorAll(visibleMenuItemSelectors)).map( + element => element.parentElement! as SelectPanelItem, + ) + } + + get items(): SelectPanelItem[] { + return Array.from(this.querySelectorAll(menuItemSelectors)).map( + element => element.parentElement! as SelectPanelItem, + ) + } + get focusableItem(): HTMLElement | undefined { + for (const item of this.items) { + const itemContent = this.#getItemContent(item) + if (!itemContent) continue + if (itemContent.getAttribute('tabindex') === '0') { + return itemContent + } + } + } + + getItemById(itemId: string): SelectPanelItem | null { + return this.querySelector(`li[data-item-id="${itemId}"`) + } + + isItemDisabled(item: SelectPanelItem | null): boolean { + if (item) { + return item.classList.contains('ActionListItem--disabled') + } else { + return false + } + } + + disableItem(item: SelectPanelItem | null) { + if (item) { + item.classList.add('ActionListItem--disabled') + this.#getItemContent(item)!.setAttribute('aria-disabled', 'true') + } + } + + enableItem(item: SelectPanelItem | null) { + if (item) { + item.classList.remove('ActionListItem--disabled') + this.#getItemContent(item)!.removeAttribute('aria-disabled') + } + } + + isItemHidden(item: SelectPanelItem | null): boolean { + if (item) { + return item.hasAttribute('hidden') + } else { + return false + } + } + + hideItem(item: SelectPanelItem | null) { + if (item) { + item.setAttribute('hidden', 'hidden') + } + } + + showItem(item: SelectPanelItem | null) { + if (item) { + item.removeAttribute('hidden') + } + } + + isItemChecked(item: SelectPanelItem | null) { + if (item) { + return this.#getItemContent(item)!.getAttribute(this.ariaSelectionType) === 'true' + } else { + return false + } + } + + checkItem(item: SelectPanelItem | null) { + if (item && (this.selectVariant === 'single' || this.selectVariant === 'multiple')) { + if (!this.isItemChecked(item)) { + this.#handleItemActivated(item) + } + } + } + + uncheckItem(item: SelectPanelItem | null) { + if (item && (this.selectVariant === 'single' || this.selectVariant === 'multiple')) { + if (this.isItemChecked(item)) { + this.#handleItemActivated(item) + } + } + } + + #getItemContent(item: SelectPanelItem): HTMLElement | null { + return item.querySelector('.ActionListContent') + } +} + +if (!window.customElements.get('select-panel')) { + window.SelectPanelElement = SelectPanelElement + window.customElements.define('select-panel', SelectPanelElement) +} + +declare global { + interface Window { + SelectPanelElement: typeof SelectPanelElement + } +} diff --git a/app/components/primer/aria_live.ts b/app/components/primer/aria_live.ts new file mode 100644 index 0000000000..dbc442be1a --- /dev/null +++ b/app/components/primer/aria_live.ts @@ -0,0 +1,41 @@ +const safeDocument = typeof document === 'undefined' ? undefined : document + +// Announce an element's text to the screen reader. +export function announceFromElement(el: HTMLElement, options?: {assertive?: boolean; element?: HTMLElement}) { + announce(getTextContent(el), options) +} + +// Announce message update to screen reader. +// Note: Use caution when using this function while a dialog is active. +// If the message is updated while the dialog is open, the screen reader may not announce the live region. +export function announce(message: string, options?: {assertive?: boolean; element?: HTMLElement}) { + const {assertive, element} = options ?? {} + + setContainerContent(message, assertive, element) +} + +// Set aria-live container to message. +function setContainerContent(message: string, assertive?: boolean, element?: HTMLElement) { + const getQuerySelector = () => { + return assertive ? '#js-global-screen-reader-notice-assertive' : '#js-global-screen-reader-notice' + } + const container = element ?? safeDocument?.querySelector(getQuerySelector()) + if (!container) return + if (container.textContent === message) { + /* This is a hack due to the way the aria live API works. + A screen reader will not read a live region again + if the text is the same. Adding a space character tells + the browser that the live region has updated, + which will cause it to read again, but with no audible difference. */ + container.textContent = `${message}\u00A0` + } else { + container.textContent = message + } +} + +// Gets the trimmed text content of an element. +function getTextContent(el: HTMLElement): string { + // innerText does not contain hidden text + /* eslint-disable-next-line github/no-innerText */ + return (el.getAttribute('aria-label') || el.innerText || '').trim() +} diff --git a/app/components/primer/primer.ts b/app/components/primer/primer.ts index bdb26be269..d4477de611 100644 --- a/app/components/primer/primer.ts +++ b/app/components/primer/primer.ts @@ -1,4 +1,5 @@ import '@github/include-fragment-element' +import '@github/remote-input-element' import './alpha/action_list' import './alpha/action_bar_element' import './alpha/dropdown' @@ -6,6 +7,8 @@ import './anchored_position' import './dialog_helper' import './focus_group' import './scrollable_region' +import './aria_live' +import './shared_events' import './alpha/image_crop' import './alpha/modal_dialog' import './beta/nav_list' @@ -22,3 +25,4 @@ import '../../../lib/primer/forms/primer_multi_input' import '../../../lib/primer/forms/primer_text_field' import '../../../lib/primer/forms/toggle_switch_input' import './alpha/action_menu/action_menu_element' +import './alpha/select_panel_element' diff --git a/app/components/primer/shared_events.ts b/app/components/primer/shared_events.ts new file mode 100644 index 0000000000..2fc70ad154 --- /dev/null +++ b/app/components/primer/shared_events.ts @@ -0,0 +1,10 @@ +export type ItemActivatedEvent = { + item: Element + checked: boolean +} + +declare global { + interface HTMLElementEventMap { + itemActivated: CustomEvent + } +} diff --git a/demo/app/controllers/auto_complete_test_controller.rb b/demo/app/controllers/auto_complete_test_controller.rb index 64c75006cd..4acf5849b3 100644 --- a/demo/app/controllers/auto_complete_test_controller.rb +++ b/demo/app/controllers/auto_complete_test_controller.rb @@ -35,5 +35,7 @@ def index ].select { |fruit| fruit.downcase.include?(params["q"].downcase) } @visual_type = params[:visual] @version = params[:version] + + render :index, formats: [:html, :html_fragment] end end diff --git a/demo/app/controllers/select_panel_items_controller.rb b/demo/app/controllers/select_panel_items_controller.rb new file mode 100644 index 0000000000..93bbb60d8b --- /dev/null +++ b/demo/app/controllers/select_panel_items_controller.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# :nodoc: +class SelectPanelItemsController < ApplicationController + SELECT_PANEL_ITEMS = [ + { value: 1, selected: true, title: "Phaser", description: "The iconic handheld laser beam" }, + { value: 2, title: "Photon torpedo", description: "Starship-mounted missile" }, + { value: 3, title: "Bat'leth", description: "The Klingon warrior's preferred means of achieving honor" }, + { value: 4, title: "Lightsaber", description: "An elegant weapon for a more civilized age", recent: true }, + { value: 5, title: "Proton pack", description: "Ghostbusting equipment" }, + { value: 6, title: "Sonic screwdriver", description: "The Time Lord's multi-purpose tool" }, + { value: 7, title: "Tricorder", description: "Handheld sensor device", recent: true }, + { value: 8, title: "TARDIS", description: "Time and relative dimension in space" } + ] + + COOKIE_PREFIX = "select-panel-seen-uuid-" + + def index + # delay a bit so loading spinners, etc can be seen + sleep 2 + + if params.fetch(:fail, "false") == "true" + uuid = params[:uuid] + + # use the uuid to succeed for the first request and fail for all subsequent requests + if !uuid || seen_uuid?(uuid) + render status: :internal_server_error, plain: "An error occurred" + return + end + + mark_seen_uuid(uuid) if uuid + end + + show_results = params.fetch(:show_results, "true") == "true" + query = (params[:q] || "").downcase + + results = if show_results + SELECT_PANEL_ITEMS.select do |item| + [item[:title], item[:description]].join(" ").downcase.include?(query) + end + else + [] + end + + clean_up_old_uuids(uuid) + + respond_to do |format| + format.any(:html, :html_fragment) do + render( + "select_panel_items/index", + locals: { results: results }, + layout: false, + formats: [:html, :html_fragment] + ) + end + end + end + + private + + def seen_uuid?(uuid) + cookies.has_key?(key_for(uuid)) + end + + def mark_seen_uuid(uuid) + cookies[key_for(uuid)] = "true" + end + + def clean_up_old_uuids(current_uuid) + current_key = key_for(current_uuid) + to_delete = [] + + cookies.each do |k, _| + if k.start_with?(COOKIE_PREFIX) && k != current_key + to_delete << k + end + end + + to_delete.each do |k| + cookies.delete(k) + end + end + + def key_for(uuid) + "#{COOKIE_PREFIX}#{uuid}" + end +end diff --git a/demo/app/views/select_panel_items/index.html.erb b/demo/app/views/select_panel_items/index.html.erb new file mode 100644 index 0000000000..47a3d0c3fc --- /dev/null +++ b/demo/app/views/select_panel_items/index.html.erb @@ -0,0 +1,17 @@ +<% unless results.empty? %> + <%= render(Primer::Alpha::SelectPanel::ItemList.new( + select_variant: :multiple, + p: 2, + role: "listbox", + aria: { + label: "options", + }, + )) do |list| %> + <% results.each do |result| %> + <% list.with_item(content_arguments: { data: { value: result[:value] } }, active: result[:selected] || false) do |item| %> + <% item.with_description { result[:description] } %> + <%= result[:title] %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/demo/config/initializers/mime_types.rb b/demo/config/initializers/mime_types.rb new file mode 100644 index 0000000000..8b82e17a62 --- /dev/null +++ b/demo/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# used by SelectPanel +Mime::Type.register "text/fragment+html", :html_fragment diff --git a/demo/config/routes.rb b/demo/config/routes.rb index 382f286e8c..e9e1529414 100644 --- a/demo/config/routes.rb +++ b/demo/config/routes.rb @@ -22,6 +22,7 @@ resources :toggle_switch, only: [:create] resources :nav_list_items, only: [:index] resources :multi, only: [:create] + resources :select_panel_items, only: [:index] # generic form submission path post "/form_handler", to: "form_handler#form_action", as: :generic_form_submission diff --git a/lib/primer/static/generate_info_arch.rb b/lib/primer/static/generate_info_arch.rb index 228db36305..ea2bd74306 100644 --- a/lib/primer/static/generate_info_arch.rb +++ b/lib/primer/static/generate_info_arch.rb @@ -37,7 +37,6 @@ def call if slot_method.base_docstring.to_s.present? render_erb_ignoring_markdown_code_fences(slot_method.base_docstring) end, - # rubocop:enable Style/IfUnlessModifier "parameters" => serialize_params(param_tags, component) } end @@ -54,11 +53,13 @@ def call method_docs = mtds.map do |mtd| param_tags = mtd.tags(:param) + return_tag = mtd.tags(:return) { "name" => mtd.name, "description" => render_erb_ignoring_markdown_code_fences(mtd.base_docstring), - "parameters" => serialize_params(param_tags, component) + "parameters" => serialize_params(param_tags, component), + "return_types" => return_tag.first&.types || [], } end diff --git a/lib/primer/yard/component_manifest.rb b/lib/primer/yard/component_manifest.rb index 2ee57271e4..deababbf07 100644 --- a/lib/primer/yard/component_manifest.rb +++ b/lib/primer/yard/component_manifest.rb @@ -67,6 +67,8 @@ class ComponentManifest Primer::Alpha::ToggleSwitch => { js: true }, Primer::Alpha::Overlay => { js: true }, Primer::Alpha::ActionMenu => { js: true }, + Primer::Alpha::SelectPanel => { js: true }, + Primer::Alpha::SelectPanel::ItemList => { js: true, examples: false }, # Examples can be seen in the NavList docs Primer::Alpha::NavList => { js: true }, diff --git a/package-lock.json b/package-lock.json index 0888bbf133..b36e32acf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,9 @@ "@github/clipboard-copy-element": "^1.3.0", "@github/details-menu-element": "^1.0.12", "@github/image-crop-element": "^5.0.0", - "@github/include-fragment-element": "^6.1.1", + "@github/include-fragment-element": "^6.3.0", "@github/relative-time-element": "^4.0.0", + "@github/remote-input-element": "^0.4.0", "@github/tab-container-element": "^3.1.2", "@oddbird/popover-polyfill": "^0.4.0", "@primer/behaviors": "^1.3.4" @@ -2198,6 +2199,11 @@ "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.2.tgz", "integrity": "sha512-wTXunu3hmuGljA5CHaaoUIKV0oI35wno0FKJl2yqKplTRnsCA5bPNj4bDeVIubkuskql6jwionWLlGM1Y6QLaw==" }, + "node_modules/@github/remote-input-element": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@github/remote-input-element/-/remote-input-element-0.4.0.tgz", + "integrity": "sha512-apsMwsFW24F+w2wzT8oKoBi9lpm6GeFOmtuL+1YwDVmIiwixfHOD3MnEsEOv0RwmHsMdWmIjP9mxWyTWPKZHGg==" + }, "node_modules/@github/tab-container-element": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@github/tab-container-element/-/tab-container-element-3.3.0.tgz", @@ -12383,6 +12389,11 @@ "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.2.tgz", "integrity": "sha512-wTXunu3hmuGljA5CHaaoUIKV0oI35wno0FKJl2yqKplTRnsCA5bPNj4bDeVIubkuskql6jwionWLlGM1Y6QLaw==" }, + "@github/remote-input-element": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@github/remote-input-element/-/remote-input-element-0.4.0.tgz", + "integrity": "sha512-apsMwsFW24F+w2wzT8oKoBi9lpm6GeFOmtuL+1YwDVmIiwixfHOD3MnEsEOv0RwmHsMdWmIjP9mxWyTWPKZHGg==" + }, "@github/tab-container-element": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@github/tab-container-element/-/tab-container-element-3.3.0.tgz", diff --git a/package.json b/package.json index 42e41a9ace..ac81981dde 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@github/image-crop-element": "^5.0.0", "@github/include-fragment-element": "^6.1.1", "@github/relative-time-element": "^4.0.0", + "@github/remote-input-element": "^0.4.0", "@github/tab-container-element": "^3.1.2", "@oddbird/popover-polyfill": "^0.4.0", "@primer/behaviors": "^1.3.4" diff --git a/previews/primer/alpha/select_panel_preview.rb b/previews/primer/alpha/select_panel_preview.rb new file mode 100644 index 0000000000..c5d0ce0fb2 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +module Primer + module Alpha + # @label SelectPanel + class SelectPanelPreview < ViewComponent::Preview + # @label Playground + # + # @param title text + # @param size [Symbol] select [auto, small, medium, medium_portrait, large, xlarge] + # @param simulate_failure toggle + # @param simulate_no_results toggle + # @param no_results_label text + # @param dynamic_label toggle + # @param dynamic_label_prefix text + # @param dynamic_aria_label_prefix text + # @param show_filter toggle + # @param open_on_load toggle + # @param anchor_align [Symbol] select [start, center, end] + # @param anchor_side [Symbol] select [outside_bottom, outside_top, outside_left, outside_right] + def playground( + title: "Sci-fi equipment", + size: :auto, + simulate_failure: false, + simulate_no_results: false, + no_results_label: "No results found", + dynamic_label: false, + dynamic_label_prefix: nil, + dynamic_aria_label_prefix: nil, + show_filter: true, + open_on_load: false, + anchor_align: :start, + anchor_side: :outside_bottom + ) + render_with_template(locals: { + system_arguments: { + title: title, + size: size, + simulate_failure: simulate_failure, + simulate_no_results: simulate_no_results, + no_results_label: no_results_label, + dynamic_label: dynamic_label, + dynamic_label_prefix: dynamic_label_prefix, + dynamic_aria_label_prefix: dynamic_aria_label_prefix, + show_filter: show_filter, + open_on_load: open_on_load, + anchor_align: anchor_align, + anchor_side: anchor_side + } + }) + end + + # @label Default + # + # @snapshot interactive + # @param open_on_load toggle + def default(open_on_load: false) + render_with_template(template: "primer/alpha/select_panel_preview/local_fetch", locals: { + open_on_load: open_on_load + }) + end + + # @!group Fetch strategies + + # @label Local fetch + # + # @snapshot interactive + # @param open_on_load toggle + def local_fetch(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Eventually local fetch + # + # @snapshot interactive + # @param open_on_load toggle + def eventually_local_fetch(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Remote fetch + # + # @snapshot interactive + # @param open_on_load toggle + def remote_fetch(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Local fetch (no results) + # + # @snapshot interactive + # @param open_on_load toggle + def local_fetch_no_results(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Eventually local fetch (no results) + # + # @snapshot interactive + # @param open_on_load toggle + def eventually_local_fetch_no_results(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Remote fetch (no results) + # + # @snapshot interactive + # @param open_on_load toggle + def remote_fetch_no_results(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @!endgroup + + # @label Single select + # + # @snapshot interactive + # @param dynamic_label toggle + # @param open_on_load toggle + def single_select(dynamic_label: false, open_on_load: false) + render_with_template(locals: { dynamic_label: dynamic_label, open_on_load: open_on_load }) + end + + # @label Multiselect + # + # @snapshot interactive + # @param open_on_load toggle + def multiselect(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @!group Dynamic label + + # @label With dynamic label + # + # @snapshot interactive + # @param open_on_load toggle + def with_dynamic_label(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label With dynamic label and aria prefix + # + # @snapshot interactive + # @param open_on_load toggle + def with_dynamic_label_and_aria_prefix(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @!endgroup + + # @label Footer buttons + # + # @snapshot interactive + # @param open_on_load toggle + def footer_buttons(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label With avatar items + # + # @snapshot interactive + # @param open_on_load toggle + def with_avatar_items(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @!group With icons + + # @label With leading icons + # + # @snapshot interactive + # @param open_on_load toggle + def with_leading_icons(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label With trailing icons + # + # @snapshot interactive + # @param open_on_load toggle + def with_trailing_icons(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @!endgroup + + # @label With subtitle + # + # @snapshot interactive + # @param open_on_load toggle + def with_subtitle(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Remote fetch initial failure + # + # @snapshot interactive + # @param open_on_load toggle + def remote_fetch_initial_failure(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Remote fetch filter failure + # + # @snapshot interactive + # @param open_on_load toggle + def remote_fetch_filter_failure(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + + # @label Eventually local fetch initial failure + # + # @snapshot interactive + # @param open_on_load toggle + def eventually_local_fetch_initial_failure(open_on_load: false) + render_with_template(locals: { open_on_load: open_on_load }) + end + end + end +end diff --git a/previews/primer/alpha/select_panel_preview/_interaction_subject_js.html.erb b/previews/primer/alpha/select_panel_preview/_interaction_subject_js.html.erb new file mode 100644 index 0000000000..b46d2e1d0f --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/_interaction_subject_js.html.erb @@ -0,0 +1,25 @@ + diff --git a/previews/primer/alpha/select_panel_preview/eventually_local_fetch.html.erb b/previews/primer/alpha/select_panel_preview/eventually_local_fetch.html.erb new file mode 100644 index 0000000000..605d22aa05 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/eventually_local_fetch.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path, + select_variant: :multiple, + fetch_strategy: :eventually_local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/eventually_local_fetch_initial_failure.html.erb b/previews/primer/alpha/select_panel_preview/eventually_local_fetch_initial_failure.html.erb new file mode 100644 index 0000000000..e3fc438902 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/eventually_local_fetch_initial_failure.html.erb @@ -0,0 +1,12 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path(fail: "true"), + fetch_strategy: :eventually_local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/eventually_local_fetch_no_results.html.erb b/previews/primer/alpha/select_panel_preview/eventually_local_fetch_no_results.html.erb new file mode 100644 index 0000000000..38211329e9 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/eventually_local_fetch_no_results.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path(show_results: "false"), + select_variant: :multiple, + fetch_strategy: :eventually_local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/footer_buttons.html.erb b/previews/primer/alpha/select_panel_preview/footer_buttons.html.erb new file mode 100644 index 0000000000..4dfce03e24 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/footer_buttons.html.erb @@ -0,0 +1,23 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :multiple, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + dynamic_aria_label_prefix: "Selected item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_footer(show_divider: true) do %> + <%= render(Primer::Beta::Button.new(scheme: :default)) { "Cancel" } %> + <%= render(Primer::Beta::Button.new(scheme: :primary)) { "Save" } %> + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/local_fetch.html.erb b/previews/primer/alpha/select_panel_preview/local_fetch.html.erb new file mode 100644 index 0000000000..a6847b9c59 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/local_fetch.html.erb @@ -0,0 +1,19 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :multiple, + fetch_strategy: :local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Panel" } %> + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/local_fetch_no_results.html.erb b/previews/primer/alpha/select_panel_preview/local_fetch_no_results.html.erb new file mode 100644 index 0000000000..a079440527 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/local_fetch_no_results.html.erb @@ -0,0 +1,15 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :multiple, + fetch_strategy: :local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Panel" } %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/multiselect.html.erb b/previews/primer/alpha/select_panel_preview/multiselect.html.erb new file mode 100644 index 0000000000..2b48c56996 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/multiselect.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :multiple, + fetch_strategy: :local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/playground.html.erb b/previews/primer/alpha/select_panel_preview/playground.html.erb new file mode 100644 index 0000000000..842e9a1a6d --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/playground.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> +<% title = system_arguments.delete(:title) %> +<% simulate_no_results = system_arguments.delete(:simulate_no_results) %> +<% simulate_failure = system_arguments.delete(:simulate_failure) %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path(show_results: !simulate_no_results, fail: simulate_failure), + select_variant: :single, + fetch_strategy: :remote, + **system_arguments +)) do |panel| %> + <% panel.with_show_button { title } %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/remote_fetch.html.erb b/previews/primer/alpha/select_panel_preview/remote_fetch.html.erb new file mode 100644 index 0000000000..6b36eeb18e --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/remote_fetch.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path, + select_variant: :multiple, + fetch_strategy: :remote, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/remote_fetch_filter_failure.html.erb b/previews/primer/alpha/select_panel_preview/remote_fetch_filter_failure.html.erb new file mode 100644 index 0000000000..962083bad0 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/remote_fetch_filter_failure.html.erb @@ -0,0 +1,13 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + # passing a uuid here causes the request to succeed the first time and fail all subsequent times + src: select_panel_items_path(fail: "true", uuid: SecureRandom.uuid), + fetch_strategy: :remote, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/remote_fetch_initial_failure.html.erb b/previews/primer/alpha/select_panel_preview/remote_fetch_initial_failure.html.erb new file mode 100644 index 0000000000..fa5fb15174 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/remote_fetch_initial_failure.html.erb @@ -0,0 +1,12 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path(fail: "true"), + fetch_strategy: :remote, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/remote_fetch_no_results.html.erb b/previews/primer/alpha/select_panel_preview/remote_fetch_no_results.html.erb new file mode 100644 index 0000000000..8079987f5e --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/remote_fetch_no_results.html.erb @@ -0,0 +1,16 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + src: select_panel_items_path(show_results: "false"), + select_variant: :multiple, + fetch_strategy: :remote, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Sci-fi equipment" } %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/single_select.html.erb b/previews/primer/alpha/select_panel_preview/single_select.html.erb new file mode 100644 index 0000000000..e62bfda34a --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/single_select.html.erb @@ -0,0 +1,19 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :single, + fetch_strategy: :local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_avatar_items.html.erb b/previews/primer/alpha/select_panel_preview/with_avatar_items.html.erb new file mode 100644 index 0000000000..f4c9a5ee24 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_avatar_items.html.erb @@ -0,0 +1,19 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + id: "with_avatar_items", + title: "Select users", + select_variant: :multiple, + fetch_strategy: :local, + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose users" } %> + + <% panel.with_avatar_item( + username: "hulk_smash", + src: "https://avatars.githubusercontent.com/u/103004183?v=4", + ) %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_dynamic_label.html.erb b/previews/primer/alpha/select_panel_preview/with_dynamic_label.html.erb new file mode 100644 index 0000000000..d8010aa017 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_dynamic_label.html.erb @@ -0,0 +1,23 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + id: "with_avatar_items", + title: "Select users", + select_variant: :single, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_item(label: "Item 5") %> + <% panel.with_item(label: "Item 6") %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_dynamic_label_and_aria_prefix.html.erb b/previews/primer/alpha/select_panel_preview/with_dynamic_label_and_aria_prefix.html.erb new file mode 100644 index 0000000000..792f4bf7f3 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_dynamic_label_and_aria_prefix.html.erb @@ -0,0 +1,24 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + id: "with_avatar_items", + title: "Select users", + select_variant: :single, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + dynamic_aria_label_prefix: "Your item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_item(label: "Item 5") %> + <% panel.with_item(label: "Item 6") %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_leading_icons.html.erb b/previews/primer/alpha/select_panel_preview/with_leading_icons.html.erb new file mode 100644 index 0000000000..435b11cccf --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_leading_icons.html.erb @@ -0,0 +1,31 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :single, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + dynamic_aria_label_prefix: "Selected item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + + <% panel.with_item(label: "Leading SVG visual") do |item| %> + <% item.with_leading_visual_svg do %> + + <% end %> + <% end %> + + <% panel.with_item(label: "Custom content") do |item| %> + <% item.with_leading_visual_content do %> + A + <% end %> + <% end %> + + <% panel.with_item(label: "Visual icons") do |item| %> + <% item.with_leading_visual_icon(icon: :star) %> + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_subtitle.html.erb b/previews/primer/alpha/select_panel_preview/with_subtitle.html.erb new file mode 100644 index 0000000000..ca9777c6c6 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_subtitle.html.erb @@ -0,0 +1,25 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :single, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + dynamic_aria_label_prefix: "Selected item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + <% panel.with_subtitle do %> + Choose the item you want to select + <% end %> + <% panel.with_item(label: "Item 1") %> + <% panel.with_item(label: "Item 2") %> + <% panel.with_item(label: "Item 3") %> + <% panel.with_item(label: "Item 4") %> + <% panel.with_footer(show_divider: true) do %> + I'm a footer! + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/previews/primer/alpha/select_panel_preview/with_trailing_icons.html.erb b/previews/primer/alpha/select_panel_preview/with_trailing_icons.html.erb new file mode 100644 index 0000000000..909da8cf90 --- /dev/null +++ b/previews/primer/alpha/select_panel_preview/with_trailing_icons.html.erb @@ -0,0 +1,19 @@ +<% subject_id = SecureRandom.hex %> + +<%= render(Primer::Alpha::SelectPanel.new( + data: { interaction_subject: subject_id }, + select_variant: :single, + fetch_strategy: :local, + dynamic_label: true, + dynamic_label_prefix: "Item", + dynamic_aria_label_prefix: "Selected item", + open_on_load: open_on_load +)) do |panel| %> + <% panel.with_show_button { "Choose item" } %> + + <% panel.with_item(label: "Visual icons") do |item| %> + <% item.with_trailing_visual_icon(icon: :star) %> + <% end %> +<% end %> + +<%= render partial: "primer/alpha/select_panel_preview/interaction_subject_js", locals: { subject_id: subject_id } %> diff --git a/test/components/alpha/select_panel_test.rb b/test/components/alpha/select_panel_test.rb new file mode 100644 index 0000000000..e5a5dbf9ee --- /dev/null +++ b/test/components/alpha/select_panel_test.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "components/test_helper" + +module Primer + module Alpha + class SelectPanelTest < Minitest::Test + include Primer::ComponentTestHelpers + + def test_renders_content + render_preview(:default) + + assert_selector("select-panel") do + assert_selector(".Overlay-headerFilter") do + assert_selector("primer-text-field") + end + end + end + + # This test is only here for coverage purposes and should be refactored + # when a full test suite is implemented. + def test_adds_src_attribute + render_preview(:remote_fetch) + + assert_selector("select-panel") do + assert_selector("remote-input[src]") + end + end + end + end +end diff --git a/test/components/component_test.rb b/test/components/component_test.rb index cb85b7609c..a364ca6aee 100644 --- a/test/components/component_test.rb +++ b/test/components/component_test.rb @@ -128,7 +128,8 @@ class PrimerComponentTest < Minitest::Test [Primer::Beta::NavList, { aria: { label: "Nav list" } }], [Primer::Alpha::Banner, {}], [Primer::Alpha::FormControl, { label: "Foo" }], - [Primer::Alpha::ActionMenu, {}, proc { |component| component.with_item(label: "Do something", value: "") }] + [Primer::Alpha::ActionMenu, {}, proc { |component| component.with_item(label: "Do something", value: "") }], + [Primer::Alpha::SelectPanel, {}] ].freeze def test_registered_components diff --git a/test/playwright/snapshots.test.ts b/test/playwright/snapshots.test.ts index 63ca3f370d..b4eb7bccdd 100644 --- a/test/playwright/snapshots.test.ts +++ b/test/playwright/snapshots.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-unused-vars */ -import {test, expect, Page} from '@playwright/test' +import {test, expect} from '@playwright/test' import {getPreviewURLs} from './helpers' import type {ComponentPreviews} from './helpers' @@ -22,7 +21,7 @@ const themes = [ 'dark', 'dark_dimmed', 'dark_high_contrast', - 'dark_colorblind' + 'dark_colorblind', ] test.describe('generate snapshots', () => { @@ -40,6 +39,11 @@ test.describe('generate snapshots', () => { await new Promise(resolve => setTimeout(resolve, 100)) await page.keyboard.press('Enter') + const subject = await page.evaluate(() => document.querySelector('[data-interaction-subject]')) + if (subject) { + await page.waitForSelector('[data-interaction-subject][data-ready=true]') + } + // Wait a bit for animations etc to resolve await new Promise(resolve => setTimeout(resolve, 100)) diff --git a/test/system/alpha/select_panel_test.rb b/test/system/alpha/select_panel_test.rb new file mode 100644 index 0000000000..e4c6d4db91 --- /dev/null +++ b/test/system/alpha/select_panel_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "system/test_case" + +module Alpha + class IntegrationSelectPanelTest < System::TestCase + ###### HELPER METHODS ###### + + def click_on_invoker_button(expect_to_open: true) + attempts = 0 + max_attempts = 3 + + begin + attempts += 1 + + find("select-panel button[aria-controls]").click + + if expect_to_open + assert_selector "dialog[open]" + end + + STDERR.puts "Succeeded" if attempts > 1 + rescue Minitest::Assertion => e + raise e if attempts >= max_attempts + STDERR.puts "Panel failed to open, retrying (attempt #{attempts} of #{max_attempts})" + retry + end + end + + ########## TESTS ############ + + def test_invoker_opens_panel + visit_preview(:default) + + refute_selector "select-panel dialog[open]" + click_on_invoker_button + assert_selector "select-panel dialog[open]" + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 41485e72da..85e8178365 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -31,3 +31,6 @@ require "primer/view_components" require File.expand_path("../demo/config/environment.rb", __dir__) + +# used by SelectPanel +Mime::Type.register "text/fragment+html", :html_fragment