Skip to content

Commit 0a0c2a0

Browse files
authored
feat: add leadingVisual to UnderlineNav.Item (#7200)
1 parent 21dd043 commit 0a0c2a0

File tree

7 files changed

+110
-64
lines changed

7 files changed

+110
-64
lines changed

.changeset/old-yaks-win.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+
Add `leadingVisual` prop to `UnderlineNav.Item`

packages/react/src/UnderlineNav/UnderlineNav.docs.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,14 @@
8383
"name": "icon",
8484
"type": "Component",
8585
"defaultValue": "",
86-
"description": "The leading icon comes before item label"
86+
"description": "The leading icon comes before item label",
87+
"deprecated": true
88+
},
89+
{
90+
"name": "leadingVisual",
91+
"type": "React.ReactElement",
92+
"defaultValue": "",
93+
"description": "The leading visual comes before item label"
8794
},
8895
{
8996
"name": "onSelect",
@@ -103,4 +110,5 @@
103110
}
104111
}
105112
]
106-
}
113+
}
114+

packages/react/src/UnderlineNav/UnderlineNav.examples.stories.tsx

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react'
2-
import type {IconProps} from '@primer/octicons-react'
32
import {
43
CodeIcon,
54
IssueOpenedIcon,
@@ -23,7 +22,6 @@ import {
2322
import type {Meta} from '@storybook/react-vite'
2423
import {UnderlineNav} from './index'
2524
import {Avatar, Button, Heading, Link, Text, StateLabel, BranchName} from '..'
26-
import Octicon from '../Octicon'
2725
import classes from './UnderlineNav.examples.stories.module.css'
2826

2927
export default {
@@ -49,33 +47,33 @@ export const PullRequestPage = () => {
4947
</div>
5048
</div>
5149
<UnderlineNav aria-label="Pull Request">
52-
<UnderlineNav.Item icon={CommentDiscussionIcon} counter="0" aria-current="page">
50+
<UnderlineNav.Item leadingVisual={<CommentDiscussionIcon />} counter="0" aria-current="page">
5351
Conversation
5452
</UnderlineNav.Item>
55-
<UnderlineNav.Item counter={3} icon={GitCommitIcon}>
53+
<UnderlineNav.Item counter={3} leadingVisual={<GitCommitIcon />}>
5654
Commits
5755
</UnderlineNav.Item>
58-
<UnderlineNav.Item counter={7} icon={ChecklistIcon}>
56+
<UnderlineNav.Item counter={7} leadingVisual={<ChecklistIcon />}>
5957
Checks
6058
</UnderlineNav.Item>
61-
<UnderlineNav.Item counter={4} icon={FileDiffIcon}>
59+
<UnderlineNav.Item counter={4} leadingVisual={<FileDiffIcon />}>
6260
Files Changes
6361
</UnderlineNav.Item>
6462
</UnderlineNav>
6563
</div>
6664
)
6765
}
6866

69-
const items: {navigation: string; icon: React.FC<IconProps>; counter?: number | string; href?: string}[] = [
70-
{navigation: 'Code', icon: CodeIcon, href: '#code'},
71-
{navigation: 'Issues', icon: IssueOpenedIcon, counter: '12K', href: '#issues'},
72-
{navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13, href: '#pull-requests'},
73-
{navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5, href: '#discussions'},
74-
{navigation: 'Actions', icon: PlayIcon, counter: 4, href: '#actions'},
75-
{navigation: 'Projects', icon: ProjectIcon, counter: 9, href: '#projects'},
76-
{navigation: 'Insights', icon: GraphIcon, counter: '0', href: '#insights'},
77-
{navigation: 'Settings', icon: GearIcon, counter: 10, href: '#settings'},
78-
{navigation: 'Security', icon: ShieldLockIcon, href: '#security'},
67+
const items: {navigation: string; icon: React.ReactElement; counter?: number | string; href?: string}[] = [
68+
{navigation: 'Code', icon: <CodeIcon />, href: '#code'},
69+
{navigation: 'Issues', icon: <IssueOpenedIcon />, counter: '12K', href: '#issues'},
70+
{navigation: 'Pull Requests', icon: <GitPullRequestIcon />, counter: 13, href: '#pull-requests'},
71+
{navigation: 'Discussions', icon: <CommentDiscussionIcon />, counter: 5, href: '#discussions'},
72+
{navigation: 'Actions', icon: <PlayIcon />, counter: 4, href: '#actions'},
73+
{navigation: 'Projects', icon: <ProjectIcon />, counter: 9, href: '#projects'},
74+
{navigation: 'Insights', icon: <GraphIcon />, counter: '0', href: '#insights'},
75+
{navigation: 'Settings', icon: <GearIcon />, counter: 10, href: '#settings'},
76+
{navigation: 'Security', icon: <ShieldLockIcon />, href: '#security'},
7977
]
8078

8179
export const ReposPage = () => {
@@ -86,7 +84,7 @@ export const ReposPage = () => {
8684
{items.map((item, index) => (
8785
<UnderlineNav.Item
8886
key={item.navigation}
89-
icon={item.icon}
87+
leadingVisual={item.icon}
9088
aria-current={index === selectedIndex ? 'page' : undefined}
9189
onSelect={event => {
9290
event.preventDefault()
@@ -102,13 +100,13 @@ export const ReposPage = () => {
102100
)
103101
}
104102

105-
const profileItems: {navigation: string; icon: React.FC<IconProps>; counter?: number | string; href?: string}[] = [
106-
{navigation: 'Overview', icon: BookIcon, href: '#overview'},
107-
{navigation: 'Repositories', icon: RepoIcon, counter: '12', href: '#repositories'},
108-
{navigation: 'Projects', icon: ProjectIcon, counter: 3, href: '#projects'},
109-
{navigation: 'Packages', icon: PackageIcon, counter: '0', href: '#packages'},
110-
{navigation: 'Stars', icon: StarIcon, counter: '0', href: '#stars'},
111-
{navigation: 'Activity', icon: ThreeBarsIcon, counter: 67, href: '#activity'},
103+
const profileItems: {navigation: string; icon: React.ReactElement; counter?: number | string; href?: string}[] = [
104+
{navigation: 'Overview', icon: <BookIcon />, href: '#overview'},
105+
{navigation: 'Repositories', icon: <RepoIcon />, counter: '12', href: '#repositories'},
106+
{navigation: 'Projects', icon: <ProjectIcon />, counter: 3, href: '#projects'},
107+
{navigation: 'Packages', icon: <PackageIcon />, counter: '0', href: '#packages'},
108+
{navigation: 'Stars', icon: <StarIcon />, counter: '0', href: '#stars'},
109+
{navigation: 'Activity', icon: <ThreeBarsIcon />, counter: 67, href: '#activity'},
112110
]
113111

114112
export const ProfilePage = () => {
@@ -120,7 +118,7 @@ export const ProfilePage = () => {
120118
{profileItems.map((item, index) => (
121119
<UnderlineNav.Item
122120
key={item.navigation}
123-
icon={item.icon}
121+
leadingVisual={item.icon}
124122
aria-current={index === selectedIndex ? 'page' : undefined}
125123
onSelect={event => {
126124
event.preventDefault()
@@ -151,7 +149,7 @@ export const ProfilePage = () => {
151149
<div className={classes.ProfileEditSection}>
152150
<Button block>Edit Profile</Button>
153151
<div className={classes.ProfileFollowRow}>
154-
<Octicon icon={PeopleIcon} size={16} className={classes.ProfileFollowerIcon} />
152+
<PeopleIcon size={16} className={classes.ProfileFollowerIcon} />
155153
<Link href="https://github.com" muted className={classes.ProfileFollowerCount}>
156154
47 Followers
157155
</Link>

packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from 'react'
2-
import type {IconProps} from '@primer/octicons-react'
32
import {
43
EyeIcon,
54
CodeIcon,
@@ -35,44 +34,44 @@ export const Default = () => {
3534
export const WithIcons = () => {
3635
return (
3736
<UnderlineNav aria-label="Repository with icons">
38-
<UnderlineNav.Item icon={<CodeIcon />}>Code</UnderlineNav.Item>
39-
<UnderlineNav.Item icon={<EyeIcon />} counter={6}>
37+
<UnderlineNav.Item leadingVisual={<CodeIcon />}>Code</UnderlineNav.Item>
38+
<UnderlineNav.Item leadingVisual={<EyeIcon />} counter={6}>
4039
Issues
4140
</UnderlineNav.Item>
42-
<UnderlineNav.Item aria-current="page" icon={<GitPullRequestIcon />}>
41+
<UnderlineNav.Item aria-current="page" leadingVisual={<GitPullRequestIcon />}>
4342
Pull Requests
4443
</UnderlineNav.Item>
45-
<UnderlineNav.Item icon={<CommentDiscussionIcon />} counter={7}>
44+
<UnderlineNav.Item leadingVisual={<CommentDiscussionIcon />} counter={7}>
4645
Discussions
4746
</UnderlineNav.Item>
48-
<UnderlineNav.Item icon={<ProjectIcon />}>Projects</UnderlineNav.Item>
47+
<UnderlineNav.Item leadingVisual={<ProjectIcon />}>Projects</UnderlineNav.Item>
4948
</UnderlineNav>
5049
)
5150
}
5251

5352
export const WithCounterLabels = () => {
5453
return (
5554
<UnderlineNav aria-label="Repository with counters">
56-
<UnderlineNav.Item aria-current="page" icon={<CodeIcon />} counter="11K">
55+
<UnderlineNav.Item aria-current="page" leadingVisual={<CodeIcon />} counter="11K">
5756
Code
5857
</UnderlineNav.Item>
59-
<UnderlineNav.Item icon={<IssueOpenedIcon />} counter={12}>
58+
<UnderlineNav.Item leadingVisual={<IssueOpenedIcon />} counter={12}>
6059
Issues
6160
</UnderlineNav.Item>
6261
</UnderlineNav>
6362
)
6463
}
6564

66-
const items: {navigation: string; icon: React.FC<IconProps>; counter?: number | string; href?: string}[] = [
67-
{navigation: 'Code', icon: CodeIcon, href: '#code'},
68-
{navigation: 'Issues', icon: IssueOpenedIcon, counter: '12K', href: '#issues'},
69-
{navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13, href: '#pull-requests'},
70-
{navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5, href: '#discussions'},
71-
{navigation: 'Actions', icon: PlayIcon, counter: 4, href: '#actions'},
72-
{navigation: 'Projects', icon: ProjectIcon, counter: 9, href: '#projects'},
73-
{navigation: 'Insights', icon: GraphIcon, counter: '0', href: '#insights'},
74-
{navigation: 'Settings', icon: GearIcon, counter: 10, href: '#settings'},
75-
{navigation: 'Security', icon: ShieldLockIcon, href: '#security'},
65+
const items: {navigation: string; icon: React.ReactElement; counter?: number | string; href?: string}[] = [
66+
{navigation: 'Code', icon: <CodeIcon />, href: '#code'},
67+
{navigation: 'Issues', icon: <IssueOpenedIcon />, counter: '12K', href: '#issues'},
68+
{navigation: 'Pull Requests', icon: <GitPullRequestIcon />, counter: 13, href: '#pull-requests'},
69+
{navigation: 'Discussions', icon: <CommentDiscussionIcon />, counter: 5, href: '#discussions'},
70+
{navigation: 'Actions', icon: <PlayIcon />, counter: 4, href: '#actions'},
71+
{navigation: 'Projects', icon: <ProjectIcon />, counter: 9, href: '#projects'},
72+
{navigation: 'Insights', icon: <GraphIcon />, counter: '0', href: '#insights'},
73+
{navigation: 'Settings', icon: <GearIcon />, counter: 10, href: '#settings'},
74+
{navigation: 'Security', icon: <ShieldLockIcon />, href: '#security'},
7675
]
7776

7877
export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedIndex?: number}) => {
@@ -86,7 +85,7 @@ export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedInd
8685
{items.map((item, index) => (
8786
<UnderlineNav.Item
8887
key={item.navigation}
89-
icon={item.icon}
88+
leadingVisual={item.icon}
9089
aria-current={index === selectedIndex ? 'page' : undefined}
9190
// Set so that navigation in interaction tests does not cause the
9291
// page to load the storybook iframe URL and instead keeps the test in
@@ -134,7 +133,7 @@ export const CountersLoadingState = () => {
134133
{items.map((item, index) => (
135134
<UnderlineNav.Item
136135
key={item.navigation}
137-
icon={item.icon}
136+
leadingVisual={item.icon}
138137
aria-current={index === selectedIndex ? 'page' : undefined}
139138
onSelect={() => setSelectedIndex(index)}
140139
counter={item.counter}

packages/react/src/UnderlineNav/UnderlineNav.figma.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ figma.connect(
3333
}),
3434
},
3535
example: ({label, current, counter, leadingVisual}) => (
36-
<UnderlineNav.Item aria-current={current} counter={counter.count} icon={leadingVisual}>
36+
<UnderlineNav.Item aria-current={current} counter={counter.count} leadingVisual={leadingVisual}>
3737
{label}
3838
</UnderlineNav.Item>
3939
),

packages/react/src/UnderlineNav/UnderlineNav.test.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {describe, expect, it, vi} from 'vitest'
22
import type React from 'react'
33
import {render, screen} from '@testing-library/react'
44
import userEvent from '@testing-library/user-event'
5-
import type {IconProps} from '@primer/octicons-react'
65
import {
76
CodeIcon,
87
IssueOpenedIcon,
@@ -24,16 +23,16 @@ const ResponsiveUnderlineNav = ({
2423
loadingCounters?: boolean
2524
displayExtraEl?: boolean
2625
}) => {
27-
const items: {navigation: string; icon?: React.FC<IconProps>; counter?: number}[] = [
28-
{navigation: 'Code', icon: CodeIcon},
29-
{navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
30-
{navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
31-
{navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
26+
const items: {navigation: string; icon?: React.ReactElement; counter?: number}[] = [
27+
{navigation: 'Code', icon: <CodeIcon />},
28+
{navigation: 'Issues', icon: <IssueOpenedIcon />, counter: 120},
29+
{navigation: 'Pull Requests', icon: <GitPullRequestIcon />, counter: 13},
30+
{navigation: 'Discussions', icon: <CommentDiscussionIcon />, counter: 5},
3231
{navigation: 'Actions', counter: 4},
33-
{navigation: 'Projects', icon: ProjectIcon, counter: 9},
34-
{navigation: 'Insights', icon: GraphIcon},
32+
{navigation: 'Projects', icon: <ProjectIcon />, counter: 9},
33+
{navigation: 'Insights', icon: <GraphIcon />},
3534
{navigation: 'Settings', counter: 10},
36-
{navigation: 'Security', icon: ShieldLockIcon},
35+
{navigation: 'Security', icon: <ShieldLockIcon />},
3736
]
3837

3938
return (
@@ -42,7 +41,7 @@ const ResponsiveUnderlineNav = ({
4241
{items.map(item => (
4342
<UnderlineNav.Item
4443
key={item.navigation}
45-
icon={item.icon}
44+
leadingVisual={item.icon}
4645
aria-current={item.navigation === selectedItemText ? 'page' : undefined}
4746
counter={item.counter}
4847
>
@@ -168,11 +167,13 @@ describe('UnderlineNav', () => {
168167
it('should support icons passed in as an element', () => {
169168
render(
170169
<UnderlineNav aria-label="Repository">
171-
<UnderlineNav.Item aria-current="page" icon={<CodeIcon aria-label="Page one icon" />}>
170+
<UnderlineNav.Item aria-current="page" leadingVisual={<CodeIcon aria-label="Page one icon" />}>
172171
Page one
173172
</UnderlineNav.Item>
174-
<UnderlineNav.Item icon={<IssueOpenedIcon aria-label="Page two icon" />}>Page two</UnderlineNav.Item>
175-
<UnderlineNav.Item icon={<GitPullRequestIcon aria-label="Page three icon" />}>Page three</UnderlineNav.Item>
173+
<UnderlineNav.Item leadingVisual={<IssueOpenedIcon aria-label="Page two icon" />}>Page two</UnderlineNav.Item>
174+
<UnderlineNav.Item leadingVisual={<GitPullRequestIcon aria-label="Page three icon" />}>
175+
Page three
176+
</UnderlineNav.Item>
176177
</UnderlineNav>,
177178
)
178179

@@ -191,6 +192,20 @@ describe('UnderlineNav', () => {
191192
expect(item).toHaveClass('custom-class')
192193
expect(item.className).toContain('UnderlineItem')
193194
})
195+
196+
it('supports the deprecated `icon` prop', () => {
197+
render(
198+
<UnderlineNav aria-label="Test">
199+
<UnderlineNav.Item icon={<CodeIcon data-testid="jsx-element" />}>as jsx element</UnderlineNav.Item>
200+
<UnderlineNav.Item icon={props => <CodeIcon {...props} data-testid="functional-component" />}>
201+
as functional component
202+
</UnderlineNav.Item>
203+
</UnderlineNav>,
204+
)
205+
206+
expect(screen.getByTestId('jsx-element')).toBeInTheDocument()
207+
expect(screen.getByTestId('functional-component')).toBeInTheDocument()
208+
})
194209
})
195210

196211
describe('Keyboard Navigation', () => {

packages/react/src/UnderlineNav/UnderlineNavItem.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,34 @@ export type UnderlineNavItemProps = {
2525
* Primary content for an UnderlineNav
2626
*/
2727
children?: React.ReactNode
28+
2829
/**
2930
* Callback that will trigger both on click selection and keyboard selection.
3031
*/
3132
onSelect?: (event: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>) => void
33+
3234
/**
3335
* Is `UnderlineNav.Item` current page?
3436
*/
3537
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean
38+
3639
/**
3740
* Icon before the text
41+
* @deprecated Use the `leadingVisual` prop instead
3842
*/
3943
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4044
icon?: React.FunctionComponent<IconProps> | React.ReactElement<any>
45+
46+
/**
47+
* Render a visual before the text
48+
*/
49+
leadingVisual?: React.ReactElement
50+
4151
/**
4252
* Renders `UnderlineNav.Item` as given component i.e. react-router's Link
4353
**/
4454
as?: React.ElementType | 'a'
55+
4556
/**
4657
* Counter
4758
*/
@@ -50,7 +61,17 @@ export type UnderlineNavItemProps = {
5061

5162
export const UnderlineNavItem = forwardRef(
5263
(
53-
{as: Component = 'a', href = '#', children, counter, onSelect, 'aria-current': ariaCurrent, icon: Icon, ...props},
64+
{
65+
as: Component = 'a',
66+
href = '#',
67+
children,
68+
counter,
69+
onSelect,
70+
'aria-current': ariaCurrent,
71+
icon: Icon,
72+
leadingVisual,
73+
...props
74+
},
5475
forwardedRef,
5576
) => {
5677
const backupRef = useRef<HTMLElement>(null)
@@ -108,7 +129,7 @@ export const UnderlineNavItem = forwardRef(
108129
onKeyDown={keyDownHandler}
109130
onClick={clickHandler}
110131
counter={counter}
111-
icon={Icon}
132+
icon={leadingVisual ?? Icon}
112133
loadingCounters={loadingCounters}
113134
iconsVisible={iconsVisible}
114135
{...props}

0 commit comments

Comments
 (0)