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 78bcafea1372..68dafe1dd178 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 4ef9b156f59a..b2719be85443 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 524a61847690..a0254e82cc97 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 b37da39acdbb..68dafe1dd178 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 78bcafea1372..68dafe1dd178 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 43a1f22cac99..f05283352e02 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 566d3d7b353a..1f2c1b2b1a50 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 ac570cf2ec31..f05283352e02 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 43a1f22cac99..f05283352e02 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 04432c956fc5..929a0fd92387 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 0bc9c8e20a18..accfb9ed1c83 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 04432c956fc5..929a0fd92387 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 0bc9c8e20a18..accfb9ed1c83 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 c6f30df62c04..22f2553664c5 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 0f9d0cc56214..7c8899c3e383 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 e44c0adabfcd..812951abd0e5 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 c6f30df62c04..22f2553664c5 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 c6f30df62c04..22f2553664c5 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 ea786b54974f..0ac72c55ce50 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 b05289fcd97f..9d9ebb4b9e0f 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 ea786b54974f..0ac72c55ce50 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 ea786b54974f..0ac72c55ce50 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 c35d8aa199d3..ddd38f08acdf 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 5964c04027ba..b50828b3e456 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 213f7b607426..37387f81f231 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 c35d8aa199d3..ddd38f08acdf 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 c35d8aa199d3..ddd38f08acdf 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 53b577bddc16..1c47d858de1e 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 4ca17804028b..82796c9cc19b 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 53b577bddc16..1c47d858de1e 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 53b577bddc16..1c47d858de1e 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/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png index f5d111c7be61..2f496c853b44 100644 Binary files a/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png and b/.playwright/snapshots/components/ActionMenu.test.ts-snapshots/ActionMenu-Groups-And-Descriptions-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Overlay.test.ts-snapshots/Overlay-Dropdown-Overlay-dark-high-contrast-linux.png b/.playwright/snapshots/components/Overlay.test.ts-snapshots/Overlay-Dropdown-Overlay-dark-high-contrast-linux.png index c20b9db4b749..f51d27d4a762 100644 Binary files a/.playwright/snapshots/components/Overlay.test.ts-snapshots/Overlay-Dropdown-Overlay-dark-high-contrast-linux.png and b/.playwright/snapshots/components/Overlay.test.ts-snapshots/Overlay-Dropdown-Overlay-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Above-Tall-Body-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Above-Tall-Body-dark-high-contrast-modern-action-list--true-linux.png index 11995b4296af..e775519a1cba 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Above-Tall-Body-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Above-Tall-Body-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-dark-high-contrast-modern-action-list--true-linux.png index c1ac13ab7cf5..aefe6193ff5c 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Default-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-External-Anchor-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-External-Anchor-dark-high-contrast-modern-action-list--true-linux.png index a00d44b39f83..8c41e778262f 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-External-Anchor-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-External-Anchor-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Overflowing-Items-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Overflowing-Items-dark-high-contrast-modern-action-list--true-linux.png index b26c31c4972e..19b42a364e73 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Overflowing-Items-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Overflowing-Items-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Underflowing-Items-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Underflowing-Items-dark-high-contrast-modern-action-list--true-linux.png index 2e5905d8554f..3784931b68d5 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Underflowing-Items-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-Height-Initial-with-Underflowing-Items-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Footer-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Footer-dark-high-contrast-modern-action-list--true-linux.png index 72a9d6e26b50..852d630bc8bb 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Footer-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Footer-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Item-Dividers-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Item-Dividers-dark-high-contrast-modern-action-list--true-linux.png index 991c9b97376c..8f112d3b7c19 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Item-Dividers-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Item-Dividers-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Internally-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Internally-dark-high-contrast-modern-action-list--true-linux.png index c6d42a4b85fc..ac85b437a1d7 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Internally-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Internally-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Visually-Hidden-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Visually-Hidden-dark-high-contrast-modern-action-list--true-linux.png index 3b7e313e8833..de6a8e6695ed 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Visually-Hidden-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Label-Visually-Hidden-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Placeholder-for-Search-Input-dark-high-contrast-modern-action-list--true-linux.png b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Placeholder-for-Search-Input-dark-high-contrast-modern-action-list--true-linux.png index a20863e9b825..14f14aea7a62 100644 Binary files a/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Placeholder-for-Search-Input-dark-high-contrast-modern-action-list--true-linux.png and b/.playwright/snapshots/components/SelectPanel.test.ts-snapshots/SelectPanel-With-Placeholder-for-Search-Input-dark-high-contrast-modern-action-list--true-linux.png differ diff --git a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-As-Modal-dark-high-contrast-linux.png b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-As-Modal-dark-high-contrast-linux.png index 5aeb800729e1..48e80e1fbd73 100644 Binary files a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-As-Modal-dark-high-contrast-linux.png and b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-As-Modal-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-Default-dark-high-contrast-linux.png b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-Default-dark-high-contrast-linux.png index 97f160c76a81..df8a9b6705fa 100644 Binary files a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-Default-dark-high-contrast-linux.png and b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-Default-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-External-Anchor-dark-high-contrast-linux.png b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-External-Anchor-dark-high-contrast-linux.png index de380a4e0786..8e1a4ae8e68f 100644 Binary files a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-External-Anchor-dark-high-contrast-linux.png and b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-External-Anchor-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-With-Warning-dark-high-contrast-linux.png b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-With-Warning-dark-high-contrast-linux.png index b9045ca5c0d6..71367098b972 100644 Binary files a/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-With-Warning-dark-high-contrast-linux.png and b/.playwright/snapshots/components/drafts/SelectPanel.test.ts-snapshots/drafts-SelectPanel-With-Warning-dark-high-contrast-linux.png differ diff --git a/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css b/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css new file mode 100644 index 000000000000..7c499f43c117 --- /dev/null +++ b/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css @@ -0,0 +1,10 @@ +@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: ''; + background: var(--bgColor-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 35c37cf213f1..bf8b7456b7f2 100644 --- a/packages/react/src/ActionList/ActionList.dev.stories.tsx +++ b/packages/react/src/ActionList/ActionList.dev.stories.tsx @@ -7,6 +7,7 @@ import {Group} from './Group' import {Divider} from './Divider' import {Description} from './Description' import Avatar from '../Avatar' +import {FileDirectoryIcon} from '@primer/octicons-react' export default { title: 'Components/ActionList/Dev', @@ -144,3 +145,23 @@ export const HeadingCustomClassname = () => ( ) + +export const DescriptionCustomClassname = () => ( + + + Label + This is a description + + +) + +export const VisualCustomClassname = () => ( + + + Label + + + + + +) diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 0f3e563ffa05..2759f846dda8 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable selector-max-specificity, selector-max-compound-selectors */ +/* stylelint-disable max-nesting-depth, selector-max-specificity, selector-max-compound-selectors */ .ActionList { padding: 0; @@ -13,15 +13,19 @@ &:where([data-variant='inset']) { /* change to padding (all) when Item is converted */ - padding-block: var(--base-size-8); + padding: 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: calc(-1 * var(--control-medium-paddingBlock)); + top: -7px; display: block; width: 100%; height: 1px; @@ -34,8 +38,12 @@ & [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: calc(-1 * var(--control-medium-paddingBlock)); + top: -7px; display: block; width: 100%; height: var(--borderWidth-thin); @@ -64,6 +72,570 @@ } } +/* 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; + } + + /* 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; + } + } +} + +/* 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] */ + + /* target items inside expanded subgroups */ + &[aria-expanded] { + & + .SubGroup { + @media screen and (prefers-reduced-motion: no-preference) { + transition: + opacity 160ms cubic-bezier(0.25, 1, 0.5, 1), + transform 160ms cubic-bezier(0.25, 1, 0.5, 1); + } + } + } + + &[aria-expanded='true'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(-1); + } + + & .SubGroup { + height: auto; + overflow: visible; + visibility: visible; + opacity: 1; + transform: translateY(0); + } + + &.ActionListContent--hasActiveSubItem { + & > .ItemLabel { + font-weight: var(--base-text-weight-semibold); + } + } + } + + &[aria-expanded='false'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(1); + } + + & + .SubGroup { + height: 0; + overflow: hidden; + visibility: hidden; + opacity: 0; + transform: translateY(calc(-1 * var(--base-size-16))); + } + + /* 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-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); +} + +/* 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 { display: block; height: var(--borderWidth-thin); @@ -77,3 +649,45 @@ 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 11ce02d53fcc..fe987278e34e 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -1,12 +1,11 @@ -import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' +import {render as HTMLRender} 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, ActionMenu} from '..' +import {BaseStyles, ThemeProvider} from '..' import {FeatureFlags} from '../FeatureFlags' function SimpleActionList(): JSX.Element { @@ -28,41 +27,6 @@ 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, @@ -75,100 +39,12 @@ 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()) @@ -186,391 +62,6 @@ 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( @@ -603,54 +94,6 @@ 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 new file mode 100644 index 000000000000..858ede5e70ea --- /dev/null +++ b/packages/react/src/ActionList/Description.test.tsx @@ -0,0 +1,82 @@ +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 68bc4f661f9e..c61f78d2189f 100644 --- a/packages/react/src/ActionList/Description.tsx +++ b/packages/react/src/ActionList/Description.tsx @@ -4,6 +4,10 @@ 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' export type ActionListDescriptionProps = { /** @@ -22,7 +26,7 @@ export type ActionListDescriptionProps = { export const Description: React.FC> = ({ variant = 'inline', - sx = {}, + sx = defaultSxProp, className, truncate, ...props @@ -42,6 +46,65 @@ 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 ? ( { + 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 c49fdf55623f..91575a365ae9 100644 --- a/packages/react/src/ActionList/Group.tsx +++ b/packages/react/src/ActionList/Group.tsx @@ -4,12 +4,37 @@ 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' + +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} + + ) +} export type ActionListGroupProps = { /** @@ -18,7 +43,7 @@ export type ActionListGroupProps = { * - `"filled"` - Superimposed on a background, offset from nearby content * - `"subtle"` - Relatively less offset from nearby content */ - variant?: 'subtle' | 'filled' + variant?: 'filled' | 'subtle' /** * @deprecated (Use `ActionList.GroupHeading` instead. i.e. → Group title) */ @@ -50,9 +75,10 @@ export const Group: React.FC> = ({ auxiliaryText, selectionVariant, role, - sx = {}, + sx = defaultSxProp, ...props }) => { + const enabled = useFeatureFlag('primer_react_css_modules_team') const id = useId() const {role: listRole} = React.useContext(ListContext) @@ -72,6 +98,54 @@ 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?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' _internalBackwardCompatibleTitle?: string + variant?: 'filled' | 'subtle' } /** @@ -125,7 +200,7 @@ export type ActionListGroupHeadingProps = Pick> = ({ as, - variant, + variant = 'subtle', // We are not recommending this prop to be used, it should only be used internally for incremental rollout. _internalBackwardCompatibleTitle, auxiliaryText, @@ -134,7 +209,7 @@ export const GroupHeading: React.FC { - const {variant: listVariant, role: listRole} = React.useContext(ListContext) + const {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 @@ -152,58 +227,47 @@ 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. - - - {_internalBackwardCompatibleTitle ?? children} - - {auxiliaryText &&
    {auxiliaryText}
    } -
    +
    + {sx !== defaultSxProp ? ( + + {_internalBackwardCompatibleTitle ?? children} + + ) : ( + + {_internalBackwardCompatibleTitle ?? children} + + )} + {auxiliaryText &&
    {auxiliaryText}
    } +
    )} ) diff --git a/packages/react/src/ActionList/Item.test.tsx b/packages/react/src/ActionList/Item.test.tsx new file mode 100644 index 000000000000..a242a02e907f --- /dev/null +++ b/packages/react/src/ActionList/Item.test.tsx @@ -0,0 +1,356 @@ +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 f51caa7b641a..24f4fef149fc 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -15,20 +15,39 @@ import {GroupContext} from './Group' import type {ActionListItemProps, ActionListProps} from './shared' import {Selection} from './Selection' import {LeadingVisual, TrailingVisual, VisualOrIndicator} from './Visuals' -import {getVariantStyles, ItemContext, TEXT_ROW_HEIGHT, ListContext} from './shared' +import {getVariantStyles, ItemContext, 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' const LiBox = styled.li(sx) -const ButtonItemContainer = React.forwardRef(({as: Component = 'button', children, styles, ...props}, forwardedRef) => { +interface SubItemProps { + children?: React.ReactNode +} + +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) => { return ( - +
    } {...props}> {children} - +
    ) }) as PolymorphicForwardRefComponent @@ -46,35 +65,43 @@ export const Item = React.forwardRef( role, loading, _PrivateItemWrapper, + className, ...props }, forwardedRef, ): JSX.Element => { - const [slots, childrenWithoutSlots] = useSlots(props.children, { + const enabled = useFeatureFlag('primer_react_css_modules_team') + + const baseSlots = { leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, trailingAction: TrailingAction, - blockDescription: [Description, props => props.variant === 'block'], - inlineDescription: [Description, props => props.variant !== 'block'], - }) + 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} 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} ) : null const trailingVisual = slots.trailingVisual ?? wrappedDefaultTrailingVisual - const { - variant: listVariant, - role: listRole, - showDividers, - selectionVariant: listSelectionVariant, - } = React.useContext(ListContext) + const {role: listRole, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) const inactive = Boolean(inactiveText) const showInactiveIndicator = inactive && container === undefined @@ -124,9 +151,8 @@ 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 || container === 'NavList' || listItemSemantics - const buttonSemantics = !listSemantics && !_PrivateItemWrapper && buttonSemanticsFeatureFlag + const listSemantics = (listRole && listRoleTypes.includes(listRole)) || inactive || listItemSemantics || !enabled + const buttonSemantics = !listSemantics && !_PrivateItemWrapper const {theme} = useTheme() @@ -145,44 +171,14 @@ export const Item = React.forwardRef( }, } - 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 ${theme?.colors.accent.emphasis}`, - }, - '&: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: TEXT_ROW_HEIGHT, + lineHeight: '16px', minHeight: 5, - marginX: listVariant === 'inset' && !buttonSemantics ? 2 : 0, borderRadius: 2, transition: 'background 33.333ms linear', color: getVariantStyles(variant, disabled, inactive || loading).color, @@ -206,7 +202,7 @@ export const Item = React.forwardRef( appearance: 'none', background: 'unset', border: 'unset', - width: listVariant === 'inset' && !buttonSemantics ? 'calc(100% - 16px)' : '100%', + width: '100%', fontFamily: 'unset', textAlign: 'unset', marginY: 'unset', @@ -218,6 +214,23 @@ 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', @@ -249,8 +262,6 @@ export const Item = React.forwardRef( /** Active styles */ ...(active ? activeStyles : {}), // NavList '&[data-is-active-descendant]': {...activeStyles, fontWeight: 'normal'}, // SelectPanel - - ...(!buttonSemantics ? hoverStyles : {}), } const clickHandler = React.useCallback( @@ -285,16 +296,22 @@ export const Item = React.forwardRef( const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined let DefaultItemWrapper = React.Fragment - if (buttonSemanticsFeatureFlag) { - DefaultItemWrapper = listSemantics ? React.Fragment : ButtonItemContainer + if (enabled) { + DefaultItemWrapper = listSemantics ? DivItemContainerNoBox : ButtonItemContainerNoBox + } else { + DefaultItemWrapper = React.Fragment } const ItemWrapper = _PrivateItemWrapper || DefaultItemWrapper + // const ItemWrapper = + // _PrivateItemWrapper || (props.wrapper === 'button' ? ButtonItemContainerNoBox : DefaultItemWrapper) // only apply aria-selected and aria-checked to selectable items const selectableRoles = ['menuitemradio', 'menuitemcheckbox', 'option'] const includeSelectionAttribute = itemSelectionAttribute && itemRole && selectableRoles.includes(itemRole) + // const blockDescriptionSlot = [Description, (props: any) => props.variant === 'block' || props.variant === undefined] + const menuItemProps = { onClick: clickHandler, onKeyPress: !buttonSemantics ? keyPressHandler : undefined, @@ -313,28 +330,194 @@ export const Item = React.forwardRef( ...(includeSelectionAttribute && {[itemSelectionAttribute]: selected}), role: itemRole, id: itemId, + className, } - 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 : {} + 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, + // styles: merge(styles, sxProp), + ref: forwardedRef, + } + + // 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 : {} + // } + + // Extract the variant prop value from the description slot component + + const descriptionVariant = slots.description?.props.variant ?? 'inline' + + // console.log(listSemantics) + + 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} +
  • +
    + ) } return ( @@ -349,15 +532,9 @@ export const Item = React.forwardRef( }} > ( - listSemantics || _PrivateItemWrapper ? styles : listItemStyles, - listSemantics || _PrivateItemWrapper ? sxProp : {}, - ) - : merge(styles, sxProp) - } + ref={listSemantics ? forwardedRef : null} + className={className} + sx={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 c878a821d1b0..49dd6389f549 100644 --- a/packages/react/src/ActionList/LinkItem.tsx +++ b/packages/react/src/ActionList/LinkItem.tsx @@ -6,6 +6,8 @@ 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' // adopted from React.AnchorHTMLAttributes type LinkProps = { @@ -18,56 +20,119 @@ type LinkProps = { target?: string type?: string referrerPolicy?: React.AnchorHTMLAttributes['referrerPolicy'] + className?: string } // LinkItem does not support selected, loading, variants, etc. -export type ActionListLinkItemProps = Pick & +export type ActionListLinkItemProps = Pick< + ActionListItemProps, + 'active' | 'children' | 'sx' | 'inactiveText' | 'variant' +> & LinkProps -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, +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, - // inherit Item styles - color: 'inherit', - '&:hover': {color: 'inherit', textDecoration: 'none'}, - } + // inherit Item styles + color: 'inherit', + '&:hover': {color: 'inherit', textDecoration: 'none'}, + } - return ( - { - const clickHandler = (event: React.MouseEvent) => { - onClick && onClick(event) - props.onClick && props.onClick(event as React.MouseEvent) - } - return inactiveText ? ( - - {children} - - ) : ( - { + const clickHandler = (event: React.MouseEvent) => { + onClick && onClick(event) + props.onClick && props.onClick(event as React.MouseEvent) + } + return inactiveText ? ( + {children} + ) : ( + + {children} + + ) + }} > - {children} - + {props.children} + ) - }} - > - {props.children} - - ) -}) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> + } + + 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> diff --git a/packages/react/src/ActionList/List.tsx b/packages/react/src/ActionList/List.tsx index c957b40060ac..c2da3daaad44 100644 --- a/packages/react/src/ActionList/List.tsx +++ b/packages/react/src/ActionList/List.tsx @@ -25,7 +25,7 @@ export const List = React.forwardRef( const styles = { margin: 0, paddingInlineStart: 0, // reset ul styles - paddingY: variant === 'inset' ? 2 : 0, + padding: variant === 'inset' ? 2 : 0, } const [slots, childrenWithoutSlots] = useSlots(props.children, { diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index 355176e34934..7b51b4346ff5 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -3,14 +3,18 @@ import {CheckIcon} from '@primer/octicons-react' import type {ActionListGroupProps} from './Group' import {GroupContext} from './Group' import {type ActionListProps, type ActionListItemProps, ListContext} from './shared' -import {LeadingVisualContainer} from './Visuals' +import {LeadingVisualContainer, VisualContainer} from './Visuals' import Box from '../Box' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' -type SelectionProps = Pick -export const Selection: React.FC> = ({selected}) => { +type SelectionProps = Pick +export const Selection: React.FC> = ({selected, className}) => { const {selectionVariant: listSelectionVariant, role: listRole} = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) + const enabled = useFeatureFlag('primer_react_css_modules_team') + /** 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'] @@ -30,6 +34,13 @@ export const Selection: React.FC> = ({se } if (selectionVariant === 'single' || listRole === 'menu') { + if (enabled) { + return ( + + + + ) + } return ( {selected && } @@ -62,11 +73,18 @@ export const Selection: React.FC> = ({se }, } + if (enabled) { + return ( + +
    + + ) + } return ( > = ({se margin: '0', placeContent: 'center', width: 'var(--base-size-16, 16px)', - backgroundColor: selected ? 'accent.fg' : 'canvas.default', + backgroundColor: selected ? 'var(--control-checked-bgColor-rest)' : 'canvas.default', transition: selected ? 'background-color, border-color 80ms cubic-bezier(0.33, 1, 0.68, 1)' : 'background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms', diff --git a/packages/react/src/ActionList/TrailingAction.tsx b/packages/react/src/ActionList/TrailingAction.tsx index 1b9df53370f7..3171f02fff74 100644 --- a/packages/react/src/ActionList/TrailingAction.tsx +++ b/packages/react/src/ActionList/TrailingAction.tsx @@ -2,6 +2,9 @@ import React, {forwardRef} from 'react' import Box from '../Box' import {Button, IconButton} from '../Button' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import {clsx} from 'clsx' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' type ElementProps = | { @@ -16,25 +19,46 @@ type ElementProps = export type ActionListTrailingActionProps = ElementProps & { icon?: React.ElementType label: string + className?: string } -export const TrailingAction = forwardRef(({as = 'button', icon, label, href = null, ...props}, forwardedRef) => { - if (!icon) { - return ( - - {/* @ts-expect-error TODO: Fix this */} - - - ) - } else { +export const TrailingAction = forwardRef( + ({as = 'button', icon, label, href = null, className, ...props}, forwardedRef) => { + const enabled = useFeatureFlag('primer_react_css_modules_team') + + if (enabled) { + return ( + + {icon ? ( + + ) : ( + // @ts-expect-error shhh + + )} + + ) + } + 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 4e75348f67a8..b35b6ea160b4 100644 --- a/packages/react/src/ActionList/Visuals.tsx +++ b/packages/react/src/ActionList/Visuals.tsx @@ -4,19 +4,38 @@ import Box from '../Box' import Spinner from '../Spinner' import type {SxProp} from '../sx' import {merge} from '../sx' -import {ItemContext, TEXT_ROW_HEIGHT, getVariantStyles} from './shared' +import {ItemContext, 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' export type VisualProps = SxProp & React.HTMLAttributes -export const LeadingVisualContainer: React.FC> = ({sx = {}, ...props}) => { +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 +}) => { return ( > = ({sx = {}, ...props}) => { +export const LeadingVisual: React.FC> = ({ + sx = defaultSxProp, + className, + ...props +}) => { const {variant, disabled, inactive} = React.useContext(ItemContext) + + const enabled = useFeatureFlag('primer_react_css_modules_team') + + if (enabled) { + if (sx !== defaultSxProp) { + return ( + + {props.children} + + ) + } + return ( + + {props.children} + + ) + } return ( > = ({s } export type ActionListTrailingVisualProps = VisualProps -export const TrailingVisual: React.FC> = ({sx = {}, ...props}) => { +export const TrailingVisual: React.FC> = ({ + sx = defaultSxProp, + className, + ...props +}) => { const {variant, disabled, inactive, trailingVisualId} = React.useContext(ItemContext) + const enabled = useFeatureFlag('primer_react_css_modules_team') + if (enabled) { + if (sx !== defaultSxProp) { + return ( + + {props.children} + + ) + } + return ( + + {props.children} + + ) + } return ( -> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position}) => { +> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position, className}) => { const VisualComponent = position === 'leading' ? LeadingVisual : TrailingVisual if (!loading && !inactiveText) return children @@ -111,26 +171,17 @@ export const VisualOrIndicator: React.FC< } return inactiveText ? ( - - - - - - - + + + + + ) : ( - + ) diff --git a/packages/react/src/ActionList/shared.ts b/packages/react/src/ActionList/shared.ts index a8f62cfc86d2..bae0b66c2a94 100644 --- a/packages/react/src/ActionList/shared.ts +++ b/packages/react/src/ActionList/shared.ts @@ -52,6 +52,7 @@ 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 = { @@ -62,6 +63,7 @@ type MenuItemProps = { 'aria-labelledby'?: string 'aria-describedby'?: string role?: string + className?: string } export type ItemContext = Pick & { diff --git a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap index 0310e28f437f..2bfc0dc43649 100644 --- a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap +++ b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap @@ -62,8 +62,7 @@ exports[`NavList renders a simple list 1`] = ` .c0 { margin: 0; padding-inline-start: 0; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px; } .c2 { @@ -77,10 +76,8 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -91,7 +88,7 @@ exports[`NavList renders a simple list 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -192,10 +189,8 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -206,7 +201,7 @@ exports[`NavList renders a simple list 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -304,7 +299,7 @@ exports[`NavList renders a simple list 1`] = ` .c2:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } .c2:active:not([aria-disabled]):not([data-inactive]) { @@ -334,7 +329,7 @@ exports[`NavList renders a simple list 1`] = ` .c6:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } .c6:active:not([aria-disabled]):not([data-inactive]) { @@ -453,34 +448,10 @@ exports[`NavList renders with groups 1`] = ` } .c3 { - padding-top: 6px; - padding-bottom: 6px; - padding-left: 16px; - padding-right: 16px; - font-size: 12px; - font-weight: 600; - color: var(--fgColor-muted,var(--color-fg-muted,#656d76)); -} - -.c3 .ActionListGroupHeading { - font-size: var(--text-body-size-small),12px; - font-weight: var(--base-text-weight-semibold,600); - line-height: var(--text-body-lineHeight-small,1.6666); - color: var(--fgColor-muted); -} - -.c3 .ActionListGroupHeadingDescription { - font-size: var(--text-body-size-small,12px); - font-weight: var(--base-text-weight-normal,400); - line-height: var(--text-body-lineHeight-small,1.6666); - color: var(--fgColor-muted); -} - -.c4 { padding-inline-start: 0; } -.c7 { +.c6 { padding-left: 8px; padding-right: 8px; padding-top: 6px; @@ -497,13 +468,13 @@ exports[`NavList renders with groups 1`] = ` color: inherit; } -.c7:hover { +.c6:hover { color: inherit; -webkit-text-decoration: none; text-decoration: none; } -.c8 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -518,7 +489,7 @@ exports[`NavList renders with groups 1`] = ` min-width: 0; } -.c9 { +.c8 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -528,7 +499,7 @@ exports[`NavList renders with groups 1`] = ` line-height: 20px; } -.c11 { +.c10 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -541,11 +512,10 @@ exports[`NavList renders with groups 1`] = ` .c0 { margin: 0; padding-inline-start: 0; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px; } -.c6 { +.c5 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -556,10 +526,8 @@ exports[`NavList renders with groups 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -570,7 +538,7 @@ exports[`NavList renders with groups 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -579,32 +547,32 @@ exports[`NavList renders with groups 1`] = ` background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c6[data-loading] { +.c5[data-loading] { cursor: default; } -.c6[aria-disabled], -.c6[data-inactive] { +.c5[aria-disabled], +.c5[data-inactive] { cursor: not-allowed; } -.c6[aria-disabled] [data-component="ActionList.Checkbox"], -.c6[data-inactive] [data-component="ActionList.Checkbox"] { +.c5[aria-disabled] [data-component="ActionList.Checkbox"], +.c5[data-inactive] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c6[aria-disabled] [data-component="ActionList.Selection"], -.c6[data-inactive] [data-component="ActionList.Selection"] { +.c5[aria-disabled] [data-component="ActionList.Selection"], +.c5[data-inactive] [data-component="ActionList.Selection"] { color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } -.c6 [data-component="ActionList.Item--DividerContainer"] { +.c5 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c6 [data-component="ActionList.Item--DividerContainer"]::before { +.c5 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -615,25 +583,25 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c6:not(:first-of-type) { +.c5:not(:first-of-type) { --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48))); } -[data-component="ActionList.Divider"] + .c5 { +[data-component="ActionList.Divider"] + .c4 { --divider-color: transparent !important; } -.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), -.c6[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { +.c5:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), +.c5[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { --divider-color: transparent; } -.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, -.c6[data-focus-visible-added] + li { +.c5:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c4, +.c5[data-focus-visible-added] + li { --divider-color: transparent; } -.c6::after { +.c5::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -644,12 +612,12 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c6[data-is-active-descendant] { +.c5[data-is-active-descendant] { font-weight: 400; background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c6[data-is-active-descendant]::after { +.c5[data-is-active-descendant]::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -660,7 +628,7 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c10 { +.c9 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -671,10 +639,8 @@ exports[`NavList renders with groups 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -685,39 +651,39 @@ exports[`NavList renders with groups 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; margin-bottom: unset; } -.c10[data-loading] { +.c9[data-loading] { cursor: default; } -.c10[aria-disabled], -.c10[data-inactive] { +.c9[aria-disabled], +.c9[data-inactive] { cursor: not-allowed; } -.c10[aria-disabled] [data-component="ActionList.Checkbox"], -.c10[data-inactive] [data-component="ActionList.Checkbox"] { +.c9[aria-disabled] [data-component="ActionList.Checkbox"], +.c9[data-inactive] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c10[aria-disabled] [data-component="ActionList.Selection"], -.c10[data-inactive] [data-component="ActionList.Selection"] { +.c9[aria-disabled] [data-component="ActionList.Selection"], +.c9[data-inactive] [data-component="ActionList.Selection"] { color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } -.c10 [data-component="ActionList.Item--DividerContainer"] { +.c9 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c10 [data-component="ActionList.Item--DividerContainer"]::before { +.c9 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -728,30 +694,30 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c10:not(:first-of-type) { +.c9:not(:first-of-type) { --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48))); } -[data-component="ActionList.Divider"] + .c5 { +[data-component="ActionList.Divider"] + .c4 { --divider-color: transparent !important; } -.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), -.c10[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { +.c9:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), +.c9[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { --divider-color: transparent; } -.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, -.c10[data-focus-visible-added] + li { +.c9:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c4, +.c9[data-focus-visible-added] + li { --divider-color: transparent; } -.c10[data-is-active-descendant] { +.c9[data-is-active-descendant] { font-weight: 400; background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c10[data-is-active-descendant]::after { +.c9[data-is-active-descendant]::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -763,60 +729,60 @@ exports[`NavList renders with groups 1`] = ` } @media (forced-colors:active) { - .c6:focus, - .c6:focus-visible, - .c6 > a.focus-visible, - .c6[data-is-active-descendant] { + .c5:focus, + .c5:focus-visible, + .c5 > a.focus-visible, + .c5[data-is-active-descendant] { outline: solid 1px transparent !important; } } @media (hover:hover) and (pointer:fine) { - .c6:hover:not([aria-disabled]):not([data-inactive]) { + .c5:hover:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent)); } - .c6:focus-visible, - .c6 > a.focus-visible, - .c6:focus.focus-visible { + .c5:focus-visible, + .c5 > a.focus-visible, + .c5:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } - .c6:active:not([aria-disabled]):not([data-inactive]) { + .c5:active:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } } @media (forced-colors:active) { - .c10:focus, - .c10:focus-visible, - .c10 > a.focus-visible, - .c10[data-is-active-descendant] { + .c9:focus, + .c9:focus-visible, + .c9 > a.focus-visible, + .c9[data-is-active-descendant] { outline: solid 1px transparent !important; } } @media (hover:hover) and (pointer:fine) { - .c10:hover:not([aria-disabled]):not([data-inactive]) { + .c9:hover:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent)); } - .c10:focus-visible, - .c10 > a.focus-visible, - .c10:focus.focus-visible { + .c9:focus-visible, + .c9 > a.focus-visible, + .c9:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } - .c10:active:not([aria-disabled]):not([data-inactive]) { + .c9:active:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } @@ -838,10 +804,11 @@ exports[`NavList renders with groups 1`] = ` class="c2" >

    @@ -850,25 +817,25 @@ exports[`NavList renders with groups 1`] = `