Skip to content

Commit 844e41f

Browse files
langermankjonrohan
andauthoredDec 6, 2024··
feat(ActionList.Heading): Convert to CSS Modules (#5367)
* heading * lint * Create new-pans-shout.md * use size prop * test(vrt): update snapshots * Update e2e/components/ActionList.test.ts * test(vrt): update snapshots * Update packages/react/src/ActionList/Heading.test.tsx --------- Co-authored-by: langermank <langermank@users.noreply.github.com> Co-authored-by: Jon Rohan <yes@jonrohan.codes> Co-authored-by: jonrohan <jonrohan@users.noreply.github.com>
1 parent 3e0fc7c commit 844e41f

17 files changed

+196
-58
lines changed
 

‎.changeset/new-pans-shout.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
Convert ActionList.Heading to CSS Modules

‎e2e/components/ActionList.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -712,4 +712,32 @@ test.describe('ActionList', () => {
712712
})
713713
}
714714
})
715+
716+
test.describe('Heading with Classname', () => {
717+
for (const theme of themes) {
718+
test.describe(theme, () => {
719+
test('default @vrt', async ({page}) => {
720+
await visit(page, {
721+
id: 'components-actionlist-dev--heading-custom-classname',
722+
globals: {
723+
colorScheme: theme,
724+
},
725+
})
726+
727+
// Default state
728+
expect(await page.screenshot()).toMatchSnapshot(`Heading with Classname.${theme}.png`)
729+
})
730+
731+
test('axe @aat', async ({page}) => {
732+
await visit(page, {
733+
id: 'components-actionlist-dev--heading-custom-classname',
734+
globals: {
735+
colorScheme: theme,
736+
},
737+
})
738+
await expect(page).toHaveNoViolations()
739+
})
740+
})
741+
}
742+
})
715743
})

‎packages/react/src/ActionList/ActionList.dev.stories.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,16 @@ export const GroupHeadingCustomClassname = () => (
116116
</ActionList.Group>
117117
</ActionList>
118118
)
119+
120+
export const HeadingCustomClassname = () => (
121+
<ActionList>
122+
<ActionList.Heading className="testCustomClassnameColor" as="h2">
123+
Filter by
124+
</ActionList.Heading>
125+
<ActionList.Group>
126+
<ActionList.GroupHeading as="h3">Repositories</ActionList.GroupHeading>
127+
<ActionList.Item onClick={() => {}}>app/assets/modules</ActionList.Item>
128+
<ActionList.Item onClick={() => {}}>src/react/components</ActionList.Item>
129+
</ActionList.Group>
130+
</ActionList>
131+
)

