Skip to content

Commit 851c857

Browse files
authored
Adds NavList.GroupHeading component (#5106)
* adds NavList.GroupHeading component * Create twelve-kings-confess.md * re-styles NavList.GroupHeading links to be fgColor-default * guards against 'title' AND 'NavList.GroupHeading' being used together, adds tests, updates docs * rm title prop from NavList.GroupHeading story
1 parent 016f760 commit 851c857

File tree

8 files changed

+147
-27
lines changed

8 files changed

+147
-27
lines changed

.changeset/twelve-kings-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Adds NavList.GroupHeading component that can be used instead of the ActionList.Group 'title' prop if you need to render something besides a string

packages/react/src/ActionList/Group.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const Group: React.FC<React.PropsWithChildren<ActionListGroupProps>> = ({
107107
)
108108
}
109109

110-
export type GroupHeadingProps = Pick<ActionListGroupProps, 'variant' | 'auxiliaryText'> &
110+
export type ActionListGroupHeadingProps = Pick<ActionListGroupProps, 'variant' | 'auxiliaryText'> &
111111
Omit<ActionListHeadingProps, 'as'> &
112112
SxProp &
113113
React.HTMLAttributes<HTMLElement> & {
@@ -123,7 +123,7 @@ export type GroupHeadingProps = Pick<ActionListGroupProps, 'variant' | 'auxiliar
123123
* hidden from the accessibility tree due to the limitation of listbox children. https://w3c.github.io/aria/#listbox
124124
* groups under menu or listbox are labelled by `aria-label`
125125
*/
126-
export const GroupHeading: React.FC<React.PropsWithChildren<GroupHeadingProps>> = ({
126+
export const GroupHeading: React.FC<React.PropsWithChildren<ActionListGroupHeadingProps>> = ({
127127
as,
128128
variant,
129129
// We are not recommending this prop to be used, it should only be used internally for incremental rollout.

packages/react/src/ActionList/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {LeadingVisual, TrailingVisual} from './Visuals'
99
import {Heading} from './Heading'
1010

1111
export type {ActionListProps} from './shared'
12-
export type {ActionListGroupProps} from './Group'
12+
export type {ActionListGroupProps, ActionListGroupHeadingProps} from './Group'
1313
export type {ActionListItemProps} from './shared'
1414
export type {ActionListLinkItemProps} from './LinkItem'
1515
export type {ActionListDividerProps} from './Divider'

packages/react/src/NavList/NavList.docs.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@
113113
},
114114
{
115115
"name": "NavList.Group",
116+
"props": [
117+
{
118+
"name": "title",
119+
"type": "string",
120+
"description": "The text that gets rendered as the group's heading. Alternatively, you can pass the `NavList.GroupHeading` component as a child of `NavList.Group`.\n If both `title` and `NavList.GroupHeading` are passed, `NavList.GroupHeading` will be rendered as the heading."
121+
},
122+
{
123+
"name": "sx",
124+
"type": "SystemStyleObject"
125+
},
126+
{
127+
"name": "ref",
128+
"type": "React.RefObject<HTMLElement>"
129+
}
130+
]
131+
},
132+
{
133+
"name": "NavList.GroupHeading",
116134
"props": [
117135
{
118136
"name": "sx",

packages/react/src/NavList/NavList.stories.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,34 @@ export const WithGroup: StoryFn = () => (
247247
</PageLayout>
248248
)
249249

250+
export const WithGroupHeadingLinks: StoryFn = () => (
251+
<PageLayout>
252+
<PageLayout.Pane position="start">
253+
<NavList>
254+
<NavList.Group>
255+
<NavList.GroupHeading>
256+
<a href="#group-1">Group 1</a>
257+
</NavList.GroupHeading>
258+
<NavList.Item aria-current="true" href="#">
259+
Item 1A
260+
</NavList.Item>
261+
<NavList.Item href="#">Item 1B</NavList.Item>
262+
<NavList.Item href="#">Item 1C</NavList.Item>
263+
</NavList.Group>
264+
<NavList.Group>
265+
<NavList.GroupHeading>
266+
<a href="#group-2">Group 2</a>
267+
</NavList.GroupHeading>
268+
<NavList.Item href="#">Item 2A</NavList.Item>
269+
<NavList.Item href="#">Item 2B</NavList.Item>
270+
<NavList.Item href="#">Item 2C</NavList.Item>
271+
</NavList.Group>
272+
</NavList>
273+
</PageLayout.Pane>
274+
<PageLayout.Content></PageLayout.Content>
275+
</PageLayout>
276+
)
277+
250278
export const WithTrailingAction = () => {
251279
return (
252280
<PageLayout>

packages/react/src/NavList/NavList.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ describe('NavList', () => {
6363
expect(container).toMatchSnapshot()
6464
})
6565

66+
it('only shows NavList.GroupHeading when NavList.Group `title` prop is passed AND NavList.GroupHeading is a child', () => {
67+
const {getByText} = render(
68+
<ThemeProvider>
69+
<NavList>
70+
<NavList.Group title="Overview">
71+
<NavList.GroupHeading>Group heading</NavList.GroupHeading>
72+
<NavList.Item href="/getting-started" aria-current="page">
73+
Getting started
74+
</NavList.Item>
75+
</NavList.Group>
76+
<NavList.Group title="Components">
77+
<NavList.Item href="/Avatar">Avatar</NavList.Item>
78+
</NavList.Group>
79+
</NavList>
80+
</ThemeProvider>,
81+
)
82+
const groupHeading = getByText('Group heading')
83+
const groupTitle = getByText('Overview')
84+
85+
expect(groupHeading).toBeVisible()
86+
expect(groupTitle).not.toBeVisible()
87+
})
88+
6689
it('supports TrailingAction', async () => {
6790
const {getByRole} = render(
6891
<NavList>

packages/react/src/NavList/NavList.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ActionListDividerProps,
88
ActionListLeadingVisualProps,
99
ActionListTrailingVisualProps,
10+
ActionListGroupHeadingProps,
1011
} from '../ActionList'
1112
import {ActionList} from '../ActionList'
1213
import {ActionListContainerContext} from '../ActionList/ActionListContainerContext'
@@ -279,9 +280,21 @@ const Group: React.FC<NavListGroupProps> = ({title, children, sx: sxProp = defau
279280
<>
280281
{/* Hide divider if the group is the first item in the list */}
281282
<ActionList.Divider sx={{'&:first-child': {display: 'none'}}} />
282-
<ActionList.Group {...props} sx={sxProp}>
283+
<ActionList.Group
284+
{...props}
285+
// If somebody tries to pass the `title` prop AND a `NavList.GroupHeading` as a child, hide the `ActionList.GroupHeading`
286+
sx={merge<SxProp['sx']>(sxProp, {
287+
':has([data-component="NavList.GroupHeading"]):has([data-component="ActionList.GroupHeading"])': {
288+
'[data-component="ActionList.GroupHeading"]': {display: 'none'},
289+
},
290+
})}
291+
>
283292
{/* Setting up the default value for the heading level. TODO: API update to give flexibility to NavList.Group title's heading level */}
284-
{title ? <ActionList.GroupHeading as="h3">{title}</ActionList.GroupHeading> : null}
293+
{title ? (
294+
<ActionList.GroupHeading as="h3" data-component="ActionList.GroupHeading">
295+
{title}
296+
</ActionList.GroupHeading>
297+
) : null}
285298
{children}
286299
</ActionList.Group>
287300
</>
@@ -290,6 +303,32 @@ const Group: React.FC<NavListGroupProps> = ({title, children, sx: sxProp = defau
290303

291304
Group.displayName = 'NavList.Group'
292305

306+
export type NavListGroupHeadingProps = ActionListGroupHeadingProps
307+
308+
/**
309+
* This is an alternative to the `title` prop on `NavList.Group`.
310+
* It was primarily added to allow links in group headings.
311+
*/
312+
const GroupHeading: React.FC<NavListGroupHeadingProps> = ({as = 'h3', sx: sxProp = defaultSxProp, ...rest}) => {
313+
return (
314+
<ActionList.GroupHeading
315+
as={as}
316+
sx={merge<SxProp['sx']>(
317+
{
318+
'> a {': {
319+
color: 'var(--fgColor-default)',
320+
textDecoration: 'inherit',
321+
':hover': {textDecoration: 'underline'},
322+
},
323+
},
324+
sxProp,
325+
)}
326+
data-component="NavList.GroupHeading"
327+
{...rest}
328+
/>
329+
)
330+
}
331+
293332
// ----------------------------------------------------------------------------
294333
// Export
295334

@@ -301,4 +340,5 @@ export const NavList = Object.assign(Root, {
301340
TrailingAction,
302341
Divider,
303342
Group,
343+
GroupHeading,
304344
})

packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,10 @@ exports[`NavList renders with groups 1`] = `
464464
margin-top: 8px;
465465
}
466466
467+
.c2:has([data-component="NavList.GroupHeading"]):has([data-component="ActionList.GroupHeading"]) [data-component="ActionList.GroupHeading"] {
468+
display: none;
469+
}
470+
467471
.c3 {
468472
padding-top: 6px;
469473
padding-bottom: 6px;
@@ -872,6 +876,7 @@ exports[`NavList renders with groups 1`] = `
872876
>
873877
<h3
874878
class="ActionListGroupHeading Heading"
879+
data-component="ActionList.GroupHeading"
875880
id=":r7:"
876881
>
877882
Overview
@@ -920,6 +925,7 @@ exports[`NavList renders with groups 1`] = `
920925
>
921926
<h3
922927
class="ActionListGroupHeading Heading"
928+
data-component="ActionList.GroupHeading"
923929
id=":r9:"
924930
>
925931
Components
@@ -1389,15 +1395,15 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
13891395
class="c0"
13901396
>
13911397
<li
1392-
aria-labelledby=":r26:"
1398+
aria-labelledby=":r2c:"
13931399
class="c1 c2"
13941400
>
13951401
<button
1396-
aria-controls=":r27:"
1402+
aria-controls=":r2d:"
13971403
aria-expanded="true"
1398-
aria-labelledby=":r26:--label :r26:--trailing-visual "
1404+
aria-labelledby=":r2c:--label :r2c:--trailing-visual "
13991405
class="c3 c4"
1400-
id=":r26:"
1406+
id=":r2c:"
14011407
tabindex="0"
14021408
>
14031409
<div
@@ -1409,13 +1415,13 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
14091415
>
14101416
<span
14111417
class="c1 c7"
1412-
id=":r26:--label"
1418+
id=":r2c:--label"
14131419
>
14141420
Item
14151421
</span>
14161422
<span
14171423
class="c1 c8"
1418-
id=":r26:--trailing-visual"
1424+
id=":r2c:--trailing-visual"
14191425
>
14201426
<svg
14211427
aria-hidden="true"
@@ -1437,19 +1443,19 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
14371443
</button>
14381444
<div>
14391445
<ul
1440-
aria-labelledby=":r26:"
1446+
aria-labelledby=":r2c:"
14411447
class="c1 c10"
1442-
id=":r27:"
1448+
id=":r2d:"
14431449
>
14441450
<li
14451451
class="c3 c11"
14461452
>
14471453
<a
14481454
aria-current="page"
1449-
aria-labelledby=":r29:--label "
1455+
aria-labelledby=":r2f:--label "
14501456
class="c12 c13"
14511457
href="#"
1452-
id=":r29:"
1458+
id=":r2f:"
14531459
tabindex="0"
14541460
>
14551461
<div
@@ -1458,7 +1464,7 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
14581464
>
14591465
<span
14601466
class="c1 c14"
1461-
id=":r29:--label"
1467+
id=":r2f:--label"
14621468
>
14631469
Sub Item
14641470
</span>
@@ -1923,15 +1929,15 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
19231929
class="c0"
19241930
>
19251931
<li
1926-
aria-labelledby=":r20:"
1932+
aria-labelledby=":r26:"
19271933
class="c1 c2"
19281934
>
19291935
<button
1930-
aria-controls=":r21:"
1936+
aria-controls=":r27:"
19311937
aria-expanded="false"
1932-
aria-labelledby=":r20:--label :r20:--trailing-visual "
1938+
aria-labelledby=":r26:--label :r26:--trailing-visual "
19331939
class="c3 c4"
1934-
id=":r20:"
1940+
id=":r26:"
19351941
tabindex="0"
19361942
>
19371943
<div
@@ -1943,13 +1949,13 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
19431949
>
19441950
<span
19451951
class="c1 c7"
1946-
id=":r20:--label"
1952+
id=":r26:--label"
19471953
>
19481954
Item
19491955
</span>
19501956
<span
19511957
class="c1 c8"
1952-
id=":r20:--trailing-visual"
1958+
id=":r26:--trailing-visual"
19531959
>
19541960
<svg
19551961
aria-hidden="true"
@@ -1971,19 +1977,19 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
19711977
</button>
19721978
<div>
19731979
<ul
1974-
aria-labelledby=":r20:"
1980+
aria-labelledby=":r26:"
19751981
class="c1 c10"
1976-
id=":r21:"
1982+
id=":r27:"
19771983
>
19781984
<li
19791985
class="c3 c11"
19801986
>
19811987
<a
19821988
aria-current="page"
1983-
aria-labelledby=":r23:--label "
1989+
aria-labelledby=":r29:--label "
19841990
class="c12 c13"
19851991
href="#"
1986-
id=":r23:"
1992+
id=":r29:"
19871993
tabindex="0"
19881994
>
19891995
<div
@@ -1992,7 +1998,7 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
19921998
>
19931999
<span
19942000
class="c1 c7"
1995-
id=":r23:--label"
2001+
id=":r29:--label"
19962002
>
19972003
Sub Item
19982004
</span>

0 commit comments

Comments
 (0)