Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/old-yaks-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add `leadingVisual` prop to `UnderlineNav.Item`
12 changes: 10 additions & 2 deletions packages/react/src/UnderlineNav/UnderlineNav.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,14 @@
"name": "icon",
"type": "Component",
"defaultValue": "",
"description": "The leading icon comes before item label"
"description": "The leading icon comes before item label",
"deprecated": true
},
{
"name": "leadingVisual",
"type": "React.ReactElement",
"defaultValue": "",
"description": "The leading visual comes before item label"
},
{
"name": "onSelect",
Expand All @@ -103,4 +110,5 @@
}
}
]
}
}

50 changes: 24 additions & 26 deletions packages/react/src/UnderlineNav/UnderlineNav.examples.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import type {IconProps} from '@primer/octicons-react'
import {
CodeIcon,
IssueOpenedIcon,
Expand All @@ -23,7 +22,6 @@ import {
import type {Meta} from '@storybook/react-vite'
import {UnderlineNav} from './index'
import {Avatar, Button, Heading, Link, Text, StateLabel, BranchName} from '..'
import Octicon from '../Octicon'
import classes from './UnderlineNav.examples.stories.module.css'

export default {
Expand All @@ -49,33 +47,33 @@ export const PullRequestPage = () => {
</div>
</div>
<UnderlineNav aria-label="Pull Request">
<UnderlineNav.Item icon={CommentDiscussionIcon} counter="0" aria-current="page">
<UnderlineNav.Item leadingVisual={<CommentDiscussionIcon />} counter="0" aria-current="page">
Conversation
</UnderlineNav.Item>
<UnderlineNav.Item counter={3} icon={GitCommitIcon}>
<UnderlineNav.Item counter={3} leadingVisual={<GitCommitIcon />}>
Commits
</UnderlineNav.Item>
<UnderlineNav.Item counter={7} icon={ChecklistIcon}>
<UnderlineNav.Item counter={7} leadingVisual={<ChecklistIcon />}>
Checks
</UnderlineNav.Item>
<UnderlineNav.Item counter={4} icon={FileDiffIcon}>
<UnderlineNav.Item counter={4} leadingVisual={<FileDiffIcon />}>
Files Changes
</UnderlineNav.Item>
</UnderlineNav>
</div>
)
}

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

export const ReposPage = () => {
Expand All @@ -86,7 +84,7 @@ export const ReposPage = () => {
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
icon={item.icon}
leadingVisual={item.icon}
aria-current={index === selectedIndex ? 'page' : undefined}
onSelect={event => {
event.preventDefault()
Expand All @@ -102,13 +100,13 @@ export const ReposPage = () => {
)
}

const profileItems: {navigation: string; icon: React.FC<IconProps>; counter?: number | string; href?: string}[] = [
{navigation: 'Overview', icon: BookIcon, href: '#overview'},
{navigation: 'Repositories', icon: RepoIcon, counter: '12', href: '#repositories'},
{navigation: 'Projects', icon: ProjectIcon, counter: 3, href: '#projects'},
{navigation: 'Packages', icon: PackageIcon, counter: '0', href: '#packages'},
{navigation: 'Stars', icon: StarIcon, counter: '0', href: '#stars'},
{navigation: 'Activity', icon: ThreeBarsIcon, counter: 67, href: '#activity'},
const profileItems: {navigation: string; icon: React.ReactElement; counter?: number | string; href?: string}[] = [
{navigation: 'Overview', icon: <BookIcon />, href: '#overview'},
{navigation: 'Repositories', icon: <RepoIcon />, counter: '12', href: '#repositories'},
{navigation: 'Projects', icon: <ProjectIcon />, counter: 3, href: '#projects'},
{navigation: 'Packages', icon: <PackageIcon />, counter: '0', href: '#packages'},
{navigation: 'Stars', icon: <StarIcon />, counter: '0', href: '#stars'},
{navigation: 'Activity', icon: <ThreeBarsIcon />, counter: 67, href: '#activity'},
]

export const ProfilePage = () => {
Expand All @@ -120,7 +118,7 @@ export const ProfilePage = () => {
{profileItems.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
icon={item.icon}
leadingVisual={item.icon}
aria-current={index === selectedIndex ? 'page' : undefined}
onSelect={event => {
event.preventDefault()
Expand Down Expand Up @@ -151,7 +149,7 @@ export const ProfilePage = () => {
<div className={classes.ProfileEditSection}>
<Button block>Edit Profile</Button>
<div className={classes.ProfileFollowRow}>
<Octicon icon={PeopleIcon} size={16} className={classes.ProfileFollowerIcon} />
<PeopleIcon size={16} className={classes.ProfileFollowerIcon} />
<Link href="https://github.com" muted className={classes.ProfileFollowerCount}>
47 Followers
</Link>
Expand Down
39 changes: 19 additions & 20 deletions packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react'
import type {IconProps} from '@primer/octicons-react'
import {
EyeIcon,
CodeIcon,
Expand Down Expand Up @@ -35,44 +34,44 @@ export const Default = () => {
export const WithIcons = () => {
return (
<UnderlineNav aria-label="Repository with icons">
<UnderlineNav.Item icon={<CodeIcon />}>Code</UnderlineNav.Item>
<UnderlineNav.Item icon={<EyeIcon />} counter={6}>
<UnderlineNav.Item leadingVisual={<CodeIcon />}>Code</UnderlineNav.Item>
<UnderlineNav.Item leadingVisual={<EyeIcon />} counter={6}>
Issues
</UnderlineNav.Item>
<UnderlineNav.Item aria-current="page" icon={<GitPullRequestIcon />}>
<UnderlineNav.Item aria-current="page" leadingVisual={<GitPullRequestIcon />}>
Pull Requests
</UnderlineNav.Item>
<UnderlineNav.Item icon={<CommentDiscussionIcon />} counter={7}>
<UnderlineNav.Item leadingVisual={<CommentDiscussionIcon />} counter={7}>
Discussions
</UnderlineNav.Item>
<UnderlineNav.Item icon={<ProjectIcon />}>Projects</UnderlineNav.Item>
<UnderlineNav.Item leadingVisual={<ProjectIcon />}>Projects</UnderlineNav.Item>
</UnderlineNav>
)
}

export const WithCounterLabels = () => {
return (
<UnderlineNav aria-label="Repository with counters">
<UnderlineNav.Item aria-current="page" icon={<CodeIcon />} counter="11K">
<UnderlineNav.Item aria-current="page" leadingVisual={<CodeIcon />} counter="11K">
Code
</UnderlineNav.Item>
<UnderlineNav.Item icon={<IssueOpenedIcon />} counter={12}>
<UnderlineNav.Item leadingVisual={<IssueOpenedIcon />} counter={12}>
Issues
</UnderlineNav.Item>
</UnderlineNav>
)
}

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

export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedIndex?: number}) => {
Expand All @@ -86,7 +85,7 @@ export const OverflowTemplate = ({initialSelectedIndex = 1}: {initialSelectedInd
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
icon={item.icon}
leadingVisual={item.icon}
aria-current={index === selectedIndex ? 'page' : undefined}
// Set so that navigation in interaction tests does not cause the
// page to load the storybook iframe URL and instead keeps the test in
Expand Down Expand Up @@ -134,7 +133,7 @@ export const CountersLoadingState = () => {
{items.map((item, index) => (
<UnderlineNav.Item
key={item.navigation}
icon={item.icon}
leadingVisual={item.icon}
aria-current={index === selectedIndex ? 'page' : undefined}
onSelect={() => setSelectedIndex(index)}
counter={item.counter}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/UnderlineNav/UnderlineNav.figma.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ figma.connect(
}),
},
example: ({label, current, counter, leadingVisual}) => (
<UnderlineNav.Item aria-current={current} counter={counter.count} icon={leadingVisual}>
<UnderlineNav.Item aria-current={current} counter={counter.count} leadingVisual={leadingVisual}>
{label}
</UnderlineNav.Item>
),
Expand Down
41 changes: 28 additions & 13 deletions packages/react/src/UnderlineNav/UnderlineNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {describe, expect, it, vi} from 'vitest'
import type React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type {IconProps} from '@primer/octicons-react'
import {
CodeIcon,
IssueOpenedIcon,
Expand All @@ -24,16 +23,16 @@ const ResponsiveUnderlineNav = ({
loadingCounters?: boolean
displayExtraEl?: boolean
}) => {
const items: {navigation: string; icon?: React.FC<IconProps>; counter?: number}[] = [
{navigation: 'Code', icon: CodeIcon},
{navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
{navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
{navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
const items: {navigation: string; icon?: React.ReactElement; counter?: number}[] = [
{navigation: 'Code', icon: <CodeIcon />},
{navigation: 'Issues', icon: <IssueOpenedIcon />, counter: 120},
{navigation: 'Pull Requests', icon: <GitPullRequestIcon />, counter: 13},
{navigation: 'Discussions', icon: <CommentDiscussionIcon />, counter: 5},
{navigation: 'Actions', counter: 4},
{navigation: 'Projects', icon: ProjectIcon, counter: 9},
{navigation: 'Insights', icon: GraphIcon},
{navigation: 'Projects', icon: <ProjectIcon />, counter: 9},
{navigation: 'Insights', icon: <GraphIcon />},
{navigation: 'Settings', counter: 10},
{navigation: 'Security', icon: ShieldLockIcon},
{navigation: 'Security', icon: <ShieldLockIcon />},
]

return (
Expand All @@ -42,7 +41,7 @@ const ResponsiveUnderlineNav = ({
{items.map(item => (
<UnderlineNav.Item
key={item.navigation}
icon={item.icon}
leadingVisual={item.icon}
aria-current={item.navigation === selectedItemText ? 'page' : undefined}
counter={item.counter}
>
Expand Down Expand Up @@ -168,11 +167,13 @@ describe('UnderlineNav', () => {
it('should support icons passed in as an element', () => {
render(
<UnderlineNav aria-label="Repository">
<UnderlineNav.Item aria-current="page" icon={<CodeIcon aria-label="Page one icon" />}>
<UnderlineNav.Item aria-current="page" leadingVisual={<CodeIcon aria-label="Page one icon" />}>
Page one
</UnderlineNav.Item>
<UnderlineNav.Item icon={<IssueOpenedIcon aria-label="Page two icon" />}>Page two</UnderlineNav.Item>
<UnderlineNav.Item icon={<GitPullRequestIcon aria-label="Page three icon" />}>Page three</UnderlineNav.Item>
<UnderlineNav.Item leadingVisual={<IssueOpenedIcon aria-label="Page two icon" />}>Page two</UnderlineNav.Item>
<UnderlineNav.Item leadingVisual={<GitPullRequestIcon aria-label="Page three icon" />}>
Page three
</UnderlineNav.Item>
</UnderlineNav>,
)

Expand All @@ -191,6 +192,20 @@ describe('UnderlineNav', () => {
expect(item).toHaveClass('custom-class')
expect(item.className).toContain('UnderlineItem')
})

it('supports the deprecated `icon` prop', () => {
render(
<UnderlineNav aria-label="Test">
<UnderlineNav.Item icon={<CodeIcon data-testid="jsx-element" />}>as jsx element</UnderlineNav.Item>
<UnderlineNav.Item icon={props => <CodeIcon {...props} data-testid="functional-component" />}>
as functional component
</UnderlineNav.Item>
</UnderlineNav>,
)

expect(screen.getByTestId('jsx-element')).toBeInTheDocument()
expect(screen.getByTestId('functional-component')).toBeInTheDocument()
})
})

describe('Keyboard Navigation', () => {
Expand Down
25 changes: 23 additions & 2 deletions packages/react/src/UnderlineNav/UnderlineNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,34 @@ export type UnderlineNavItemProps = {
* Primary content for an UnderlineNav
*/
children?: React.ReactNode

/**
* Callback that will trigger both on click selection and keyboard selection.
*/
onSelect?: (event: React.MouseEvent<HTMLAnchorElement> | React.KeyboardEvent<HTMLAnchorElement>) => void

/**
* Is `UnderlineNav.Item` current page?
*/
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean

/**
* Icon before the text
* @deprecated Use the `leadingVisual` prop instead
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon?: React.FunctionComponent<IconProps> | React.ReactElement<any>

/**
* Render a visual before the text
*/
leadingVisual?: React.ReactElement

/**
* Renders `UnderlineNav.Item` as given component i.e. react-router's Link
**/
as?: React.ElementType | 'a'

/**
* Counter
*/
Expand All @@ -50,7 +61,17 @@ export type UnderlineNavItemProps = {

export const UnderlineNavItem = forwardRef(
(
{as: Component = 'a', href = '#', children, counter, onSelect, 'aria-current': ariaCurrent, icon: Icon, ...props},
{
as: Component = 'a',
href = '#',
children,
counter,
onSelect,
'aria-current': ariaCurrent,
icon: Icon,
leadingVisual,
...props
},
forwardedRef,
) => {
const backupRef = useRef<HTMLElement>(null)
Expand Down Expand Up @@ -108,7 +129,7 @@ export const UnderlineNavItem = forwardRef(
onKeyDown={keyDownHandler}
onClick={clickHandler}
counter={counter}
icon={Icon}
icon={leadingVisual ?? Icon}
loadingCounters={loadingCounters}
iconsVisible={iconsVisible}
{...props}
Expand Down
Loading