‎packages/react/src/ActionList/ActionList.features.stories.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export const SimpleList = () => (
5050

5151
export const WithVisualListHeading = () => (
5252
<ActionList>
53-
<ActionList.Heading as="h2">Filter by</ActionList.Heading>
53+
<ActionList.Heading as="h2" size="small">
54+
Filter by
55+
</ActionList.Heading>
5456
<ActionList.Group>
5557
<ActionList.GroupHeading as="h3">Repositories</ActionList.GroupHeading>
5658
<ActionList.Item onClick={() => {}}>

‎packages/react/src/ActionList/ActionList.test.tsx

-46
Original file line numberDiff line numberDiff line change
@@ -237,52 +237,6 @@ describe('ActionList', () => {
237237
expect(onClick).toHaveBeenCalled()
238238
})
239239

240-
it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
241-
const container = HTMLRender(
242-
<ActionList>
243-
<ActionList.Heading as="h1">Heading</ActionList.Heading>
244-
</ActionList>,
245-
)
246-
const heading = container.getByRole('heading', {level: 1})
247-
expect(heading).toBeInTheDocument()
248-
expect(heading).toHaveTextContent('Heading')
249-
})
250-
it('should label the action list with the heading id', async () => {
251-
const {container, getByRole} = HTMLRender(
252-
<ActionList>
253-
<ActionList.Heading as="h1">Heading</ActionList.Heading>
254-
<ActionList.Item>Item</ActionList.Item>
255-
</ActionList>,
256-
)
257-
const list = container.querySelector('ul')
258-
const heading = getByRole('heading', {level: 1})
259-
expect(list).toHaveAttribute('aria-labelledby', heading.id)
260-
})
261-
it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => {
262-
const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
263-
expect(() =>
264-
HTMLRender(
265-
<ThemeProvider theme={theme}>
266-
<BaseStyles>
267-
<ActionMenu open={true}>
268-
<ActionMenu.Button>Trigger</ActionMenu.Button>
269-
<ActionMenu.Overlay>
270-
<ActionList>
271-
<ActionList.Heading as="h1">Heading</ActionList.Heading>
272-
<ActionList.Item>Item</ActionList.Item>
273-
</ActionList>
274-
</ActionMenu.Overlay>
275-
</ActionMenu>
276-
</BaseStyles>
277-
</ThemeProvider>,
278-
),
279-
).toThrow(
280-
"ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.",
281-
)
282-
expect(spy).toHaveBeenCalled()
283-
spy.mockRestore()
284-
})
285-
286240
it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => {
287241
const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
288242
expect(() =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.ActionListHeader {
2+
margin-block-end: var(--base-size-8);
3+
4+
&:where([data-list-variant='full']) {
5+
margin-inline-start: var(--base-size-8);
6+
}
7+
8+
&:where([data-list-variant='inset']) {
9+
/* stylelint-disable-next-line primer/spacing */
10+
margin-inline-start: calc(var(--control-medium-paddingInline-condensed) + var(--base-size-8));
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {render as HTMLRender} from '@testing-library/react'
2+
import React from 'react'
3+
import theme from '../theme'
4+
import {ActionList} from '.'
5+
import {BaseStyles, ThemeProvider, ActionMenu} from '..'
6+
import {FeatureFlags} from '../FeatureFlags'
7+
8+
describe('ActionList.Heading', () => {
9+
it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
10+
const container = HTMLRender(
11+
<ActionList>
12+
<ActionList.Heading as="h1">Heading</ActionList.Heading>
13+
</ActionList>,
14+
)
15+
const heading = container.getByRole('heading', {level: 1})
16+
expect(heading).toBeInTheDocument()
17+
expect(heading).toHaveTextContent('Heading')
18+
})
19+
20+
it('should label the action list with the heading id', async () => {
21+
const {container, getByRole} = HTMLRender(
22+
<ActionList>
23+
<ActionList.Heading as="h1">Heading</ActionList.Heading>
24+
<ActionList.Item>Item</ActionList.Item>
25+
</ActionList>,
26+
)
27+
const list = container.querySelector('ul')
28+
const heading = getByRole('heading', {level: 1})
29+
expect(list).toHaveAttribute('aria-labelledby', heading.id)
30+
})
31+
32+
it('should throw an error when ActionList.Heading is used within ActionMenu context', async () => {
33+
const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
34+
expect(() =>
35+
HTMLRender(
36+
<ThemeProvider theme={theme}>
37+
<BaseStyles>
38+
<ActionMenu open={true}>
39+
<ActionMenu.Button>Trigger</ActionMenu.Button>
40+
<ActionMenu.Overlay>
41+
<ActionList>
42+
<ActionList.Heading as="h1">Heading</ActionList.Heading>
43+
<ActionList.Item>Item</ActionList.Item>
44+
</ActionList>
45+
</ActionMenu.Overlay>
46+
</ActionMenu>
47+
</BaseStyles>
48+
</ThemeProvider>,
49+
),
50+
).toThrow(
51+
"ActionList.Heading shouldn't be used within an ActionMenu container. Menus are labelled by the menu button's name.",
52+
)
53+
expect(spy).toHaveBeenCalled()
54+
spy.mockRestore()
55+
})
56+
57+
it('should support a custom `className` on the outermost element', () => {
58+
const Element = () => {
59+
return (
60+
<ActionList>
61+
<ActionList.Heading as="h2" className="test-class-name">
62+
Filter by
63+
</ActionList.Heading>
64+
</ActionList>
65+
)
66+
}
67+
const FeatureFlagElement = () => {
68+
return (
69+
<FeatureFlags
70+
flags={{
71+
primer_react_css_modules_team: true,
72+
primer_react_css_modules_staff: true,
73+
primer_react_css_modules_ga: true,
74+
}}
75+
>
76+
<Element />
77+
</FeatureFlags>
78+
)
79+
}
80+
expect(HTMLRender(<FeatureFlagElement />).container.querySelector('h2')).toHaveClass('test-class-name')
81+
expect(HTMLRender(<Element />).container.querySelector('h2')).toHaveClass('test-class-name')
82+
})
83+
})

‎packages/react/src/ActionList/Heading.tsx

+52-11
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,26 @@ import {ListContext} from './shared'
99
import VisuallyHidden from '../_VisuallyHidden'
1010
import {ActionListContainerContext} from './ActionListContainerContext'
1111
import {invariant} from '../utils/invariant'
12+
import {clsx} from 'clsx'
13+
import {useFeatureFlag} from '../FeatureFlags'
14+
import classes from './Heading.module.css'
1215

1316
type HeadingLevels = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
17+
type HeadingVariants = 'large' | 'medium' | 'small'
1418
export type ActionListHeadingProps = {
1519
as: HeadingLevels
20+
size?: HeadingVariants
1621
visuallyHidden?: boolean
22+
className?: string
1723
} & SxProp
1824

1925
export const Heading = forwardRef(
20-
({as, children, sx = defaultSxProp, visuallyHidden = false, ...props}, forwardedRef) => {
26+
({as, size, children, sx = defaultSxProp, visuallyHidden = false, className, ...props}, forwardedRef) => {
2127
const innerRef = React.useRef<HTMLHeadingElement>(null)
2228
useRefObjectAsForwardedRef(forwardedRef, innerRef)
2329

30+
const enabled = useFeatureFlag('primer_react_css_modules_team')
31+
2432
const {headingId: headingId, variant: listVariant} = React.useContext(ListContext)
2533
const {container} = React.useContext(ActionListContainerContext)
2634

@@ -37,16 +45,49 @@ export const Heading = forwardRef(
3745

3846
return (
3947
<VisuallyHidden isVisible={!visuallyHidden}>
40-
<HeadingComponent
41-
as={as}
42-
ref={innerRef}
43-
// use custom id if it is provided. Otherwise, use the id from the context
44-
id={props.id ?? headingId}
45-
sx={merge<BetterSystemStyleObject>(styles, sx)}
46-
{...props}
47-
>
48-
{children}
49-
</HeadingComponent>
48+
{enabled ? (
49+
sx !== defaultSxProp ? (
50+
<HeadingComponent
51+
as={as}
52+
variant={size}
53+
ref={innerRef}
54+
// use custom id if it is provided. Otherwise, use the id from the context
55+
id={props.id ?? headingId}
56+
className={clsx(className, classes.ActionListHeader)}
57+
data-list-variant={listVariant}
58+
sx={sx}
59+
{...props}
60+
>
61+
{children}
62+
</HeadingComponent>
63+
) : (
64+
<HeadingComponent
65+
as={as}
66+
variant={size}
67+
ref={innerRef}
68+
// use custom id if it is provided. Otherwise, use the id from the context
69+
id={props.id ?? headingId}
70+
className={clsx(className, classes.ActionListHeader)}
71+
data-list-variant={listVariant}
72+
{...props}
73+
>
74+
{children}
75+
</HeadingComponent>
76+
)
77+
) : (
78+
<HeadingComponent
79+
as={as}
80+
variant={size}
81+
ref={innerRef}
82+
// use custom id if it is provided. Otherwise, use the id from the context
83+
id={props.id ?? headingId}
84+
sx={merge<BetterSystemStyleObject>(styles, sx)}
85+
className={className}
86+
{...props}
87+
>
88+
{children}
89+
</HeadingComponent>
90+
)}
5091
</VisuallyHidden>
5192
)
5293
},

0 commit comments

Comments
 (0)
Please sign in to comment.