diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-dark-high-contrast-linux.png index 66b071b08e9..6c9e29c92f5 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-light-high-contrast-linux.png index 341c9b75755..3740f4558e4 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Full-Variant-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-colorblind-linux.png index 68dafe1dd17..78bcafea137 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-dimmed-linux.png index b2719be8544..4ef9b156f59 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-high-contrast-linux.png index a0254e82cc9..524a6184769 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-linux.png index 68dafe1dd17..b37da39acdb 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-tritanopia-linux.png index 68dafe1dd17..78bcafea137 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-colorblind-linux.png index f05283352e0..43a1f22cac9 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-high-contrast-linux.png index 1f2c1b2b1a5..566d3d7b353 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-linux.png index f05283352e0..ac570cf2ec3 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-tritanopia-linux.png index f05283352e0..43a1f22cac9 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Inactive-Item-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-dark-high-contrast-linux.png index 929a0fd9238..04432c956fc 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-light-high-contrast-linux.png index 07c846b6f00..0bc9c8e20a1 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Simple-List-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-dark-high-contrast-linux.png index 929a0fd9238..04432c956fc 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-light-high-contrast-linux.png index 07c846b6f00..0bc9c8e20a1 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/ActionList-Single-Divider-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-colorblind-linux.png index 6775f209791..3978cfffa0b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-dimmed-linux.png index 3cfd6196573..0f9d0cc5621 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-high-contrast-linux.png index fd6ee4eac8f..e44c0adabfc 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-linux.png index 6775f209791..3978cfffa0b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-tritanopia-linux.png index 6775f209791..3978cfffa0b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-colorblind-linux.png index dbc7b626dbe..d6b20e92cae 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-high-contrast-linux.png index a90d04d7702..b05289fcd97 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-linux.png index dbc7b626dbe..d6b20e92cae 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-tritanopia-linux.png index dbc7b626dbe..d6b20e92cae 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Group-Heading-with-Classname-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png index ddd38f08acd..c35d8aa199d 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png index b50828b3e45..5964c04027b 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png index 37387f81f23..213f7b60742 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png index ddd38f08acd..c35d8aa199d 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png index ddd38f08acd..c35d8aa199d 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png index 1c47d858de1..53b577bddc1 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png index 82796c9cc19..4ca17804028 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png index 1c47d858de1..53b577bddc1 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png index 1c47d858de1..53b577bddc1 100644 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-colorblind-linux.png deleted file mode 100644 index 01a66d9a90d..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-dimmed-linux.png deleted file mode 100644 index 5c4fa12bd21..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-high-contrast-linux.png deleted file mode 100644 index 41c0d685b39..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-linux.png deleted file mode 100644 index 01a66d9a90d..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-tritanopia-linux.png deleted file mode 100644 index 01a66d9a90d..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-colorblind-linux.png deleted file mode 100644 index 9ef9de7f86f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-high-contrast-linux.png deleted file mode 100644 index 8609f3a02ed..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-linux.png deleted file mode 100644 index 9ef9de7f86f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-tritanopia-linux.png deleted file mode 100644 index 9ef9de7f86f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Link-Item-Options-light-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-colorblind-linux.png deleted file mode 100644 index e2b1086ba0b..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-dimmed-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-dimmed-linux.png deleted file mode 100644 index 4f73a62ef6c..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-dimmed-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-high-contrast-linux.png deleted file mode 100644 index dc493d626ba..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-linux.png deleted file mode 100644 index e2b1086ba0b..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-tritanopia-linux.png deleted file mode 100644 index e2b1086ba0b..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-dark-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-colorblind-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-colorblind-linux.png deleted file mode 100644 index 3ca414d532f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-colorblind-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-high-contrast-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-high-contrast-linux.png deleted file mode 100644 index 3b6cf5d88ec..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-high-contrast-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-linux.png deleted file mode 100644 index 3ca414d532f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-tritanopia-linux.png b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-tritanopia-linux.png deleted file mode 100644 index 3ca414d532f..00000000000 Binary files a/.playwright/snapshots/components/ActionList.test.ts-snapshots/Visuals-with-Classnames-light-tritanopia-linux.png and /dev/null differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-colorblind-linux.png index 3c68a3a188f..fcfeeafd6b4 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-dimmed-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-dimmed-linux.png index d41bafef5f2..8e976b35e98 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-dimmed-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-high-contrast-linux.png index d5e341bfb60..f92c69bfe36 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-linux.png index 3c68a3a188f..fcfeeafd6b4 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-tritanopia-linux.png index 3c68a3a188f..fcfeeafd6b4 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-colorblind-linux.png index bfc8bd6404a..8942c090b6e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-high-contrast-linux.png index 4b8afc32794..cd07fc19464 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-linux.png index bfc8bd6404a..8942c090b6e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-tritanopia-linux.png index bfc8bd6404a..8942c090b6e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Group-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-colorblind-linux.png index 775c5303598..927c94e60df 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-dimmed-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-dimmed-linux.png index ee62c7e371e..d7a78224dd0 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-dimmed-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-high-contrast-linux.png index 0559571be5a..2ceb1532cb4 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-linux.png index 775c5303598..927c94e60df 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-tritanopia-linux.png index 775c5303598..927c94e60df 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-colorblind-linux.png index a13e78de0cd..b8eb76b8174 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-high-contrast-linux.png index c392ed83263..0716506d4cb 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-linux.png index a13e78de0cd..b8eb76b8174 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-tritanopia-linux.png index a13e78de0cd..b8eb76b8174 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Sub-Items-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-colorblind-linux.png index 83a232b2233..29cecd47a61 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-dimmed-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-dimmed-linux.png index 0b8314507cf..0fb86842c3e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-dimmed-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-high-contrast-linux.png index 0367d592b76..a6ac979ee28 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-linux.png index 83a232b2233..29cecd47a61 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-tritanopia-linux.png index 83a232b2233..29cecd47a61 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-colorblind-linux.png index a680da6b2b0..83569c11c8e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-colorblind-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-high-contrast-linux.png index 7f8b2bb09b8..aec058854c4 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-high-contrast-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-linux.png index a680da6b2b0..83569c11c8e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-tritanopia-linux.png index a680da6b2b0..83569c11c8e 100644 Binary files a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-tritanopia-linux.png and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Title-and-Heading-light-tritanopia-linux.png differ diff --git a/e2e/components/ActionList.test.ts b/e2e/components/ActionList.test.ts index 65ad8745e91..3da80621f8a 100644 --- a/e2e/components/ActionList.test.ts +++ b/e2e/components/ActionList.test.ts @@ -313,38 +313,33 @@ test.describe('ActionList', () => { } }) - // removing this temporarily as there is a slight diff betqeen default and enabled CSS feature flag that feels like a non-issue - // eslint-disable-next-line jest/no-commented-out-tests - // test.describe('Text Wrap And Truncation', () => { - // for (const theme of themes) { - // eslint-disable-next-line jest/no-commented-out-tests - // test.describe(theme, () => { - // eslint-disable-next-line jest/no-commented-out-tests - // test('default @vrt', async ({page}) => { - // await visit(page, { - // id: 'components-actionlist-features--text-wrap-and-truncation', - // globals: { - // colorScheme: theme, - // }, - // }) - - // // Default state - // expect(await page.screenshot()).toMatchSnapshot(`ActionList.Text Wrap And Truncation.${theme}.png`) - // }) - - // eslint-disable-next-line jest/no-commented-out-tests - // test('axe @aat', async ({page}) => { - // await visit(page, { - // id: 'components-actionlist-features--text-wrap-and-truncation', - // globals: { - // colorScheme: theme, - // }, - // }) - // await expect(page).toHaveNoViolations() - // }) - // }) - // } - // }) + test.describe('Text Wrap And Truncation', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-actionlist-features--text-wrap-and-truncation', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`ActionList.Text Wrap And Truncation.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-actionlist-features--text-wrap-and-truncation', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + }) test.describe('With Avatars', () => { for (const theme of themes) { @@ -745,60 +740,4 @@ test.describe('ActionList', () => { }) } }) - - test.describe('Visuals with Classnames', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-actionlist-dev--visual-custom-classname', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Visuals with Classnames.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-actionlist-dev--visual-custom-classname', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) - - test.describe('Link Item Options', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-actionlist-examples--list-link-item', - globals: { - colorScheme: theme, - }, - }) - - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Link Item Options.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-actionlist-examples--list-link-item', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) }) diff --git a/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css b/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css deleted file mode 100644 index 515947bacc7..00000000000 --- a/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css +++ /dev/null @@ -1,11 +0,0 @@ -@define-mixin activeIndicatorLine { - position: absolute; - top: calc(50% - var(--base-size-12)); - left: calc(-1 * var(--base-size-8)); - width: var(--base-size-4); - height: var(--base-size-24); - content: ''; - /* stylelint-disable-next-line primer/colors */ - background: var(--borderColor-accent-emphasis); - border-radius: var(--borderRadius-medium); -} diff --git a/packages/react/src/ActionList/ActionList.dev.stories.tsx b/packages/react/src/ActionList/ActionList.dev.stories.tsx index 70faab969be..35c37cf213f 100644 --- a/packages/react/src/ActionList/ActionList.dev.stories.tsx +++ b/packages/react/src/ActionList/ActionList.dev.stories.tsx @@ -7,7 +7,6 @@ import {Group} from './Group' import {Divider} from './Divider' import {Description} from './Description' import Avatar from '../Avatar' -import {FileDirectoryIcon, HeartFillIcon} from '@primer/octicons-react' export default { title: 'Components/ActionList/Dev', @@ -145,26 +144,3 @@ export const HeadingCustomClassname = () => ( ) - -export const DescriptionCustomClassname = () => ( - - - Label - This is a description - - -) - -export const VisualCustomClassname = () => ( - - - Label - - - - - - - - -) diff --git a/packages/react/src/ActionList/ActionList.examples.stories.tsx b/packages/react/src/ActionList/ActionList.examples.stories.tsx index 8b7da25fe97..bf6562f38d2 100644 --- a/packages/react/src/ActionList/ActionList.examples.stories.tsx +++ b/packages/react/src/ActionList/ActionList.examples.stories.tsx @@ -94,21 +94,9 @@ export const ListLinkItem = () => ( - With inline description + ActionList.LinkItem with everything inline description - - - - - - With block description Block description - - - - - - Trailing visual ⌘ + L diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 9666be99f04..0f3e563ffa0 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable max-nesting-depth, selector-max-specificity, selector-max-compound-selectors */ +/* stylelint-disable selector-max-specificity, selector-max-compound-selectors */ .ActionList { padding: 0; @@ -12,24 +12,16 @@ } &:where([data-variant='inset']) { + /* change to padding (all) when Item is converted */ padding-block: var(--base-size-8); - - /* this is only to match default experience */ - & .ActionListItem { - margin-inline: var(--base-size-8); - } } &:where([data-dividers='true']) { /* place dividers on the wrapper that excludes leading visuals/actions */ & .ActionListSubContent::before { position: absolute; - - /* use this top size after FF removed */ - - /* top: calc(-1 * var(--control-medium-paddingBlock)); */ /* stylelint-disable-next-line primer/spacing */ - top: -7px; + top: calc(-1 * var(--control-medium-paddingBlock)); display: block; width: 100%; height: 1px; @@ -42,12 +34,8 @@ & [data-description-variant='inline'] { &::before { position: absolute; - - /* use this top size after FF removed */ - - /* top: calc(-1 * var(--control-medium-paddingBlock)); */ /* stylelint-disable-next-line primer/spacing */ - top: -7px; + top: calc(-1 * var(--control-medium-paddingBlock)); display: block; width: 100%; height: var(--borderWidth-thin); @@ -74,563 +62,6 @@ visibility: hidden; } } - - /* Make sure that the first visible item isn't a divider */ - & .Divider:first-child { - display: none; - } -} - -/* ActionListItem is a li that handles visual state, while ActionListItemContent controls actual state via button or link */ - -.ActionListItem { - position: relative; - list-style: none; - background-color: var(--control-transparent-bgColor-rest); - border-radius: var(--borderRadius-medium); - - /* apply flex if trailing action exists as an immediate child */ - &:has(> .TrailingAction) { - display: flex; - flex-wrap: nowrap; - } - - /* state */ - - &:not(:has([aria-disabled], [disabled]), [aria-disabled='true'], [data-has-subitem='true']) { - @media (hover: hover) { - &:hover, - &:active { - cursor: pointer; - } - - &:hover { - background-color: var(--control-transparent-bgColor-hover); - - &:not([data-active], :focus-visible) { - /* Support for "Windows high contrast mode" https:sarahmhigley.com/writing/whcm-quick-tips/ */ - outline: solid var(--borderWidth-thin) transparent; - outline-offset: calc(-1 * var(--borderWidth-thin)); - box-shadow: var(--boxShadow-thin) var(--control-transparent-borderColor-active); - } - } - } - - &:active { - background-color: var(--control-transparent-bgColor-active); - - &:not([data-active]) { - /* Support for "Windows high contrast mode" https:sarahmhigley.com/writing/whcm-quick-tips/ */ - outline: solid var(--borderWidth-thin) transparent; - outline-offset: calc(-1 * var(--borderWidth-thin)); - box-shadow: var(--boxShadow-thin) var(--control-transparent-borderColor-active); - } - } - - &:focus-visible { - @mixin focusOutline 0; - - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } - } - - /* danger */ - &:where([data-variant='danger']) { - & * :not([popover], .TrailingVisual) { - color: var(--control-danger-fgColor-rest); - } - - @media (hover: hover) { - &:hover { - background: var(--control-danger-bgColor-hover); - - & * :not([popover]) { - color: var(--control-danger-fgColor-hover); - } - } - } - - &:active { - background: var(--control-danger-bgColor-active); - - & * :not([popover]) { - color: var(--control-danger-fgColor-hover); - } - } - } - - /* active state [aria-current] */ - &:where([data-active]) { - background: var(--control-transparent-bgColor-selected); - - /* provides a visual indication of the current item for Windows high-contrast mode */ - outline: 2px solid transparent; - - & .ItemLabel { - font-weight: var(--base-text-weight-semibold); - color: var(--control-fgColor-rest); - } - - @media (hover: hover) { - &:hover { - background-color: var(--control-transparent-bgColor-hover); - } - } - - /* hide dividers if showDividers is true and item is active */ - - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } - - /* blue accent line */ - &::after { - @mixin activeIndicatorLine; - } - } - - &:where([data-is-active-descendant]) { - background: var(--control-transparent-bgColor-selected); - - /* provides a visual indication of the current item for Windows high-contrast mode */ - outline: 2px solid transparent; - - /* hide dividers if showDividers is true and item is active */ - - /* add back in after FF ship */ - - /* & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } */ - - /* blue accent line */ - &::after { - @mixin activeIndicatorLine; - } - } - - /* inactive */ - &:where([data-inactive='true']) { - /* ignore tooltip */ - & * :not([popover], .InactiveWarning) { - color: var(--fgColor-muted); - } - - @media (hover: hover) { - &:hover { - cursor: not-allowed; - background-color: transparent; - - & * :not([popover], .InactiveWarning) { - color: var(--fgColor-muted); - } - } - } - - &:active { - background: transparent; - } - } - - &:where([data-loading='true']), - &:has([data-loading='true']) { - & * { - color: var(--fgColor-muted); - } - } - - /* hide dividers */ - @media (hover: hover) { - &:hover { - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } - - & [data-description-variant='inline']::before, - & + .ActionListItem [data-description-variant='inline']::before { - visibility: hidden; - } - } - } - } - - /* if item has subitem, move hover styles to ActionListContent */ - &[data-has-subitem='true'] { - /* first child */ - & > .ActionListContent { - z-index: 1; - - @media (hover: hover) { - &:hover { - cursor: pointer; - background-color: var(--control-transparent-bgColor-hover); - } - } - - &:active { - background-color: var(--control-transparent-bgColor-active); - } - } - - & .Spacer { - display: block; - } - } - - /* disabled */ - - &[aria-disabled='true'], - &:has([aria-disabled='true'], [disabled]) { - & .ActionListContent * { - color: var(--control-fgColor-disabled); - } - - & .ActionListContent { - @media (hover: hover) { - &:hover { - cursor: not-allowed; - background-color: transparent; - } - } - } - - @media (hover: hover) { - &:hover { - background-color: transparent; - } - } - - & .MultiSelectCheckbox { - background-color: var(--control-bgColor-disabled); - border-color: var(--control-borderColor-disabled); - } - - &[aria-checked='true'], - &[aria-selected='true'] { - & .MultiSelectCheckbox { - background-color: var(--control-checked-bgColor-disabled); - /* stylelint-disable-next-line primer/colors */ - border-color: var(--control-checked-bgColor-disabled); - - &::before { - /* stylelint-disable-next-line primer/colors */ - background-color: var(--control-checked-fgColor-disabled); - } - } - } - } - - /* Make sure that the first visible item isn't a divider */ - &[aria-hidden] + .Divider { - display: none; - } - - /* - * checkbox item [aria-checked] - * listbox [aria-selected] - */ - - & .MultiSelectCheckbox { - position: relative; - display: grid; - width: var(--base-size-16); - height: var(--base-size-16); - margin: 0; - cursor: pointer; - background-color: var(--bgColor-default); - border: var(--borderWidth-thin) solid var(--control-borderColor-emphasis); - border-radius: var(--borderRadius-small); - transition: - background-color, - border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */ - - place-content: center; - - &::before { - width: var(--base-size-16); - height: var(--base-size-16); - content: ''; - /* stylelint-disable-next-line primer/colors */ - background-color: var(--control-checked-fgColor-rest); - transition: visibility 0s linear 230ms; - clip-path: inset(var(--base-size-16) 0 0 0); - - /* octicon checkmark image */ - mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); - mask-size: 75%; - mask-repeat: no-repeat; - mask-position: center; - animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1); /* forwards; slightly snappier animation out */ - } - } - - &[aria-checked='true'], - &[aria-selected='true'] { - & .MultiSelectCheckbox { - background-color: var(--control-checked-bgColor-rest); - border-color: var(--control-checked-borderColor-rest); - transition: - background-color, - border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */ - - &::before { - visibility: visible; - transition: visibility 0s linear 0s; - animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; - } - } - - & .SingleSelectCheckmark { - visibility: visible; - } - } - - &[aria-checked='false'], - &[aria-selected='false'] { - & .MultiSelectCheckbox { - &::before { - visibility: hidden; - } - } - - & .SingleSelectCheckmark { - visibility: hidden; - } - } -} - -/* hide by default to support inactive state where role cannot be menuitemradio or menuitemcheckbox */ -.SingleSelectCheckmark { - visibility: hidden; -} - -/* button or a tag */ - -/* [ [spacer] [leadingAction] [leadingVisual] [content] ] */ -.ActionListContent { - --subitem-depth: 0px; - - position: relative; - display: grid; - width: 100%; - color: var(--control-fgColor-rest); - text-align: left; - user-select: none; - background-color: transparent; - border: none; - border-radius: var(--borderRadius-medium); - transition: background 33.333ms linear; - /* stylelint-disable-next-line primer/spacing */ - padding-block: var(--control-medium-paddingBlock); - /* stylelint-disable-next-line primer/spacing */ - padding-inline: var(--control-medium-paddingInline-condensed); - touch-action: manipulation; - -webkit-tap-highlight-color: transparent; - grid-template-rows: min-content; - grid-template-areas: 'spacer leadingAction leadingVisual content'; - grid-template-columns: min-content min-content min-content minmax(0, auto); - align-items: start; - - /* column-gap persists with empty grid-areas, margin applies only when children exist */ - & > :not(:last-child, .Spacer) { - /* stylelint-disable-next-line primer/spacing */ - margin-right: var(--control-medium-gap); - } - - &:hover { - text-decoration: none; - cursor: pointer; - } - - /* collapsible item [aria-expanded] */ - - &[aria-expanded='true'] { - & .ExpandIcon { - transform: scaleY(-1); - } - - &.ActionListContent--hasActiveSubItem { - & > .ItemLabel { - font-weight: var(--base-text-weight-semibold); - } - } - } - - &[aria-expanded='false'] { - & .ExpandIcon { - transform: scaleY(1); - } - - /* show active indicator on parent collapse if child is active */ - &:has(+ .SubGroup [data-active='true']) { - background: var(--control-transparent-bgColor-selected); - - & .ItemLabel { - font-weight: var(--base-text-weight-semibold); - } - - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } - - /* blue accent line */ - &::after { - @mixin activeIndicatorLine; - } - } - } -} - -/* [ [content] [trailingVisual] [trailingAction] ] */ -.ActionListSubContent { - grid-area: content; - position: relative; - display: grid; - width: 100%; - grid-template-rows: min-content; - grid-template-areas: 'label trailingVisual trailingAction'; - grid-template-columns: minmax(0, auto) min-content min-content; - align-items: start; -} - -/* place children on grid */ - -/* spacer used to create depth for nested lists */ - -.Spacer { - display: none; - width: max(0px, var(--subitem-depth) * 8px); - grid-area: spacer; -} - -.LeadingAction { - grid-area: leadingAction; -} - -.LeadingVisual { - grid-area: leadingVisual; -} - -.TrailingVisual { - grid-area: trailingVisual; - font-size: var(--text-body-size-medium); -} - -.TrailingAction { - grid-area: trailingAction; -} - -/* wrapper span -default block */ -.ItemDescriptionWrap { - grid-area: label; - display: flex; - flex-direction: column; - gap: var(--base-size-4); - - & .ItemLabel { - font-weight: var(--base-text-weight-semibold); - word-break: break-word; - } - - /* inline */ - &:where([data-description-variant='inline']) { - position: relative; - word-break: normal; - flex-direction: row; - align-items: baseline; - gap: var(--base-size-8); - - & .ItemLabel { - word-break: normal; - } - - &:has([data-truncate='true']) { - & .ItemLabel { - flex: 1 0 auto; - } - } - - & .Description { - /* adjust line-height for baseline alignment */ - - /* line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; - } - } -} - -/* description */ -.Description { - font-size: var(--text-body-size-small); - font-weight: var(--base-text-weight-normal); - - /* line-height: var(--text-caption-lineHeight); */ - - /* remove after FF ships */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; - color: var(--fgColor-muted); -} - -/* helper for grid alignment with multi-line content -span wrapping svg or text */ -.VisualWrap { - display: flex; - min-width: max-content; - min-height: var(--control-medium-lineBoxHeight); - /* stylelint-disable-next-line primer/typography */ - line-height: 20px; /* temporary until we fix line-height rounding in primitives */ - color: var(--fgColor-muted); - pointer-events: none; - fill: var(--fgColor-muted); - align-items: center; -} - -/* text */ -.ItemLabel { - position: relative; - font-size: var(--text-body-size-medium); - font-weight: var(--base-text-weight-normal); - /* stylelint-disable-next-line primer/typography */ - line-height: 20px; /* temporary until we fix line-height rounding in primitives */ - color: var(--fgColor-default); - grid-area: label; - word-break: break-word; -} - -.SubGroup { - & .ItemLabel { - font-size: var(--text-body-size-small); - } - - & .ActionListItem { - margin-inline: 0; - } -} - -/* trailing action icon button */ - -.TrailingActionButton { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.InactiveButtonWrap { - &:has(.TrailingVisual) { - grid-area: trailingVisual; - } - - &:has(.LeadingVisual) { - grid-area: leadingVisual; - } } .Divider { @@ -640,50 +71,9 @@ span wrapping svg or text */ /* stylelint-disable-next-line primer/spacing */ margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); margin-block-end: var(--base-size-8); + margin-inline: calc(-1 * var(--base-size-8)); list-style: none; /* stylelint-disable-next-line primer/colors */ background: var(--borderColor-muted); border: 0; } - -.InactiveButtonReset { - display: flex; - padding: 0; - font: inherit; - color: inherit; - cursor: pointer; - background: none; - border: none; -} - -.InactiveWarning { - font-size: var(--text-body-size-small); - - /* line-height: var(--text-caption-lineHeight); */ - - /* use variable when FF removed */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; - color: var(--fgColor-attention); - grid-row: 2/2; -} - -@keyframes checkmarkIn { - from { - clip-path: inset(var(--base-size-16) 0 0 0); - } - - to { - clip-path: inset(0 0 0 0); - } -} - -@keyframes checkmarkOut { - from { - clip-path: inset(0 0 0 0); - } - - to { - clip-path: inset(var(--base-size-16) 0 0 0); - } -} diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index fe987278e34..11ce02d53fc 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -1,11 +1,12 @@ -import {render as HTMLRender} from '@testing-library/react' +import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' import userEvent from '@testing-library/user-event' import axe from 'axe-core' import React from 'react' import theme from '../theme' import {ActionList} from '.' +import {BookIcon} from '@primer/octicons-react' import {behavesAsComponent, checkExports} from '../utils/testing' -import {BaseStyles, ThemeProvider} from '..' +import {BaseStyles, ThemeProvider, ActionMenu} from '..' import {FeatureFlags} from '../FeatureFlags' function SimpleActionList(): JSX.Element { @@ -27,6 +28,41 @@ function SimpleActionList(): JSX.Element { ) } +const projects = [ + {name: 'Primer Backlog', scope: 'GitHub'}, + {name: 'Primer React', scope: 'github/primer'}, + {name: 'Disabled Project', scope: 'github/primer', disabled: true}, + {name: 'Inactive Project', scope: 'github/primer', inactiveText: 'Unavailable due to an outage'}, + {name: 'Loading Project', scope: 'github/primer', loading: true}, + { + name: 'Inactive and Loading Project', + scope: 'github/primer', + loading: true, + inactiveText: 'Unavailable due to an outage, but loading still passed', + }, +] +function SingleSelectListStory(): JSX.Element { + const [selectedIndex, setSelectedIndex] = React.useState(0) + + return ( + + {projects.map((project, index) => ( + setSelectedIndex(index)} + disabled={project.disabled} + inactiveText={project.inactiveText} + loading={project.loading} + > + {project.name} + + ))} + + ) +} + describe('ActionList', () => { behavesAsComponent({ Component: ActionList, @@ -39,12 +75,100 @@ describe('ActionList', () => { ActionList, }) + it('should have aria-keyshortcuts applied to the correct element', async () => { + const {container} = HTMLRender() + + const linkOptions = await waitFor(() => container.querySelectorAll('a')) + + expect(linkOptions[0]).toHaveAttribute('aria-keyshortcuts', 'd') + expect(linkOptions[0].parentElement).not.toHaveAttribute('aria-keyshortcuts', 'd') + }) + it('should have no axe violations', async () => { const {container} = HTMLRender() const results = await axe.run(container) expect(results).toHaveNoViolations() }) + it('should fire onSelect on click and keypress', async () => { + const component = HTMLRender() + const options = await waitFor(() => component.getAllByRole('option')) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[1]).toHaveAttribute('aria-selected', 'false') + + fireEvent.click(options[1]) + + expect(options[0]).toHaveAttribute('aria-selected', 'false') + expect(options[1]).toHaveAttribute('aria-selected', 'true') + + // We pass keycode here to navigate a implementation detail in react-testing-library + // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 + fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13}) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[1]).toHaveAttribute('aria-selected', 'false') + + fireEvent.keyPress(options[1], {key: ' ', charCode: 32}) + + expect(options[0]).toHaveAttribute('aria-selected', 'false') + expect(options[1]).toHaveAttribute('aria-selected', 'true') + }) + + it('should skip onSelect on disabled items', async () => { + const component = HTMLRender() + const options = await waitFor(() => component.getAllByRole('option')) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[2]).toHaveAttribute('aria-selected', 'false') + + fireEvent.click(options[2]) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[2]).toHaveAttribute('aria-selected', 'false') + + fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13}) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[2]).toHaveAttribute('aria-selected', 'false') + }) + + it('should skip onSelect on inactive items', async () => { + const component = HTMLRender() + const options = await waitFor(() => component.getAllByRole('option')) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[3]).toHaveAttribute('aria-selected', 'false') + + fireEvent.click(options[3]) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[3]).toHaveAttribute('aria-selected', 'false') + + fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[3]).toHaveAttribute('aria-selected', 'false') + }) + + it('should skip onSelect on loading items', async () => { + const component = HTMLRender() + const options = await waitFor(() => component.getAllByRole('option')) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[4]).toHaveAttribute('aria-selected', 'false') + + fireEvent.click(options[4]) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[4]).toHaveAttribute('aria-selected', 'false') + + fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) + + expect(options[0]).toHaveAttribute('aria-selected', 'true') + expect(options[4]).toHaveAttribute('aria-selected', 'false') + }) + it('should throw when selected is provided without a selectionVariant on parent', async () => { // we expect console.error to be called, so we suppress that in the test const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) @@ -62,6 +186,391 @@ describe('ActionList', () => { mockError.mockRestore() }) + it('should not crash when clicking an item without an onSelect', async () => { + const component = HTMLRender( + + Primer React + , + ) + const option = await waitFor(() => component.getByRole('option')) + expect(option).toBeInTheDocument() + + fireEvent.click(option) + fireEvent.keyPress(option, {key: 'Enter', charCode: 13}) + expect(option).toBeInTheDocument() + }) + + it('should focus the button around the leading visual when tabbing to an inactive item', async () => { + const component = HTMLRender() + const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[3].inactiveText})) + + await userEvent.tab() // get focus on first element + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + expect(inactiveOptionButton).toHaveFocus() + }) + + it('should behave as inactive if both inactiveText and loading props are passed', async () => { + const component = HTMLRender() + const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[5].inactiveText})) + + await userEvent.tab() // get focus on first element + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + await userEvent.keyboard('{ArrowDown}') + + expect(inactiveOptionButton).toHaveFocus() + }) + + it('should call onClick for a link item', async () => { + const onClick = jest.fn() + const component = HTMLRender( + + + Primer React + + , + ) + const link = await waitFor(() => component.getByRole('link')) + fireEvent.click(link) + expect(onClick).toHaveBeenCalled() + }) + + it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + expect(() => + HTMLRender( + + + + Trigger + + + + Group Heading + + + + + + , + ), + ).toThrow( + "Looks like you are trying to set a heading level to a menu role. Group headings for menu type action lists are for representational purposes, and rendered as divs. Therefore they don't need a heading level.", + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) + + it('should render the ActionList.GroupHeading component as a heading with the given heading level', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const heading = container.getByRole('heading', {level: 2}) + expect(heading).toBeInTheDocument() + expect(heading).toHaveTextContent('Group Heading') + }) + it('should throw an error if ActionList.GroupHeading is used without an `as` prop when no role is specified (for list role)', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + expect(() => + HTMLRender( + + Heading + + Group Heading + Item + + , + ), + ).toThrow( + "You are setting a heading for a list, that requires a heading level. Please use 'as' prop to set a proper heading level.", + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) + it('should render the ActionList.GroupHeading component as a span (not a heading tag) when role is specified as listbox', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const label = container.getByText('Group Heading') + expect(label).toBeInTheDocument() + expect(label.tagName).toEqual('SPAN') + }) + it('should render the ActionList.GroupHeading component as a span with role="presentation" and aria-hidden="true" when role is specified as listbox', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const label = container.getByText('Group Heading') + const wrapper = label.parentElement + expect(wrapper).toHaveAttribute('role', 'presentation') + expect(wrapper).toHaveAttribute('aria-hidden', 'true') + }) + it('should label the list with the group heading id', async () => { + const {container, getByText} = HTMLRender( + + Heading + + Group Heading + Item + + , + ) + const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) + const heading = getByText('Group Heading') + expect(list).toHaveAttribute('aria-labelledby', heading.id) + }) + it('should NOT label the list with the group heading id when role is specified', async () => { + const {container, getByText} = HTMLRender( + + Heading + + Group Heading + Item + + , + ) + const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) + const heading = getByText('Group Heading') + expect(list).not.toHaveAttribute('aria-labelledby', heading.id) + }) + it('should label the list using aria-label when role is specified', async () => { + const {container, getByText} = HTMLRender( + + Heading + + Group Heading + Item + + , + ) + const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) + const heading = getByText('Group Heading') + expect(list).toHaveAttribute('aria-label', heading.textContent) + }) + + it('should render ActionList.Item as button when feature flag is enabled', async () => { + const featureFlag = { + primer_react_action_list_item_as_button: true, + } + + const {container} = HTMLRender( + + + Item 1 + Item 2 + + , + ) + + const button = container.querySelector('button') + expect(button).toHaveTextContent('Item 1') + + // Ensure passed prop "disabled" is applied to the button + expect(button).toHaveAttribute('aria-disabled', 'true') + + const listItems = container.querySelectorAll('li') + expect(listItems.length).toBe(2) + }) + + it('should render ActionList.Item as li when feature flag is disabled', async () => { + const {container} = HTMLRender( + + + Item 1 + Item 2 + + , + ) + + const listitem = container.querySelector('li') + const button = container.querySelector('button') + + expect(listitem).toHaveTextContent('Item 1') + expect(listitem).toHaveAttribute('tabindex', '0') + expect(button).toBeNull() + + const listItems = container.querySelectorAll('li') + expect(listItems.length).toBe(2) + }) + + it('should apply ref to ActionList.Item when feature flag is disabled', async () => { + const MockComponent = () => { + const ref = React.useRef(null) + + const focusRef = () => { + if (ref.current) ref.current.focus() + } + + return ( + + + + Item 1 + Item 2 + + + ) + } + + const {getByRole} = HTMLRender() + const triggerBtn = getByRole('button', {name: 'Prompt'}) + const focusTarget = getByRole('listitem', {name: 'Item 1'}) + + fireEvent.click(triggerBtn) + + expect(document.activeElement).toBe(focusTarget) + }) + + it('should render ActionList.Item as li when feature flag is enabled and has proper aria role', async () => { + const {container} = HTMLRender( + + + Item 1 + Item 2 + + , + ) + + const listitem = container.querySelector('li') + const button = container.querySelector('button') + + expect(listitem).toHaveTextContent('Item 1') + expect(listitem).toHaveAttribute('tabindex', '0') + expect(button).toBeNull() + + const listItems = container.querySelectorAll('li') + expect(listItems.length).toBe(2) + }) + + it('should render the trailing action as a button (default)', async () => { + const {container} = HTMLRender( + + + Item 1 + + + , + ) + + const action = container.querySelector('button[aria-labelledby]') + expect(action).toHaveAccessibleName('Action') + }) + + it('should render the trailing action as a link', async () => { + const {container} = HTMLRender( + + + Item 1 + + + , + ) + + const action = container.querySelector('a[href="#"][aria-labelledby]') + expect(action).toHaveAccessibleName('Action') + }) + + it('should do action when trailing action is clicked', async () => { + const onClick = jest.fn() + const component = HTMLRender( + + + Item 1 + + + , + ) + + const trailingAction = await waitFor(() => component.getByRole('button', {name: 'Action'})) + fireEvent.click(trailingAction) + expect(onClick).toHaveBeenCalled() + }) + + it('should focus the trailing action', async () => { + HTMLRender( + + + Item 1 + + + , + ) + + await userEvent.tab() + expect(document.activeElement).toHaveTextContent('Item 1') + await userEvent.tab() + expect(document.activeElement).toHaveAccessibleName('Action') + }) + + it('should only trigger a key event once when feature flag is enabled', async () => { + const mockOnSelect = jest.fn() + const user = userEvent.setup() + const {getByRole} = HTMLRender( + + + Item 1 + + , + ) + const item = getByRole('button') + + item.focus() + + expect(document.activeElement).toBe(item) + await user.keyboard('{Enter}') + + expect(mockOnSelect).toHaveBeenCalledTimes(1) + }) + + it('should not render buttons when feature flag is enabled and is specified role', async () => { + const {getByRole} = HTMLRender( + + + Item 1 + Item 2 + Item 3 + Item 4 + Item 5 + + , + ) + + const option = getByRole('option') + expect(option.tagName).toBe('LI') + expect(option.textContent).toBe('Item 1') + + const menuItem = getByRole('menuitem') + expect(menuItem.tagName).toBe('LI') + + const menuItemCheckbox = getByRole('menuitemcheckbox') + expect(menuItemCheckbox.tagName).toBe('LI') + + const menuItemRadio = getByRole('menuitemradio') + expect(menuItemRadio.tagName).toBe('LI') + + const button = getByRole('button') + expect(button.parentElement?.tagName).toBe('LI') + expect(button.textContent).toBe('Item 5') + }) + it('should be navigatable with arrow keys for certain roles', async () => { HTMLRender( @@ -94,6 +603,54 @@ describe('ActionList', () => { expect(document.activeElement).toHaveTextContent('Option 4') }) + describe('ActionList.Description', () => { + it('should render the description as inline without truncation by default', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('SPAN') + expect(description).toHaveStyleRule('flex-basis', 'auto') + expect(description).not.toHaveStyleRule('overflow', 'ellipsis') + expect(description).not.toHaveStyleRule('white-space', 'nowrap') + }) + it('should render the description as `Truncate` when truncate is true', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('DIV') + expect(description).toHaveAttribute('title', 'Item 1 description') + expect(description).toHaveStyleRule('flex-basis', '0') + expect(description).toHaveStyleRule('text-overflow', 'ellipsis') + expect(description).toHaveStyleRule('overflow', 'hidden') + expect(description).toHaveStyleRule('white-space', 'nowrap') + }) + it('should render the description in a new line when variant is block', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('SPAN') + expect(description.parentElement).toHaveAttribute('data-component', 'ActionList.Item--DividerContainer') + }) + }) + it('should support a custom `className` on the outermost element', () => { const Element = () => { return ( diff --git a/packages/react/src/ActionList/Description.test.tsx b/packages/react/src/ActionList/Description.test.tsx deleted file mode 100644 index 858ede5e70e..00000000000 --- a/packages/react/src/ActionList/Description.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import {render as HTMLRender} from '@testing-library/react' -import React from 'react' -import {ActionList} from '.' -import {FeatureFlags} from '../FeatureFlags' - -describe('ActionList.Description', () => { - it('should render the description as inline without truncation by default', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description).toHaveStyleRule('flex-basis', 'auto') - expect(description).not.toHaveStyleRule('overflow', 'ellipsis') - expect(description).not.toHaveStyleRule('white-space', 'nowrap') - }) - it('should render the description as `Truncate` when truncate is true', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('DIV') - expect(description).toHaveAttribute('title', 'Item 1 description') - expect(description).toHaveStyleRule('flex-basis', '0') - expect(description).toHaveStyleRule('text-overflow', 'ellipsis') - expect(description).toHaveStyleRule('overflow', 'hidden') - expect(description).toHaveStyleRule('white-space', 'nowrap') - }) - it('should render the description in a new line when variant is block', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description.parentElement).toHaveAttribute('data-component', 'ActionList.Item--DividerContainer') - }) - it('should support a custom `className`', () => { - const Element = () => { - return ( - - - Item 1Item 1 description - - - ) - } - const FeatureFlagElement = () => { - return ( - - - - ) - } - expect( - HTMLRender().container.querySelector('span[data-component="ActionList.Description"]'), - ).toHaveClass('test-class-name') - expect( - HTMLRender().container.querySelector('span[data-component="ActionList.Description"]'), - ).toHaveClass('test-class-name') - }) -}) diff --git a/packages/react/src/ActionList/Description.tsx b/packages/react/src/ActionList/Description.tsx index 15b2e19c3f4..68bc4f661f9 100644 --- a/packages/react/src/ActionList/Description.tsx +++ b/packages/react/src/ActionList/Description.tsx @@ -4,11 +4,6 @@ import Truncate from '../Truncate' import type {SxProp} from '../sx' import {merge} from '../sx' import {ItemContext} from './shared' -import {useFeatureFlag} from '../FeatureFlags' -import classes from './ActionList.module.css' -import {clsx} from 'clsx' -import {defaultSxProp} from '../utils/defaultSxProp' -import {actionListCssModulesFlag} from './featureflag' export type ActionListDescriptionProps = { /** @@ -27,7 +22,7 @@ export type ActionListDescriptionProps = { export const Description: React.FC> = ({ variant = 'inline', - sx = defaultSxProp, + sx = {}, className, truncate, ...props @@ -47,65 +42,6 @@ export const Description: React.FC - {props.children} - - ) - } else { - return ( - - {props.children} - - ) - } - } - if (variant === 'block' || !truncate) { - return ( - - {props.children} - - ) - } else { - return ( - - {props.children} - - ) - } - } - return variant === 'block' || !truncate ? ( > = ({sx = defaultSxProp, className}) => { - const enabled = useFeatureFlag(actionListCssModulesFlag) + const enabled = useFeatureFlag('primer_react_css_modules_team') if (enabled) { if (sx !== defaultSxProp) { return ( @@ -44,6 +43,8 @@ export const Divider: React.FC> marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`, marginBottom: 2, listStyle: 'none', // hide the ::marker inserted by browser's stylesheet + marginRight: 'calc(-1 * var(--base-size-8))', + marginLeft: 'calc(-1 * var(--base-size-8))', }, sx as SxProp, )} diff --git a/packages/react/src/ActionList/Group.module.css b/packages/react/src/ActionList/Group.module.css deleted file mode 100644 index 1a517c52463..00000000000 --- a/packages/react/src/ActionList/Group.module.css +++ /dev/null @@ -1,52 +0,0 @@ -.Group:not(:first-child) { - margin-block-start: var(--base-size-8); - - /* If somebody tries to pass the `title` prop AND a `NavList.GroupHeading` as a child, hide the `ActionList.GroupHeading */ - /* stylelint-disable-next-line selector-max-specificity */ - &:has(.GroupHeadingWrap + ul > .GroupHeadingWrap) { - /* stylelint-disable-next-line selector-max-specificity */ - & > .GroupHeadingWrap { - display: none; - } - } -} - -.GroupHeadingWrap { - display: flex; - font-size: var(--text-body-size-small); - font-weight: var(--base-text-weight-semibold); - - /* line-height: var(--text-body-lineHeight-small); use when FF rolls out */ - /* stylelint-disable-next-line primer/typography */ - line-height: 18px; - color: var(--fgColor-muted); - flex-direction: column; - padding-inline: var(--base-size-16); - padding-block: var(--base-size-6); - - &:where([data-variant='filled']) { - /* stylelint-disable-next-line primer/spacing */ - margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); - margin-block-end: var(--base-size-8); - background: var(--bgColor-muted); - border-top: solid var(--borderWidth-thin) var(--borderColor-muted); - border-bottom: solid var(--borderWidth-thin) var(--borderColor-muted); - padding-inline: var(--base-size-16); - - &:first-child { - margin-block-start: 0; - } - } - - /* & + ul:has(.GroupHeadingWrap) { - outline: solid 1px red; - } */ -} - -.GroupHeading { - margin: 0; - font-size: var(--text-body-size-small); - font-weight: var(--base-text-weight-semibold); - color: var(--fgColor-muted); - align-self: flex-start; -} diff --git a/packages/react/src/ActionList/Group.test.tsx b/packages/react/src/ActionList/Group.test.tsx deleted file mode 100644 index 742cd50c105..00000000000 --- a/packages/react/src/ActionList/Group.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import {render as HTMLRender} from '@testing-library/react' -import React from 'react' -import theme from '../theme' -import {ActionList} from '.' -import {BaseStyles, ThemeProvider, ActionMenu} from '..' -import {FeatureFlags} from '../FeatureFlags' - -describe('ActionList.Group', () => { - it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - expect(() => - HTMLRender( - - - - Trigger - - - - Group Heading - - - - - - , - ), - ).toThrow( - "Looks like you are trying to set a heading level to a menu role. Group headings for menu type action lists are for representational purposes, and rendered as divs. Therefore they don't need a heading level.", - ) - expect(spy).toHaveBeenCalled() - spy.mockRestore() - }) - - it('should render the ActionList.GroupHeading component as a heading with the given heading level', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const heading = container.getByRole('heading', {level: 2}) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Group Heading') - }) - it('should throw an error if ActionList.GroupHeading is used without an `as` prop when no role is specified (for list role)', async () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - expect(() => - HTMLRender( - - Heading - - Group Heading - Item - - , - ), - ).toThrow( - "You are setting a heading for a list, that requires a heading level. Please use 'as' prop to set a proper heading level.", - ) - expect(spy).toHaveBeenCalled() - spy.mockRestore() - }) - it('should render the ActionList.GroupHeading component as a span (not a heading tag) when role is specified as listbox', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const label = container.getByText('Group Heading') - expect(label).toBeInTheDocument() - expect(label.tagName).toEqual('SPAN') - }) - it('should render the ActionList.GroupHeading component as a span with role="presentation" and aria-hidden="true" when role is specified as listbox', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const label = container.getByText('Group Heading') - const wrapper = label.parentElement - expect(wrapper).toHaveAttribute('role', 'presentation') - expect(wrapper).toHaveAttribute('aria-hidden', 'true') - }) - it('should label the list with the group heading id', async () => { - const {container, getByText} = HTMLRender( - - Heading - - Group Heading - Item - - , - ) - const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).toHaveAttribute('aria-labelledby', heading.id) - }) - it('should NOT label the list with the group heading id when role is specified', async () => { - const {container, getByText} = HTMLRender( - - Heading - - Group Heading - Item - - , - ) - const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).not.toHaveAttribute('aria-labelledby', heading.id) - }) - - it('should support a custom `className` on the outermost element', () => { - const Element = () => { - return ( - - - - Test - - - - ) - } - const FeatureFlagElement = () => { - return ( - - - - ) - } - expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') - expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') - }) -}) diff --git a/packages/react/src/ActionList/Group.tsx b/packages/react/src/ActionList/Group.tsx index 44be2fa68d4..957f37a4863 100644 --- a/packages/react/src/ActionList/Group.tsx +++ b/packages/react/src/ActionList/Group.tsx @@ -4,48 +4,12 @@ import Box from '../Box' import type {SxProp} from '../sx' import {ListContext, type ActionListProps} from './shared' import type {AriaRole} from '../utils/types' +import {default as Heading} from '../Heading' import type {ActionListHeadingProps} from './Heading' import {useSlots} from '../hooks/useSlots' import {defaultSxProp} from '../utils/defaultSxProp' import {invariant} from '../utils/invariant' import {clsx} from 'clsx' -import {useFeatureFlag} from '../FeatureFlags' -import classes from './ActionList.module.css' -import groupClasses from './Group.module.css' -import {actionListCssModulesFlag} from './featureflag' - -type HeadingProps = { - as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - className?: string - children: React.ReactNode - id?: string -} & SxProp - -const Heading: React.FC> = ({ - as: Component = 'h3', - className, - children, - sx = defaultSxProp, - id, - ...rest -}) => { - return ( - // Box is temporary to support lingering sx usage - - {children} - - ) -} - -type HeadingWrapProps = { - as?: 'div' | 'li' - className?: string - children: React.ReactNode -} - -const HeadingWrap: React.FC = ({as = 'div', children, className, ...rest}) => { - return React.createElement(as, {...rest, className}, children) -} export type ActionListGroupProps = { /** @@ -54,7 +18,7 @@ export type ActionListGroupProps = { * - `"filled"` - Superimposed on a background, offset from nearby content * - `"subtle"` - Relatively less offset from nearby content */ - variant?: 'filled' | 'subtle' + variant?: 'subtle' | 'filled' /** * @deprecated (Use `ActionList.GroupHeading` instead. i.e. Group title) */ @@ -86,10 +50,9 @@ export const Group: React.FC> = ({ auxiliaryText, selectionVariant, role, - sx = defaultSxProp, + sx = {}, ...props }) => { - const enabled = useFeatureFlag(actionListCssModulesFlag) const id = useId() const {role: listRole} = React.useContext(ListContext) @@ -109,54 +72,6 @@ export const Group: React.FC> = ({ groupHeadingId = id } - if (enabled) { - if (sx !== defaultSxProp) { - return ( - - - {title && !slots.groupHeading ? ( - // Escape hatch: supports old API in a non breaking way - - ) : null} - {/* Supports new API ActionList.GroupHeading */} - {!title && slots.groupHeading ? React.cloneElement(slots.groupHeading) : null} - - - - ) - } - return ( -
  • - - {title && !slots.groupHeading ? ( - // Escape hatch: supports old API in a non breaking way - - ) : null} - {/* Supports new API ActionList.GroupHeading */} - {!title && slots.groupHeading ? React.cloneElement(slots.groupHeading) : null} - - -
  • - ) - } return ( > = ({ as, - variant = 'subtle', + variant, // We are not recommending this prop to be used, it should only be used internally for incremental rollout. _internalBackwardCompatibleTitle, auxiliaryText, @@ -222,7 +136,7 @@ export const GroupHeading: React.FC { - const {role: listRole} = React.useContext(ListContext) + const {variant: listVariant, role: listRole} = React.useContext(ListContext) const {groupHeadingId} = React.useContext(GroupContext) // for list role, the headings are proper heading tags, for menu and listbox, they are just representational and divs const missingAsForList = (listRole === undefined || listRole === 'list') && children !== undefined && as === undefined @@ -240,54 +154,59 @@ export const GroupHeading: React.FC {/* for listbox (SelectPanel) and menu (ActionMenu) roles, group titles are presentational. */} {listRole && listRole !== 'list' ? ( - + ) : ( // for explicit (role="list" is passed as prop) and implicit list roles (ActionList ins rendered as list by default), group titles are proper heading tags. - - {sx !== defaultSxProp ? ( - - {_internalBackwardCompatibleTitle ?? children} - - ) : ( - - {_internalBackwardCompatibleTitle ?? children} - - )} - {auxiliaryText &&
    {auxiliaryText}
    } -
    + + + {_internalBackwardCompatibleTitle ?? children} + + {auxiliaryText &&
    {auxiliaryText}
    } +
    )} ) diff --git a/packages/react/src/ActionList/Heading.tsx b/packages/react/src/ActionList/Heading.tsx index a100a3fef74..d024be646c7 100644 --- a/packages/react/src/ActionList/Heading.tsx +++ b/packages/react/src/ActionList/Heading.tsx @@ -12,7 +12,6 @@ import {invariant} from '../utils/invariant' import {clsx} from 'clsx' import {useFeatureFlag} from '../FeatureFlags' import classes from './Heading.module.css' -import {actionListCssModulesFlag} from './featureflag' type HeadingLevels = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' type HeadingVariants = 'large' | 'medium' | 'small' @@ -28,7 +27,7 @@ export const Heading = forwardRef( const innerRef = React.useRef(null) useRefObjectAsForwardedRef(forwardedRef, innerRef) - const enabled = useFeatureFlag(actionListCssModulesFlag) + const enabled = useFeatureFlag('primer_react_css_modules_team') const {headingId: headingId, variant: listVariant} = React.useContext(ListContext) const {container} = React.useContext(ActionListContainerContext) diff --git a/packages/react/src/ActionList/Item.test.tsx b/packages/react/src/ActionList/Item.test.tsx deleted file mode 100644 index a242a02e907..00000000000 --- a/packages/react/src/ActionList/Item.test.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import React from 'react' -import {ActionList} from '.' -import {BookIcon} from '@primer/octicons-react' -import {FeatureFlags} from '../FeatureFlags' - -function SimpleActionList(): JSX.Element { - return ( - - New file - - Copy link - Edit file - Delete file - - Link Item - - - ) -} - -const projects = [ - {name: 'Primer Backlog', scope: 'GitHub'}, - {name: 'Primer React', scope: 'github/primer'}, - {name: 'Disabled Project', scope: 'github/primer', disabled: true}, - {name: 'Inactive Project', scope: 'github/primer', inactiveText: 'Unavailable due to an outage'}, - {name: 'Loading Project', scope: 'github/primer', loading: true}, - { - name: 'Inactive and Loading Project', - scope: 'github/primer', - loading: true, - inactiveText: 'Unavailable due to an outage, but loading still passed', - }, -] - -function SingleSelectListStory(): JSX.Element { - const [selectedIndex, setSelectedIndex] = React.useState(0) - - return ( - - {projects.map((project, index) => ( - setSelectedIndex(index)} - disabled={project.disabled} - inactiveText={project.inactiveText} - loading={project.loading} - > - {project.name} - - ))} - - ) -} - -describe('ActionList.Item', () => { - it('should have aria-keyshortcuts applied to the correct element', async () => { - const {container} = HTMLRender() - const linkOptions = await waitFor(() => container.querySelectorAll('a')) - expect(linkOptions[0]).toHaveAttribute('aria-keyshortcuts', 'd') - expect(linkOptions[0].parentElement).not.toHaveAttribute('aria-keyshortcuts', 'd') - }) - it('should fire onSelect on click and keypress', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[1]) - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') - // We pass keycode here to navigate a implementation detail in react-testing-library - // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 - fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[1], {key: ' ', charCode: 32}) - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') - }) - it('should skip onSelect on disabled items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[2]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - }) - it('should skip onSelect on inactive items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[3]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - }) - it('should skip onSelect on loading items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - fireEvent.click(options[4]) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - }) - it('should not crash when clicking an item without an onSelect', async () => { - const component = HTMLRender( - - Primer React - , - ) - const option = await waitFor(() => component.getByRole('option')) - expect(option).toBeInTheDocument() - fireEvent.click(option) - fireEvent.keyPress(option, {key: 'Enter', charCode: 13}) - expect(option).toBeInTheDocument() - }) - it('should focus the button around the leading visual when tabbing to an inactive item', async () => { - const component = HTMLRender() - const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[3].inactiveText})) - await userEvent.tab() // get focus on first element - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - expect(inactiveOptionButton).toHaveFocus() - }) - it('should behave as inactive if both inactiveText and loading props are passed', async () => { - const component = HTMLRender() - const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[5].inactiveText})) - await userEvent.tab() // get focus on first element - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - expect(inactiveOptionButton).toHaveFocus() - }) - it('should call onClick for a link item', async () => { - const onClick = jest.fn() - const component = HTMLRender( - - - Primer React - - , - ) - const link = await waitFor(() => component.getByRole('link')) - fireEvent.click(link) - expect(onClick).toHaveBeenCalled() - }) - it('should render ActionList.Item as button when feature flag is enabled', async () => { - const featureFlag = { - primer_react_css_modules_team: true, - primer_react_css_modules_staff: true, - primer_react_css_modules_ga: true, - } - const {container} = HTMLRender( - - - Item 1 - Item 2 - - , - ) - const button = container.querySelector('button') - expect(button).toHaveTextContent('Item 1') - // Ensure passed prop "disabled" is applied to the button - expect(button).toHaveAttribute('aria-disabled', 'true') - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - it('should render ActionList.Item as li when feature flag is disabled', async () => { - const {container} = HTMLRender( - - - Item 1 - Item 2 - - , - ) - const listitem = container.querySelector('li') - const button = container.querySelector('button') - expect(listitem).toHaveTextContent('Item 1') - expect(listitem).toHaveAttribute('tabindex', '0') - expect(button).toBeNull() - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - it('should apply ref to ActionList.Item when feature flag is disabled', async () => { - const MockComponent = () => { - const ref = React.useRef(null) - const focusRef = () => { - if (ref.current) ref.current.focus() - } - return ( - - - - Item 1 - Item 2 - - - ) - } - const {getByRole} = HTMLRender() - const triggerBtn = getByRole('button', {name: 'Prompt'}) - const focusTarget = getByRole('listitem', {name: 'Item 1'}) - fireEvent.click(triggerBtn) - expect(document.activeElement).toBe(focusTarget) - }) - it('should render ActionList.Item as li when item has proper aria role', async () => { - const {container} = HTMLRender( - - Item 1 - Item 2 - , - ) - const listitem = container.querySelector('li') - const button = container.querySelector('button') - expect(listitem).toHaveTextContent('Item 1') - expect(listitem).toHaveAttribute('tabindex', '0') - expect(button).toBeNull() - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - it('should render the trailing action as a button (default)', async () => { - const {container} = HTMLRender( - - - Item 1 - - - , - ) - const action = container.querySelector('button[aria-labelledby]') - expect(action).toHaveAccessibleName('Action') - }) - it('should render the trailing action as a link', async () => { - const {container} = HTMLRender( - - - Item 1 - - - , - ) - const action = container.querySelector('a[href="#"][aria-labelledby]') - expect(action).toHaveAccessibleName('Action') - }) - it('should do action when trailing action is clicked', async () => { - const onClick = jest.fn() - const component = HTMLRender( - - - Item 1 - - - , - ) - const trailingAction = await waitFor(() => component.getByRole('button', {name: 'Action'})) - fireEvent.click(trailingAction) - expect(onClick).toHaveBeenCalled() - }) - it('should focus the trailing action', async () => { - HTMLRender( - - - Item 1 - - - , - ) - await userEvent.tab() - expect(document.activeElement).toHaveTextContent('Item 1') - await userEvent.tab() - expect(document.activeElement).toHaveAccessibleName('Action') - }) - it('should only trigger a key event once when feature flag is enabled', async () => { - const mockOnSelect = jest.fn() - const user = userEvent.setup() - const {getByRole} = HTMLRender( - - - Item 1 - - , - ) - const item = getByRole('button') - item.focus() - expect(document.activeElement).toBe(item) - await user.keyboard('{Enter}') - expect(mockOnSelect).toHaveBeenCalledTimes(1) - }) - it('should not render buttons when feature flag is enabled and is specified role', async () => { - const {getByRole} = HTMLRender( - - - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - - , - ) - const option = getByRole('option') - expect(option.tagName).toBe('LI') - expect(option.textContent).toBe('Item 1') - const menuItem = getByRole('menuitem') - expect(menuItem.tagName).toBe('LI') - const menuItemCheckbox = getByRole('menuitemcheckbox') - expect(menuItemCheckbox.tagName).toBe('LI') - const menuItemRadio = getByRole('menuitemradio') - expect(menuItemRadio.tagName).toBe('LI') - const button = getByRole('button') - expect(button.parentElement?.tagName).toBe('LI') - expect(button.textContent).toBe('Item 5') - }) -}) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index d90b40a1ce8..fe614b91e43 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -15,42 +15,20 @@ import {GroupContext} from './Group' import type {ActionListItemProps, ActionListProps} from './shared' import {Selection} from './Selection' import {LeadingVisual, TrailingVisual, VisualOrIndicator} from './Visuals' -import {getVariantStyles, ItemContext, ListContext} from './shared' +import {getVariantStyles, ItemContext, TEXT_ROW_HEIGHT, ListContext} from './shared' import {TrailingAction} from './TrailingAction' import {ConditionalWrapper} from '../internal/components/ConditionalWrapper' import {invariant} from '../utils/invariant' import {useFeatureFlag} from '../FeatureFlags' import VisuallyHidden from '../_VisuallyHidden' -import classes from './ActionList.module.css' -import {clsx} from 'clsx' - -import {actionListCssModulesFlag} from './featureflag' const LiBox = styled.li(sx) -type ActionListSubItemProps = { - children?: React.ReactNode -} - -export const SubItem: React.FC = ({children}) => { - return <>{children} -} - -SubItem.displayName = 'ActionList.SubItem' - -const ButtonItemContainerNoBox = React.forwardRef(({children, style, ...props}, forwardedRef) => { - return ( - - ) -}) as PolymorphicForwardRefComponent - -const DivItemContainerNoBox = React.forwardRef(({children, ...props}, forwardedRef) => { +const ButtonItemContainer = React.forwardRef(({as: Component = 'button', children, styles, ...props}, forwardedRef) => { return ( -
    } {...props}> + {children} -
    +
    ) }) as PolymorphicForwardRefComponent @@ -68,36 +46,23 @@ export const Item = React.forwardRef( role, loading, _PrivateItemWrapper, - className, ...props }, forwardedRef, ): JSX.Element => { - const enabled = useFeatureFlag(actionListCssModulesFlag) - - const baseSlots = { + const [slots, childrenWithoutSlots] = useSlots(props.children, { leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, trailingAction: TrailingAction, - subItem: SubItem, - } - - const [partialSlots, childrenWithoutSlots] = useSlots( - props.children, - enabled - ? {...baseSlots, description: Description} - : { - ...baseSlots, - blockDescription: [Description, props => props.variant === 'block'], - inlineDescription: [Description, props => props.variant !== 'block'], - }, - ) - - const slots = {blockDescription: undefined, inlineDescription: undefined, description: undefined, ...partialSlots} + blockDescription: [Description, props => props.variant === 'block'], + inlineDescription: [Description, props => props.variant !== 'block'], + }) const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = React.useContext(ActionListContainerContext) + const buttonSemanticsFeatureFlag = useFeatureFlag('primer_react_action_list_item_as_button') + // Be sure to avoid rendering the container unless there is a default const wrappedDefaultTrailingVisual = defaultTrailingVisual ? ( {defaultTrailingVisual} @@ -159,8 +124,9 @@ export const Item = React.forwardRef( role === 'option' || role === 'menuitem' || role === 'menuitemradio' || role === 'menuitemcheckbox' const listRoleTypes = ['listbox', 'menu', 'list'] - const listSemantics = (listRole && listRoleTypes.includes(listRole)) || inactive || listItemSemantics || !enabled - const buttonSemantics = !listSemantics && !_PrivateItemWrapper + const listSemantics = + (listRole && listRoleTypes.includes(listRole)) || inactive || container === 'NavList' || listItemSemantics + const buttonSemantics = !listSemantics && !_PrivateItemWrapper && buttonSemanticsFeatureFlag const {theme} = useTheme() @@ -174,20 +140,49 @@ export const Item = React.forwardRef( width: '4px', height: '24px', content: '""', - bg: 'var(--borderColor-accent-emphasis)', + bg: 'accent.fg', borderRadius: 2, }, } + const hoverStyles = { + '@media (hover: hover) and (pointer: fine)': { + '&:hover:not([aria-disabled]):not([data-inactive])': { + backgroundColor: `actionListItem.${variant}.hoverBg`, + color: getVariantStyles(variant, disabled, inactive).hoverColor, + boxShadow: `inset 0 0 0 max(1px, 0.0625rem) ${theme?.colors.actionListItem.default.activeBorder}`, + }, + '&:focus-visible, > a.focus-visible, &:focus.focus-visible': { + outline: 'none', + border: `2 solid`, + boxShadow: `0 0 0 2px var(--focus-outlineColor)`, + }, + '&:active:not([aria-disabled]):not([data-inactive])': { + backgroundColor: `actionListItem.${variant}.activeBg`, + color: getVariantStyles(variant, disabled, inactive).hoverColor, + }, + }, + } + + const listItemStyles = { + display: 'flex', + // show between 2 items + ':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider}, + width: buttonSemantics && listVariant !== 'full' ? 'calc(100% - 16px)' : '100%', + marginX: buttonSemantics && listVariant !== 'full' ? '2' : '0', + borderRadius: 2, + ...(buttonSemantics ? hoverStyles : {}), + } + const styles = { position: 'relative', display: 'flex', paddingX: 2, fontSize: 1, paddingY: '6px', // custom value off the scale - lineHeight: '16px', + lineHeight: TEXT_ROW_HEIGHT, minHeight: 5, - marginX: listVariant === 'inset' ? 2 : 0, + marginX: listVariant === 'inset' && !buttonSemantics ? 2 : 0, borderRadius: 2, transition: 'background 33.333ms linear', color: getVariantStyles(variant, disabled, inactive || loading).color, @@ -211,7 +206,7 @@ export const Item = React.forwardRef( appearance: 'none', background: 'unset', border: 'unset', - width: listVariant === 'inset' ? 'calc(100% - 16px)' : '100%', + width: listVariant === 'inset' && !buttonSemantics ? 'calc(100% - 16px)' : '100%', fontFamily: 'unset', textAlign: 'unset', marginY: 'unset', @@ -223,23 +218,6 @@ export const Item = React.forwardRef( }, }, - '@media (hover: hover) and (pointer: fine)': { - '&:hover:not([aria-disabled]):not([data-inactive])': { - backgroundColor: `actionListItem.${variant}.hoverBg`, - color: getVariantStyles(variant, disabled, inactive).hoverColor, - boxShadow: `inset 0 0 0 max(1px, 0.0625rem) ${theme?.colors.actionListItem.default.activeBorder}`, - }, - '&:focus-visible, > a.focus-visible, &:focus.focus-visible': { - outline: 'none', - border: `2 solid`, - boxShadow: `0 0 0 2px var(--focus-outlineColor)`, - }, - '&:active:not([aria-disabled]):not([data-inactive])': { - backgroundColor: `actionListItem.${variant}.activeBg`, - color: getVariantStyles(variant, disabled, inactive).hoverColor, - }, - }, - /** Divider styles */ '[data-component="ActionList.Item--DividerContainer"]': { position: 'relative', @@ -271,6 +249,8 @@ export const Item = React.forwardRef( /** Active styles */ ...(active ? activeStyles : {}), // NavList '&[data-is-active-descendant]': {...activeStyles, fontWeight: 'normal'}, // SelectPanel + + ...(!buttonSemantics ? hoverStyles : {}), } const clickHandler = React.useCallback( @@ -305,10 +285,8 @@ export const Item = React.forwardRef( const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined let DefaultItemWrapper = React.Fragment - if (enabled) { - DefaultItemWrapper = listSemantics ? DivItemContainerNoBox : ButtonItemContainerNoBox - } else { - DefaultItemWrapper = React.Fragment + if (buttonSemanticsFeatureFlag) { + DefaultItemWrapper = listSemantics ? React.Fragment : ButtonItemContainer } const ItemWrapper = _PrivateItemWrapper || DefaultItemWrapper @@ -335,172 +313,28 @@ export const Item = React.forwardRef( ...(includeSelectionAttribute && {[itemSelectionAttribute]: selected}), role: itemRole, id: itemId, - className, } - const containerProps = _PrivateItemWrapper - ? {role: itemRole ? 'none' : undefined, ...props} - : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (listSemantics && {...menuItemProps, ...props, ref: forwardedRef}) || {} - - const wrapperProps = _PrivateItemWrapper - ? menuItemProps - : !listSemantics && { - ...menuItemProps, - ...props, - ref: forwardedRef, - } - - // Extract the variant prop value from the description slot component - - const descriptionVariant = slots.description?.props.variant ?? 'inline' - - if (enabled) { - if (sxProp !== defaultSxProp) { - return ( - - (styles, sxProp)} - ref={listSemantics ? forwardedRef : null} - data-variant={variant === 'danger' ? variant : undefined} - data-active={active ? true : undefined} - data-inactive={inactiveText ? true : undefined} - data-has-subitem={slots.subItem ? true : undefined} - className={clsx(classes.ActionListItem, className)} - > - - - - - {slots.leadingVisual} - - - - - {childrenWithoutSlots} - {/* Loading message needs to be in here so it is read with the label */} - {loading === true && Loading} - - {slots.description} - - - {trailingVisual} - - - { - // If the item is inactive, but it's not in an overlay (e.g. ActionMenu, SelectPanel), - // render the inactive warning message directly in the item. - inactive && container ? ( - - {inactiveText} - - ) : null - } - - - {!inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction} - {slots.subItem} - - - ) - } - return ( - -
  • - - - - - {slots.leadingVisual} - - - - - {childrenWithoutSlots} - {/* Loading message needs to be in here so it is read with the label */} - {loading === true && Loading} - - {slots.description} - - - {trailingVisual} - - - { - // If the item is inactive, but it's not in an overlay (e.g. ActionMenu, SelectPanel), - // render the inactive warning message directly in the item. - inactive && container ? ( - - {inactiveText} - - ) : null - } - - - {!inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction} - {slots.subItem} -
  • -
    - ) + let containerProps + let wrapperProps + + if (buttonSemanticsFeatureFlag) { + containerProps = _PrivateItemWrapper + ? {role: itemRole ? 'none' : undefined, ...props} + : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (listSemantics && {...menuItemProps, ...props, ref: forwardedRef}) || {} + + wrapperProps = _PrivateItemWrapper + ? menuItemProps + : !listSemantics && { + ...menuItemProps, + ...props, + styles: merge(styles, sxProp), + ref: forwardedRef, + } + } else { + containerProps = _PrivateItemWrapper ? {role: itemRole ? 'none' : undefined} : {...menuItemProps, ...props} + wrapperProps = _PrivateItemWrapper ? menuItemProps : {} } return ( @@ -515,9 +349,15 @@ export const Item = React.forwardRef( }} > (styles, sxProp)} + ref={!buttonSemanticsFeatureFlag || listSemantics ? forwardedRef : null} + sx={ + buttonSemanticsFeatureFlag + ? merge( + listSemantics || _PrivateItemWrapper ? styles : listItemStyles, + listSemantics || _PrivateItemWrapper ? sxProp : {}, + ) + : merge(styles, sxProp) + } data-variant={variant === 'danger' ? variant : undefined} {...containerProps} > diff --git a/packages/react/src/ActionList/LinkItem.tsx b/packages/react/src/ActionList/LinkItem.tsx index fd9d908f292..c878a821d1b 100644 --- a/packages/react/src/ActionList/LinkItem.tsx +++ b/packages/react/src/ActionList/LinkItem.tsx @@ -6,9 +6,6 @@ import {merge} from '../sx' import {Item} from './Item' import type {ActionListItemProps} from './shared' import Box from '../Box' -import {defaultSxProp} from '../utils/defaultSxProp' -import {useFeatureFlag} from '../FeatureFlags' -import {actionListCssModulesFlag} from './featureflag' // adopted from React.AnchorHTMLAttributes type LinkProps = { @@ -21,119 +18,56 @@ type LinkProps = { target?: string type?: string referrerPolicy?: React.AnchorHTMLAttributes['referrerPolicy'] - className?: string } // LinkItem does not support selected, loading, variants, etc. -export type ActionListLinkItemProps = Pick< - ActionListItemProps, - 'active' | 'children' | 'sx' | 'inactiveText' | 'variant' -> & +export type ActionListLinkItemProps = Pick & LinkProps -export const LinkItem = React.forwardRef( - ({sx = defaultSxProp, active, inactiveText, variant, as: Component, className, ...props}, forwardedRef) => { - const styles = { - // occupy full size of Item - paddingX: 2, - paddingY: '6px', // custom value off the scale - display: 'flex', - flexGrow: 1, // full width - borderRadius: 2, +export const LinkItem = React.forwardRef(({sx = {}, active, inactiveText, as: Component, ...props}, forwardedRef) => { + const styles = { + // occupy full size of Item + paddingX: 2, + paddingY: '6px', // custom value off the scale + display: 'flex', + flexGrow: 1, // full width + borderRadius: 2, - // inherit Item styles - color: 'inherit', - '&:hover': {color: 'inherit', textDecoration: 'none'}, - } + // inherit Item styles + color: 'inherit', + '&:hover': {color: 'inherit', textDecoration: 'none'}, + } - const enabled = useFeatureFlag(actionListCssModulesFlag) - - if (enabled) { - if (sx !== defaultSxProp) { - return ( - { - const clickHandler = (event: React.MouseEvent) => { - onClick && onClick(event) - props.onClick && props.onClick(event as React.MouseEvent) - } - return inactiveText ? ( - {children} - ) : ( - - {children} - - ) - }} + return ( + { + const clickHandler = (event: React.MouseEvent) => { + onClick && onClick(event) + props.onClick && props.onClick(event as React.MouseEvent) + } + return inactiveText ? ( + + {children} + + ) : ( + - {props.children} - + {children} + ) - } - - return ( - { - const clickHandler = (event: React.MouseEvent) => { - onClick && onClick(event) - props.onClick && props.onClick(event as React.MouseEvent) - } - return inactiveText ? ( - {children} - ) : ( - - {children} - - ) - }} - > - {props.children} - - ) - } - - return ( - { - const clickHandler = (event: React.MouseEvent) => { - onClick && onClick(event) - props.onClick && props.onClick(event as React.MouseEvent) - } - return inactiveText ? ( - - {children} - - ) : ( - - {children} - - ) - }} - > - {props.children} - - ) - }, -) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> + }} + > + {props.children} + + ) +}) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> diff --git a/packages/react/src/ActionList/List.tsx b/packages/react/src/ActionList/List.tsx index 9b84e3d1424..c957b40060a 100644 --- a/packages/react/src/ActionList/List.tsx +++ b/packages/react/src/ActionList/List.tsx @@ -14,7 +14,6 @@ import {FocusKeys, useFocusZone} from '../hooks/useFocusZone' import {clsx} from 'clsx' import {useFeatureFlag} from '../FeatureFlags' import classes from './ActionList.module.css' -import {actionListCssModulesFlag} from './featureflag' const ListBox = styled.ul(sx) @@ -58,7 +57,7 @@ export const List = React.forwardRef( focusOutBehavior: listRole === 'menu' ? 'wrap' : undefined, }) - const enabled = useFeatureFlag(actionListCssModulesFlag) + const enabled = useFeatureFlag('primer_react_css_modules_team') return ( -export const Selection: React.FC> = ({selected, className}) => { +type SelectionProps = Pick +export const Selection: React.FC> = ({selected}) => { const {selectionVariant: listSelectionVariant, role: listRole} = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) - const enabled = useFeatureFlag(actionListCssModulesFlag) - /** selectionVariant in Group can override the selectionVariant in List root */ /** fallback to selectionVariant from container menu if any (ActionMenu, SelectPanel ) */ let selectionVariant: ActionListProps['selectionVariant'] | ActionListGroupProps['selectionVariant'] @@ -35,13 +30,6 @@ export const Selection: React.FC> = ({se } if (selectionVariant === 'single' || listRole === 'menu') { - if (enabled) { - return ( - - - - ) - } return ( {selected && } @@ -74,13 +62,6 @@ export const Selection: React.FC> = ({se }, } - if (enabled) { - return ( - -
    - - ) - } return ( { - const enabled = useFeatureFlag(actionListCssModulesFlag) - - if (enabled) { - return ( - - {icon ? ( - - ) : ( - // @ts-expect-error shhh - - )} - - ) - } - +export const TrailingAction = forwardRef(({as = 'button', icon, label, href = null, ...props}, forwardedRef) => { + if (!icon) { + return ( + + {/* @ts-expect-error TODO: Fix this */} + + + ) + } else { return ( - {icon ? ( - - ) : ( - // @ts-expect-error shhh - - )} + ) - }, -) as PolymorphicForwardRefComponent<'button' | 'a', ActionListTrailingActionProps> + } +}) as PolymorphicForwardRefComponent<'button' | 'a', ActionListTrailingActionProps> TrailingAction.displayName = 'ActionList.TrailingAction' diff --git a/packages/react/src/ActionList/Visuals.tsx b/packages/react/src/ActionList/Visuals.tsx index 3cad24a0eb2..4e75348f67a 100644 --- a/packages/react/src/ActionList/Visuals.tsx +++ b/packages/react/src/ActionList/Visuals.tsx @@ -4,39 +4,19 @@ import Box from '../Box' import Spinner from '../Spinner' import type {SxProp} from '../sx' import {merge} from '../sx' -import {ItemContext, getVariantStyles} from './shared' +import {ItemContext, TEXT_ROW_HEIGHT, getVariantStyles} from './shared' import {Tooltip, type TooltipProps} from '../TooltipV2' -import {clsx} from 'clsx' -import {useFeatureFlag} from '../FeatureFlags' -import classes from './ActionList.module.css' -import {defaultSxProp} from '../utils/defaultSxProp' -import {actionListCssModulesFlag} from './featureflag' export type VisualProps = SxProp & React.HTMLAttributes -export const VisualContainer: React.FC> = ({ - sx = defaultSxProp, - className, - ...props -}) => { - if (sx !== defaultSxProp) { - return - } - return -} - -// remove when primer_react_css_modules_X is shipped -export const LeadingVisualContainer: React.FC> = ({ - sx = defaultSxProp, - ...props -}) => { +export const LeadingVisualContainer: React.FC> = ({sx = {}, ...props}) => { return ( > = ({ - sx = defaultSxProp, - className, - ...props -}) => { +export const LeadingVisual: React.FC> = ({sx = {}, ...props}) => { const {variant, disabled, inactive} = React.useContext(ItemContext) - - const enabled = useFeatureFlag(actionListCssModulesFlag) - - if (enabled) { - return ( - - {props.children} - - ) - } return ( > = ({ } export type ActionListTrailingVisualProps = VisualProps -export const TrailingVisual: React.FC> = ({ - sx = defaultSxProp, - className, - ...props -}) => { +export const TrailingVisual: React.FC> = ({sx = {}, ...props}) => { const {variant, disabled, inactive, trailingVisualId} = React.useContext(ItemContext) - const enabled = useFeatureFlag(actionListCssModulesFlag) - if (enabled) { - if (sx !== defaultSxProp) { - return ( - - {props.children} - - ) - } - return ( - - {props.children} - - ) - } return ( -> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position, className}) => { +> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position}) => { const VisualComponent = position === 'leading' ? LeadingVisual : TrailingVisual if (!loading && !inactiveText) return children @@ -167,17 +111,26 @@ export const VisualOrIndicator: React.FC< } return inactiveText ? ( - - - - - + + + + + + + ) : ( - + ) diff --git a/packages/react/src/ActionList/featureflag.ts b/packages/react/src/ActionList/featureflag.ts deleted file mode 100644 index f4e5922f54d..00000000000 --- a/packages/react/src/ActionList/featureflag.ts +++ /dev/null @@ -1 +0,0 @@ -export const actionListCssModulesFlag = 'primer_react_css_modules_team' diff --git a/packages/react/src/ActionList/shared.ts b/packages/react/src/ActionList/shared.ts index bae0b66c2a9..a8f62cfc86d 100644 --- a/packages/react/src/ActionList/shared.ts +++ b/packages/react/src/ActionList/shared.ts @@ -52,7 +52,6 @@ export type ActionListItemProps = { * Private API for use internally only. Used by LinkItem to wrap contents in an anchor */ _PrivateItemWrapper?: React.FC> - className?: string } & SxProp type MenuItemProps = { @@ -63,7 +62,6 @@ type MenuItemProps = { 'aria-labelledby'?: string 'aria-describedby'?: string role?: string - className?: string } export type ItemContext = Pick & { diff --git a/packages/react/src/NavList/NavList.test.tsx b/packages/react/src/NavList/NavList.test.tsx index b62bf6290c2..178ff7a3a81 100644 --- a/packages/react/src/NavList/NavList.test.tsx +++ b/packages/react/src/NavList/NavList.test.tsx @@ -63,6 +63,29 @@ describe('NavList', () => { expect(container).toMatchSnapshot() }) + it('only shows NavList.GroupHeading when NavList.Group `title` prop is passed AND NavList.GroupHeading is a child', () => { + const {getByText} = render( + + + + Group heading + + Getting started + + + + Avatar + + + , + ) + const groupHeading = getByText('Group heading') + const groupTitle = getByText('Overview') + + expect(groupHeading).toBeVisible() + expect(groupTitle).not.toBeVisible() + }) + it('supports TrailingAction', async () => { const {getByRole} = render( diff --git a/packages/react/src/NavList/NavList.tsx b/packages/react/src/NavList/NavList.tsx index 910f6c516fb..7a482cc8da5 100644 --- a/packages/react/src/NavList/NavList.tsx +++ b/packages/react/src/NavList/NavList.tsx @@ -10,7 +10,6 @@ import type { ActionListGroupHeadingProps, } from '../ActionList' import {ActionList} from '../ActionList' -import {SubItem} from '../ActionList/Item' import {ActionListContainerContext} from '../ActionList/ActionListContainerContext' import Box from '../Box' import Octicon from '../Octicon' @@ -19,14 +18,11 @@ import sx, {merge} from '../sx' import {defaultSxProp} from '../utils/defaultSxProp' import {useId} from '../hooks/useId' import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect' -import {useFeatureFlag} from '../FeatureFlags' -import classes from '../ActionList/ActionList.module.css' -import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' const getSubnavStyles = (depth: number) => { return { paddingLeft: depth > 0 ? depth + 2 : null, // Indent sub-items - fontSize: depth > 0 ? 0 : 1, // Reduce font size of sub-items + fontSize: depth > 0 ? 0 : null, // Reduce font size of sub-items fontWeight: depth > 0 ? 'normal' : null, // Sub-items don't get bolded } } @@ -39,7 +35,7 @@ export type NavListProps = { } & SxProp & React.ComponentProps<'nav'> -const NavBox = toggleStyledComponent('primer_react_css_modules_team', 'nav', styled.nav(sx)) +const NavBox = styled.nav(sx) const Root = React.forwardRef(({children, ...props}, ref) => { return ( @@ -70,7 +66,6 @@ export type NavListItemProps = { const Item = React.forwardRef( ({'aria-current': ariaCurrent, children, defaultOpen, sx: sxProp = defaultSxProp, ...props}, ref) => { - const enabled = useFeatureFlag('primer_react_css_modules_team') const {depth} = React.useContext(SubNavContext) // Get SubNav from children @@ -88,13 +83,7 @@ const Item = React.forwardRef( // Render ItemWithSubNav if SubNav is present if (subNav && isValidElement(subNav)) { return ( - + {childrenWithoutSubNavOrTrailingAction} ) @@ -105,8 +94,7 @@ const Item = React.forwardRef( ref={ref} aria-current={ariaCurrent} active={Boolean(ariaCurrent) && ariaCurrent !== 'false'} - sx={enabled ? undefined : merge(getSubnavStyles(depth), sxProp)} - style={{'--subitem-depth': depth} as React.CSSProperties} + sx={merge(getSubnavStyles(depth), sxProp)} {...props} > {children} @@ -125,7 +113,6 @@ type ItemWithSubNavProps = { subNav: React.ReactNode depth: number defaultOpen?: boolean - style: React.CSSProperties } & SxProp const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: string; isOpen: boolean}>({ @@ -134,14 +121,9 @@ const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: s isOpen: false, }) -function ItemWithSubNav({ - children, - subNav, - depth, - defaultOpen, - style = {}, - sx: sxProp = defaultSxProp, -}: ItemWithSubNavProps) { +// TODO: ref prop +// TODO: Animate open/close transition +function ItemWithSubNav({children, subNav, depth, defaultOpen, sx: sxProp = defaultSxProp}: ItemWithSubNavProps) { const buttonId = useId() const subNavId = useId() const [isOpen, setIsOpen] = React.useState((defaultOpen || null) ?? false) @@ -161,50 +143,6 @@ function ItemWithSubNav({ } }, [subNav, buttonId]) - const enabled = useFeatureFlag('primer_react_css_modules_team') - if (enabled) { - if (sxProp !== defaultSxProp) { - return ( - - setIsOpen(open => !open)} - style={style} - sx={sxProp} - > - {children} - {/* What happens if the user provides a TrailingVisual? */} - - - - {React.cloneElement(subNav as React.ReactElement, {ref: subNavRef})} - - - ) - } - return ( - - setIsOpen(open => !open)} - style={style} - > - {children} - {/* What happens if the user provides a TrailingVisual? */} - - - - {React.cloneElement(subNav as React.ReactElement, {ref: subNavRef})} - - - ) - } return ( @@ -251,11 +189,12 @@ export type NavListSubNavProps = { const SubNavContext = React.createContext<{depth: number}>({depth: 0}) +// TODO: ref prop // NOTE: SubNav must be a direct child of an Item -const SubNav = React.forwardRef(({children, sx: sxProp = defaultSxProp}: NavListSubNavProps, forwardedRef) => { +const SubNav = ({children, sx: sxProp = defaultSxProp}: NavListSubNavProps) => { const {buttonId, subNavId, isOpen} = React.useContext(ItemWithSubNavContext) const {depth} = React.useContext(SubNavContext) - const enabled = useFeatureFlag('primer_react_css_modules_team') + if (!buttonId || !subNavId) { // eslint-disable-next-line no-console console.error('NavList.SubNav must be a child of a NavList.Item') @@ -268,32 +207,6 @@ const SubNav = React.forwardRef(({children, sx: sxProp = defaultSxProp}: NavList return null } - if (enabled) { - if (sxProp !== defaultSxProp) { - return ( - - - {children} - - - ) - } - return ( - -
      - {children} -
    -
    - ) - } - return ( ) -}) as PolymorphicForwardRefComponent<'ul', NavListSubNavProps> +} SubNav.displayName = 'NavList.SubNav' @@ -361,33 +274,8 @@ export type NavListGroupProps = { } & SxProp const defaultSx = {} +// TODO: ref prop const Group: React.FC = ({title, children, sx: sxProp = defaultSx, ...props}) => { - const enabled = useFeatureFlag('primer_react_css_modules_team') - - if (enabled) { - if (sxProp !== defaultSx) { - return ( - - {title ? {title} : null} - {children} - - ) - } - return ( - <> - - - {/* Setting up the default value for the heading level. TODO: API update to give flexibility to NavList.Group title's heading level */} - {title ? ( - - {title} - - ) : null} - {children} - - - ) - } return ( <> {/* Hide divider if the group is the first item in the list */} @@ -396,8 +284,8 @@ const Group: React.FC = ({title, children, sx: sxProp = defau {...props} // If somebody tries to pass the `title` prop AND a `NavList.GroupHeading` as a child, hide the `ActionList.GroupHeading` sx={merge(sxProp, { - ':has([data-component="GroupHeadingWrap"] + ul > [data-component="GroupHeadingWrap"])': { - '& > [data-component="GroupHeadingWrap"]': {display: 'none'}, + ':has([data-component="NavList.GroupHeading"]):has([data-component="ActionList.GroupHeading"])': { + '[data-component="ActionList.GroupHeading"]': {display: 'none'}, }, })} > diff --git a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap index e3848f074b3..8b12be29285 100644 --- a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap +++ b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap @@ -16,7 +16,6 @@ exports[`NavList renders a simple list 1`] = ` flex-grow: 1; border-radius: 6px; color: inherit; - font-size: 14px; } .c3:hover { @@ -78,7 +77,7 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 16px; + line-height: 20px; min-height: 5px; margin-left: 8px; margin-right: 8px; @@ -162,7 +161,7 @@ exports[`NavList renders a simple list 1`] = ` width: 4px; height: 24px; content: ""; - background-color: var(--borderColor-accent-emphasis); + background-color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); border-radius: 6px; } @@ -178,7 +177,7 @@ exports[`NavList renders a simple list 1`] = ` width: 4px; height: 24px; content: ""; - background-color: var(--borderColor-accent-emphasis); + background-color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); border-radius: 6px; } @@ -193,7 +192,7 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 16px; + line-height: 20px; min-height: 5px; margin-left: 8px; margin-right: 8px; @@ -280,7 +279,7 @@ exports[`NavList renders a simple list 1`] = ` width: 4px; height: 24px; content: ""; - background-color: var(--borderColor-accent-emphasis); + background-color: var(--fgColor-accent,var(--color-accent-fg,#0969da)); border-radius: 6px; } @@ -360,7 +359,6 @@ exports[`NavList renders a simple list 1`] = ` class="c3 Link" href="/" id=":r2:" - style="--subitem-depth: 0;" tabindex="0" >