diff --git a/.changeset/slimy-starfishes-agree.md b/.changeset/slimy-starfishes-agree.md
new file mode 100644
index 00000000000..be22781c088
--- /dev/null
+++ b/.changeset/slimy-starfishes-agree.md
@@ -0,0 +1,7 @@
+---
+'@primer/react': minor
+---
+
+ActionList: Add ActionList.Heading component
+
+
diff --git a/src/ActionList/ActionList.features.stories.tsx b/src/ActionList/ActionList.features.stories.tsx
index d342d981263..5751f9038b4 100644
--- a/src/ActionList/ActionList.features.stories.tsx
+++ b/src/ActionList/ActionList.features.stories.tsx
@@ -23,6 +23,8 @@ import {
AlertIcon,
TableIcon,
PeopleIcon,
+ FileDirectoryIcon,
+ PlusCircleIcon,
} from '@primer/octicons-react'
export default {
@@ -41,6 +43,98 @@ export const SimpleList = () => (
)
+export const WithVisualListHeading = () => (
+
+ Filter by
+
+ {}}>
+
+
+
+ app/assets/modules
+
+ {}}>
+
+
+
+ src/react/components
+
+ {}}>
+
+
+
+ memex/shared-ui/components
+
+ {}}>
+
+
+
+ views/assets/modules
+
+
+
+
+ {}}>
+
+
+
+ Owner
+
+ {}}>
+
+
+
+ Symbol
+
+ {}}>
+
+
+
+ Exclude archived
+
+
+
+)
+
+export const WithCustomHeading = () => (
+ <>
+
+ Details
+
+
+
+
+
+
+ Readme
+
+
+
+
+
+ MIT License
+
+
+
+
+
+ 1.5k stars
+
+
+
+
+
+ 21 watching
+
+
+
+
+
+ 225 forks
+
+
+ >
+)
export const WithIcons = () => (
diff --git a/src/__tests__/ActionList.test.tsx b/src/ActionList/ActionList.test.tsx
similarity index 74%
rename from src/__tests__/ActionList.test.tsx
rename to src/ActionList/ActionList.test.tsx
index ca070d04305..5613411dda3 100644
--- a/src/__tests__/ActionList.test.tsx
+++ b/src/ActionList/ActionList.test.tsx
@@ -2,9 +2,9 @@ import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
import {axe} from 'jest-axe'
import React from 'react'
import theme from '../theme'
-import {ActionList} from '../ActionList'
+import {ActionList} from '.'
import {behavesAsComponent, checkExports} from '../utils/testing'
-import {BaseStyles, ThemeProvider, SSRProvider} from '..'
+import {BaseStyles, ThemeProvider, SSRProvider, ActionMenu} from '..'
function SimpleActionList(): JSX.Element {
return (
@@ -166,4 +166,52 @@ describe('ActionList', () => {
fireEvent.click(link)
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()
+ })
})
diff --git a/src/ActionList/Heading.tsx b/src/ActionList/Heading.tsx
new file mode 100644
index 00000000000..076d690df28
--- /dev/null
+++ b/src/ActionList/Heading.tsx
@@ -0,0 +1,54 @@
+import React, {forwardRef} from 'react'
+import {BetterSystemStyleObject, SxProp, merge} from '../sx'
+import {defaultSxProp} from '../utils/defaultSxProp'
+import {useRefObjectAsForwardedRef} from '../hooks'
+import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
+import {default as HeadingComponent} from '../Heading'
+import {ListContext} from './List'
+import VisuallyHidden from '../_VisuallyHidden'
+import {ActionListContainerContext} from './ActionListContainerContext'
+import {invariant} from '../utils/invariant'
+
+type HeadingLevels = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
+export type ActionListHeadingProps = {
+ as: HeadingLevels
+ visuallyHidden?: boolean
+} & SxProp
+
+export const Heading = forwardRef(
+ ({as, children, sx = defaultSxProp, visuallyHidden = false, ...props}, forwardedRef) => {
+ const innerRef = React.useRef(null)
+ useRefObjectAsForwardedRef(forwardedRef, innerRef)
+
+ const {headingId: headingId, variant: listVariant} = React.useContext(ListContext)
+ const {container} = React.useContext(ActionListContainerContext)
+
+ // Semantic