Skip to content

Commit

Permalink
feat(UnderlineNavItem): add support for icons as React.ReactElement (#…
Browse files Browse the repository at this point in the history
…4718)

* feat(UnderlineNavItem): add support for icons as React.ReactElement

* chore: add changeset

* fix: update render logic for elements, update stories

* chore: update typescript for storybook

---------

Co-authored-by: Josh Black <joshblack@users.noreply.github.com>
  • Loading branch information
joshblack and joshblack authored Jul 9, 2024
1 parent 7f55577 commit fd80a60
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-islands-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add support for providing icons as an element to UnderlineNavItem
20 changes: 11 additions & 9 deletions packages/react/src/UnderlineNav/UnderlineNav.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import type {Meta} from '@storybook/react'
import {UnderlineNav} from './index'
import {INITIAL_VIEWPORTS} from '@storybook/addon-viewport'

export default {
const meta = {
title: 'Components/UnderlineNav/Features',
} as Meta
} satisfies Meta<typeof UnderlineNav>

export default meta

export const Default = () => {
return (
Expand All @@ -33,28 +35,28 @@ 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 icon={<CodeIcon />}>Code</UnderlineNav.Item>
<UnderlineNav.Item icon={<EyeIcon />} counter={6}>
Issues
</UnderlineNav.Item>
<UnderlineNav.Item aria-current="page" icon={GitPullRequestIcon}>
<UnderlineNav.Item aria-current="page" icon={<GitPullRequestIcon />}>
Pull Requests
</UnderlineNav.Item>
<UnderlineNav.Item icon={CommentDiscussionIcon} counter={7}>
<UnderlineNav.Item icon={<CommentDiscussionIcon />} counter={7}>
Discussions
</UnderlineNav.Item>
<UnderlineNav.Item icon={ProjectIcon}>Projects</UnderlineNav.Item>
<UnderlineNav.Item icon={<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" icon={<CodeIcon />} counter="11K">
Code
</UnderlineNav.Item>
<UnderlineNav.Item icon={IssueOpenedIcon} counter={12}>
<UnderlineNav.Item icon={<IssueOpenedIcon />} counter={12}>
Issues
</UnderlineNav.Item>
</UnderlineNav>
Expand Down
7 changes: 4 additions & 3 deletions packages/react/src/UnderlineNav/UnderlineNav.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import {UnderlineNavItem} from './UnderlineNavItem'

const excludedControlKeys = ['sx', 'as', 'variant', 'align', 'afterSelect']

export default {
const meta: Meta<typeof UnderlineNav> = {
title: 'Components/UnderlineNav',
component: UnderlineNav,
subcomponents: {UnderlineNavItem},
parameters: {
controls: {
expanded: true,
Expand All @@ -33,7 +32,9 @@ export default {
'aria-label': 'Repository',
loadingCounters: false,
},
} as Meta<typeof UnderlineNav>
}

export default meta

export const Default: StoryFn<typeof UnderlineNav> = () => {
const children = ['Code', 'Pull requests', 'Actions', 'Projects', 'Wiki']
Expand Down
28 changes: 27 additions & 1 deletion packages/react/src/UnderlineNav/UnderlineNav.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import {render} from '@testing-library/react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type {IconProps} from '@primer/octicons-react'
import {
Expand Down Expand Up @@ -77,22 +77,26 @@ describe('UnderlineNav', () => {
default: undefined,
UnderlineNav,
})

it('renders aria-current attribute to be pages when an item is selected', () => {
const {getByRole} = render(<ResponsiveUnderlineNav />)
const selectedNavLink = getByRole('link', {name: 'Code'})
expect(selectedNavLink.getAttribute('aria-current')).toBe('page')
})

it('renders aria-label attribute correctly', () => {
const {container, getByRole} = render(<ResponsiveUnderlineNav />)
expect(container.getElementsByTagName('nav').length).toEqual(1)
const nav = getByRole('navigation')
expect(nav.getAttribute('aria-label')).toBe('Repository')
})

it('renders icons correctly', () => {
const {getByRole} = render(<ResponsiveUnderlineNav />)
const nav = getByRole('navigation')
expect(nav.getElementsByTagName('svg').length).toEqual(7)
})

it('fires onSelect on click', async () => {
const onSelect = jest.fn()
const {getByRole} = render(
Expand All @@ -107,6 +111,7 @@ describe('UnderlineNav', () => {
await user.click(item)
expect(onSelect).toHaveBeenCalledTimes(1)
})

it('fires onSelect on keypress', async () => {
const onSelect = jest.fn()
const {getByRole} = render(
Expand All @@ -128,27 +133,31 @@ describe('UnderlineNav', () => {
await user.keyboard(' ') // space
expect(onSelect).toHaveBeenCalledTimes(3)
})

it('respects counter prop', () => {
const {getByRole} = render(<ResponsiveUnderlineNav />)
const item = getByRole('link', {name: 'Issues (120)'})
const counter = item.getElementsByTagName('span')[3]
expect(counter.textContent).toBe('120')
expect(counter).toHaveAttribute('aria-hidden', 'true')
})

it('renders the content of visually hidden span properly for screen readers', () => {
const {getByRole} = render(<ResponsiveUnderlineNav />)
const item = getByRole('link', {name: 'Issues (120)'})
const counter = item.getElementsByTagName('span')[4]
// non breaking space unified code
expect(counter.textContent).toBe('\u00A0(120)')
})

it('respects loadingCounters prop', () => {
const {getByRole} = render(<ResponsiveUnderlineNav loadingCounters={true} />)
const item = getByRole('link', {name: 'Actions'})
const loadingCounter = item.getElementsByTagName('span')[2]
expect(loadingCounter.className).toContain('LoadingCounter')
expect(loadingCounter.textContent).toBe('')
})

it('renders a visually hidden h2 heading for screen readers when aria-label is present', () => {
const {getByRole} = render(<ResponsiveUnderlineNav />)
const heading = getByRole('heading', {name: 'Repository navigation'})
Expand All @@ -157,6 +166,7 @@ describe('UnderlineNav', () => {
expect(heading.className).toContain('VisuallyHidden')
expect(heading.textContent).toBe('Repository navigation')
})

it('throws an error when there are multiple items that have aria-current', () => {
const spy = jest.spyOn(console, 'error').mockImplementation()
expect(() => {
Expand Down Expand Up @@ -186,6 +196,22 @@ describe('UnderlineNav', () => {
// We are expecting a left value back, that way we know the `getAnchoredPosition` ran.
expect(results).toEqual(expect.objectContaining({left: 0}))
})

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" />}>
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>,
)

expect(screen.getByLabelText('Page one icon')).toBeInTheDocument()
expect(screen.getByLabelText('Page two icon')).toBeInTheDocument()
expect(screen.getByLabelText('Page three icon')).toBeInTheDocument()
})
})

describe('Keyboard Navigation', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/UnderlineNav/UnderlineNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type UnderlineNavItemProps = {
/**
* Icon before the text
*/
icon?: React.FunctionComponent<IconProps>
icon?: React.FunctionComponent<IconProps> | React.ReactElement
/**
* Renders `UnderlineNav.Item` as given component i.e. react-router's Link
**/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Used for UnderlineNav and UnderlinePanels components

import React, {forwardRef, type FC, type PropsWithChildren} from 'react'
import {isElement} from 'react-is'
import type {IconProps} from '@primer/octicons-react'
import styled, {keyframes} from 'styled-components'
import CounterLabel from '../../CounterLabel'
Expand Down Expand Up @@ -193,7 +194,7 @@ export type UnderlineItemProps = {
iconsVisible?: boolean
loadingCounters?: boolean
counter?: number | string
icon?: FC<IconProps>
icon?: FC<IconProps> | React.ReactElement
id?: string
} & SxProp

Expand All @@ -213,11 +214,7 @@ export const UnderlineItem = forwardRef(
) => {
return (
<StyledUnderlineItem ref={forwardedRef} as={as} sx={sxProp} {...rest}>
{iconsVisible && Icon && (
<span data-component="icon">
<Icon />
</span>
)}
{iconsVisible && Icon && <span data-component="icon">{isElement(Icon) ? Icon : <Icon />}</span>}
{children && (
<span data-component="text" data-content={children}>
{children}
Expand Down

0 comments on commit fd80a60

Please sign in to comment.