Skip to content

Commit

Permalink
feat: New Component: Breadcrumb (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonlenz authored Feb 3, 2021
1 parent 41d7e8e commit 1a804d3
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 1 deletion.
8 changes: 7 additions & 1 deletion src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export type DefaultLinkProps = StyledLinkProps<JSX.IntrinsicElements['a']> &
// props, plus the required props on WithCustomLinkProps
export type CustomLinkProps<T> = StyledLinkProps<T> & WithCustomLinkProps<T>

export function isCustomProps<T>(
props: DefaultLinkProps | CustomLinkProps<T>
): props is CustomLinkProps<T> {
return 'asCustom' in props
}

function linkClasses<T>(
variant: StyledLinkProps<T>['variant'],
className: StyledLinkProps<T>['className']
Expand All @@ -51,7 +57,7 @@ export function Link<T>(props: CustomLinkProps<T>): React.ReactElement
export function Link<FCProps = DefaultLinkProps>(
props: DefaultLinkProps | CustomLinkProps<FCProps>
): React.ReactElement {
if ('asCustom' in props) {
if (isCustomProps(props)) {
const { variant, className, asCustom, children, ...remainingProps } = props
// 1. We know props is AsCustomProps<FCProps>
// 2. We know AsCustomProps<FCProps> is
Expand Down
47 changes: 47 additions & 0 deletions src/components/breadcrumb/Breadcrumb/Breadcrumb.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react'
import { render } from '@testing-library/react'
import { Breadcrumb } from './Breadcrumb'
import { Link } from '../../Link/Link'
import { BreadcrumbLink } from '../BreadcrumbLink/BreadcrumbLink'

const testPageName = 'Test Page'

describe('Breadcrumb component', () => {
it('renders without errors', () => {
const { getByRole, queryByText } = render(
<Breadcrumb>{testPageName}</Breadcrumb>
)
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass('usa-breadcrumb__list-item')
})

it('renders properly with BreadcrumbLinks', () => {
const { getByRole, queryByText } = render(
<Breadcrumb>
<BreadcrumbLink href="#">{testPageName}</BreadcrumbLink>
</Breadcrumb>
)
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass('usa-breadcrumb__list-item')
})

it('renders properly with custom elements passesd in', () => {
const { getByRole, queryByText } = render(
<Breadcrumb>
<Link href="#">{testPageName}</Link>
</Breadcrumb>
)
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass('usa-breadcrumb__list-item')
})

it("renders the appropriate class when defined as 'current'", () => {
const { getByRole, queryByText } = render(
<Breadcrumb current>{testPageName}</Breadcrumb>
)
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('listitem')).toHaveClass(
'usa-breadcrumb__list-item usa-current'
)
})
})
29 changes: 29 additions & 0 deletions src/components/breadcrumb/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import classnames from 'classnames'
export interface BreadcrumbProps {
children: React.ReactNode
className?: string
current?: boolean
}

export const Breadcrumb = (
props: BreadcrumbProps & JSX.IntrinsicElements['li']
): React.ReactElement => {
const { children, current = false, className, ...listItemProps } = props
const classes = classnames(
'usa-breadcrumb__list-item',
{
'usa-current': current,
},
className
)

return (
<li
className={classes}
aria-current={current ? 'page' : undefined}
{...listItemProps}>
{children}
</li>
)
}
159 changes: 159 additions & 0 deletions src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React from 'react'
import { Breadcrumb } from '../Breadcrumb/Breadcrumb'
import { BreadcrumbBar } from './BreadcrumbBar'
import { BreadcrumbLink } from '../BreadcrumbLink/BreadcrumbLink'

export default {
title: 'Components/Breadcrumb',
component: BreadcrumbBar,
parameters: {
info: `
Provide secondary navigation to help users understand where they are in a website.
Source: https://designsystem.digital.gov/components/breadcrumb/
`,
},
}

export const DefaultBreadcrumb = (): React.ReactElement => (
<BreadcrumbBar>
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Home</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Federal Contracting</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Contacting assistance programs</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb current>
<span>Women-owned small business federal contracting program</span>
</Breadcrumb>
</BreadcrumbBar>
)

export const BreadcrumbWithRdfaMetadata = (): React.ReactElement => {
const rdfaMetadata = {
ol: {
vocab: 'http://schema.org/',
typeof: 'BreadcrumbList',
},
li: {
property: 'itemListElement',
typeof: 'ListItem',
},
a: {
property: 'item',
typeof: 'WebPage',
},
}

return (
<BreadcrumbBar listProps={{ ...rdfaMetadata.ol }}>
<Breadcrumb {...rdfaMetadata.li}>
<BreadcrumbLink href="#" {...rdfaMetadata.a}>
<span property="name">Home</span>
</BreadcrumbLink>
<meta property="position" content="1" />
</Breadcrumb>
<Breadcrumb {...rdfaMetadata.li}>
<BreadcrumbLink href="#" {...rdfaMetadata.a}>
<span property="name">Federal Contracting</span>
</BreadcrumbLink>
<meta property="position" content="2" />
</Breadcrumb>
<Breadcrumb {...rdfaMetadata.li}>
<BreadcrumbLink href="#" {...rdfaMetadata.a}>
<span property="name">Contacting assistance programs</span>
</BreadcrumbLink>
<meta property="position" content="3" />
</Breadcrumb>
<Breadcrumb current {...rdfaMetadata.li}>
<span property="name">
Women-owned small business federal contracting program
</span>
<meta property="position" content="4" />
</Breadcrumb>
</BreadcrumbBar>
)
}

export const WrappingBreadcrumb = (): React.ReactElement => (
<BreadcrumbBar variant="wrap">
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Home</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Federal Contracting</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink href="#">
<span>Contacting assistance programs</span>
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb current>
<span>Women-owned small business federal contracting program</span>
</Breadcrumb>
</BreadcrumbBar>
)

export const CustomBreadcrumbLinks = (): React.ReactElement => {
type MockLinkProps = React.PropsWithChildren<{
to: string
className: string
}> &
JSX.IntrinsicElements['a']

const CustomLink: React.FunctionComponent<MockLinkProps> = ({
to,
className,
children,
...linkProps
}: MockLinkProps): React.ReactElement => (
<a href={to} className={className} {...linkProps}>
{children}
</a>
)

return (
<BreadcrumbBar>
<Breadcrumb>
<BreadcrumbLink<MockLinkProps>
className="abc"
asCustom={CustomLink}
to="#">
Home
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink<MockLinkProps>
className="abc"
asCustom={CustomLink}
to="#">
Federal Contracting
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb>
<BreadcrumbLink<MockLinkProps>
className="abc"
asCustom={CustomLink}
to="#">
Contacting assistance programs
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb current>
Women-owned small business federal contracting program
</Breadcrumb>
</BreadcrumbBar>
)
}
69 changes: 69 additions & 0 deletions src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'
import { render } from '@testing-library/react'
import { Breadcrumb } from '../Breadcrumb/Breadcrumb'
import { BreadcrumbBar } from './BreadcrumbBar'
import { BreadcrumbLink } from '../BreadcrumbLink/BreadcrumbLink'

const testPageName = 'Test Page'
const testParentPageName = 'Test Parent Page'

describe('BreadcrumbBar component', () => {
it('renders without errors', () => {
const { getByRole, queryByText } = render(
<BreadcrumbBar>
<Breadcrumb>{testParentPageName}</Breadcrumb>
<Breadcrumb current>{testPageName}</Breadcrumb>
</BreadcrumbBar>
)
expect(queryByText(testParentPageName)).toBeInTheDocument()
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-breadcrumb__list')
})

it('renders properly with BreadcrumbLinks', () => {
const { getByRole, queryByText } = render(
<BreadcrumbBar>
<Breadcrumb>
<BreadcrumbLink href="#">{testParentPageName}</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb current>{testPageName}</Breadcrumb>
</BreadcrumbBar>
)
expect(queryByText(testParentPageName)).toBeInTheDocument()
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-breadcrumb__list')
})

it('renders properly with Breadcrumbs using custom elements', () => {
type CustomLinkProps = React.PropsWithChildren<{
to: string
className?: string
}> &
JSX.IntrinsicElements['a']

const CustomLink: React.FunctionComponent<CustomLinkProps> = ({
to,
children,
className,
...linkProps
}: CustomLinkProps): React.ReactElement => (
<a href={to} className={className} {...linkProps}>
{children}
</a>
)

const { getByRole, queryByText } = render(
<BreadcrumbBar>
<Breadcrumb>
<BreadcrumbLink<CustomLinkProps> to="#" asCustom={CustomLink}>
{testParentPageName}
</BreadcrumbLink>
</Breadcrumb>
<Breadcrumb current>{testPageName}</Breadcrumb>
</BreadcrumbBar>
)
expect(queryByText(testParentPageName)).toBeInTheDocument()
expect(queryByText(testPageName)).toBeInTheDocument()
expect(getByRole('list')).toHaveClass('usa-breadcrumb__list')
})
})
38 changes: 38 additions & 0 deletions src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { ReactElement } from 'react'
import classnames from 'classnames'
import { BreadcrumbProps } from '../Breadcrumb/Breadcrumb'

interface BreadcrumbBarProps {
children: ReactElement<BreadcrumbProps> | ReactElement<BreadcrumbProps>[]
variant?: 'default' | 'wrap'
className?: string
navProps?: JSX.IntrinsicElements['nav']
listProps?: JSX.IntrinsicElements['ol']
}

export const BreadcrumbBar = (
props: BreadcrumbBarProps
): React.ReactElement => {
const {
variant = 'default',
children,
className,
navProps,
listProps,
} = props
const classes = classnames(
'usa-breadcrumb',
{
'usa-breadcrumb--wrap': variant === 'wrap',
},
className
)

return (
<nav className={classes} {...navProps} aria-label="Breadcrumbs">
<ol className="usa-breadcrumb__list" {...listProps}>
{children}
</ol>
</nav>
)
}
Loading

0 comments on commit 1a804d3

Please sign in to comment.