diff --git a/.changeset/new-pans-shout.md b/.changeset/new-pans-shout.md
new file mode 100644
index 00000000000..961312476d0
--- /dev/null
+++ b/.changeset/new-pans-shout.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": minor
+---
+
+Convert ActionList.Heading to CSS Modules
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
new file mode 100644
index 00000000000..c35d8aa199d
Binary files /dev/null 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
new file mode 100644
index 00000000000..5964c04027b
Binary files /dev/null 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
new file mode 100644
index 00000000000..213f7b60742
Binary files /dev/null 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
new file mode 100644
index 00000000000..c35d8aa199d
Binary files /dev/null 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
new file mode 100644
index 00000000000..c35d8aa199d
Binary files /dev/null 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
new file mode 100644
index 00000000000..53b577bddc1
Binary files /dev/null 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
new file mode 100644
index 00000000000..4ca17804028
Binary files /dev/null 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
new file mode 100644
index 00000000000..53b577bddc1
Binary files /dev/null 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
new file mode 100644
index 00000000000..53b577bddc1
Binary files /dev/null and b/.playwright/snapshots/components/ActionList.test.ts-snapshots/Heading-with-Classname-light-tritanopia-linux.png differ
diff --git a/e2e/components/ActionList.test.ts b/e2e/components/ActionList.test.ts
index 6d10201126d..3da80621f8a 100644
--- a/e2e/components/ActionList.test.ts
+++ b/e2e/components/ActionList.test.ts
@@ -712,4 +712,32 @@ test.describe('ActionList', () => {
})
}
})
+
+ test.describe('Heading with Classname', () => {
+ for (const theme of themes) {
+ test.describe(theme, () => {
+ test('default @vrt', async ({page}) => {
+ await visit(page, {
+ id: 'components-actionlist-dev--heading-custom-classname',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+
+ // Default state
+ expect(await page.screenshot()).toMatchSnapshot(`Heading with Classname.${theme}.png`)
+ })
+
+ test('axe @aat', async ({page}) => {
+ await visit(page, {
+ id: 'components-actionlist-dev--heading-custom-classname',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+ await expect(page).toHaveNoViolations()
+ })
+ })
+ }
+ })
})
diff --git a/packages/react/src/ActionList/ActionList.dev.stories.tsx b/packages/react/src/ActionList/ActionList.dev.stories.tsx
index 6d0fc3d1da7..ab7de7a1157 100644
--- a/packages/react/src/ActionList/ActionList.dev.stories.tsx
+++ b/packages/react/src/ActionList/ActionList.dev.stories.tsx
@@ -116,3 +116,16 @@ export const GroupHeadingCustomClassname = () => (
)
+
+export const HeadingCustomClassname = () => (
+
+
+ Filter by
+
+
+ Repositories
+ {}}>app/assets/modules
+ {}}>src/react/components
+
+
+)
diff --git a/packages/react/src/ActionList/ActionList.features.stories.tsx b/packages/react/src/ActionList/ActionList.features.stories.tsx
index 18c5393042c..2cb63ac8fff 100644
--- a/packages/react/src/ActionList/ActionList.features.stories.tsx
+++ b/packages/react/src/ActionList/ActionList.features.stories.tsx
@@ -50,7 +50,9 @@ export const SimpleList = () => (
export const WithVisualListHeading = () => (
- Filter by
+
+ Filter by
+
Repositories
{}}>
diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx
index ae983870427..bf1dfdffae1 100644
--- a/packages/react/src/ActionList/ActionList.test.tsx
+++ b/packages/react/src/ActionList/ActionList.test.tsx
@@ -237,52 +237,6 @@ describe('ActionList', () => {
expect(onClick).toHaveBeenCalled()
})
- it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
- const container = HTMLRender(
-
- Heading
- ,
- )
- const heading = container.getByRole('heading', {level: 1})
- expect(heading).toBeInTheDocument()
- expect(heading).toHaveTextContent('Heading')
- })
- it('should label the action list with the heading id', async () => {
- const {container, getByRole} = HTMLRender(
-
- Heading
- Item
- ,
- )
- const list = container.querySelector('ul')
- const heading = getByRole('heading', {level: 1})
- expect(list).toHaveAttribute('aria-labelledby', heading.id)
- })
- it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => {
- const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
- expect(() =>
- HTMLRender(
-
-
-
- Trigger
-
-
- Heading
- Item
-
-
-
-
- ,
- ),
- ).toThrow(
- "ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.",
- )
- expect(spy).toHaveBeenCalled()
- spy.mockRestore()
- })
-
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(() =>
diff --git a/packages/react/src/ActionList/Heading.module.css b/packages/react/src/ActionList/Heading.module.css
new file mode 100644
index 00000000000..fffceddac23
--- /dev/null
+++ b/packages/react/src/ActionList/Heading.module.css
@@ -0,0 +1,12 @@
+.ActionListHeader {
+ margin-block-end: var(--base-size-8);
+
+ &:where([data-list-variant='full']) {
+ margin-inline-start: var(--base-size-8);
+ }
+
+ &:where([data-list-variant='inset']) {
+ /* stylelint-disable-next-line primer/spacing */
+ margin-inline-start: calc(var(--control-medium-paddingInline-condensed) + var(--base-size-8));
+ }
+}
diff --git a/packages/react/src/ActionList/Heading.test.tsx b/packages/react/src/ActionList/Heading.test.tsx
new file mode 100644
index 00000000000..d8a2e6cbb98
--- /dev/null
+++ b/packages/react/src/ActionList/Heading.test.tsx
@@ -0,0 +1,83 @@
+import {render as HTMLRender} from '@testing-library/react'
+import React from 'react'
+import theme from '../theme'
+import {ActionList} from '.'
+import {BaseStyles, ThemeProvider, ActionMenu} from '..'
+import {FeatureFlags} from '../FeatureFlags'
+
+describe('ActionList.Heading', () => {
+ it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
+ const container = HTMLRender(
+
+ Heading
+ ,
+ )
+ const heading = container.getByRole('heading', {level: 1})
+ expect(heading).toBeInTheDocument()
+ expect(heading).toHaveTextContent('Heading')
+ })
+
+ it('should label the action list with the heading id', async () => {
+ const {container, getByRole} = HTMLRender(
+
+ Heading
+ Item
+ ,
+ )
+ const list = container.querySelector('ul')
+ const heading = getByRole('heading', {level: 1})
+ expect(list).toHaveAttribute('aria-labelledby', heading.id)
+ })
+
+ it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => {
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
+ expect(() =>
+ HTMLRender(
+
+
+
+ Trigger
+
+
+ Heading
+ Item
+
+
+
+
+ ,
+ ),
+ ).toThrow(
+ "ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.",
+ )
+ expect(spy).toHaveBeenCalled()
+ spy.mockRestore()
+ })
+
+ it('should support a custom `className` on the outermost element', () => {
+ const Element = () => {
+ return (
+
+
+ Filter by
+
+
+ )
+ }
+ 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/Heading.tsx b/packages/react/src/ActionList/Heading.tsx
index 1971f9a3886..d024be646c7 100644
--- a/packages/react/src/ActionList/Heading.tsx
+++ b/packages/react/src/ActionList/Heading.tsx
@@ -9,18 +9,26 @@ import {ListContext} from './shared'
import VisuallyHidden from '../_VisuallyHidden'
import {ActionListContainerContext} from './ActionListContainerContext'
import {invariant} from '../utils/invariant'
+import {clsx} from 'clsx'
+import {useFeatureFlag} from '../FeatureFlags'
+import classes from './Heading.module.css'
type HeadingLevels = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
+type HeadingVariants = 'large' | 'medium' | 'small'
export type ActionListHeadingProps = {
as: HeadingLevels
+ size?: HeadingVariants
visuallyHidden?: boolean
+ className?: string
} & SxProp
export const Heading = forwardRef(
- ({as, children, sx = defaultSxProp, visuallyHidden = false, ...props}, forwardedRef) => {
+ ({as, size, children, sx = defaultSxProp, visuallyHidden = false, className, ...props}, forwardedRef) => {
const innerRef = React.useRef(null)
useRefObjectAsForwardedRef(forwardedRef, innerRef)
+ const enabled = useFeatureFlag('primer_react_css_modules_team')
+
const {headingId: headingId, variant: listVariant} = React.useContext(ListContext)
const {container} = React.useContext(ActionListContainerContext)
@@ -37,16 +45,49 @@ export const Heading = forwardRef(
return (
- (styles, sx)}
- {...props}
- >
- {children}
-
+ {enabled ? (
+ sx !== defaultSxProp ? (
+
+ {children}
+
+ ) : (
+
+ {children}
+
+ )
+ ) : (
+ (styles, sx)}
+ className={className}
+ {...props}
+ >
+ {children}
+
+ )}
)
},