diff --git a/src/components/Link/Link.tsx b/src/components/Link/Link.tsx index c1983798f4..cb460cec85 100644 --- a/src/components/Link/Link.tsx +++ b/src/components/Link/Link.tsx @@ -28,6 +28,12 @@ export type DefaultLinkProps = StyledLinkProps & // props, plus the required props on WithCustomLinkProps export type CustomLinkProps = StyledLinkProps & WithCustomLinkProps +export function isCustomProps( + props: DefaultLinkProps | CustomLinkProps +): props is CustomLinkProps { + return 'asCustom' in props +} + function linkClasses( variant: StyledLinkProps['variant'], className: StyledLinkProps['className'] @@ -51,7 +57,7 @@ export function Link(props: CustomLinkProps): React.ReactElement export function Link( props: DefaultLinkProps | CustomLinkProps ): React.ReactElement { - if ('asCustom' in props) { + if (isCustomProps(props)) { const { variant, className, asCustom, children, ...remainingProps } = props // 1. We know props is AsCustomProps // 2. We know AsCustomProps is diff --git a/src/components/breadcrumb/Breadcrumb/Breadcrumb.test.tsx b/src/components/breadcrumb/Breadcrumb/Breadcrumb.test.tsx new file mode 100644 index 0000000000..129bfe7f5f --- /dev/null +++ b/src/components/breadcrumb/Breadcrumb/Breadcrumb.test.tsx @@ -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( + {testPageName} + ) + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('listitem')).toHaveClass('usa-breadcrumb__list-item') + }) + + it('renders properly with BreadcrumbLinks', () => { + const { getByRole, queryByText } = render( + + {testPageName} + + ) + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('listitem')).toHaveClass('usa-breadcrumb__list-item') + }) + + it('renders properly with custom elements passesd in', () => { + const { getByRole, queryByText } = render( + + {testPageName} + + ) + 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( + {testPageName} + ) + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('listitem')).toHaveClass( + 'usa-breadcrumb__list-item usa-current' + ) + }) +}) diff --git a/src/components/breadcrumb/Breadcrumb/Breadcrumb.tsx b/src/components/breadcrumb/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000..8cf0add014 --- /dev/null +++ b/src/components/breadcrumb/Breadcrumb/Breadcrumb.tsx @@ -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 ( +
  • + {children} +
  • + ) +} diff --git a/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.stories.tsx b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.stories.tsx new file mode 100644 index 0000000000..671d6dbe74 --- /dev/null +++ b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.stories.tsx @@ -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 => ( + + + + Home + + + + + Federal Contracting + + + + + Contacting assistance programs + + + + Women-owned small business federal contracting program + + +) + +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 ( + + + + Home + + + + + + Federal Contracting + + + + + + Contacting assistance programs + + + + + + Women-owned small business federal contracting program + + + + + ) +} + +export const WrappingBreadcrumb = (): React.ReactElement => ( + + + + Home + + + + + Federal Contracting + + + + + Contacting assistance programs + + + + Women-owned small business federal contracting program + + +) + +export const CustomBreadcrumbLinks = (): React.ReactElement => { + type MockLinkProps = React.PropsWithChildren<{ + to: string + className: string + }> & + JSX.IntrinsicElements['a'] + + const CustomLink: React.FunctionComponent = ({ + to, + className, + children, + ...linkProps + }: MockLinkProps): React.ReactElement => ( + + {children} + + ) + + return ( + + + + className="abc" + asCustom={CustomLink} + to="#"> + Home + + + + + className="abc" + asCustom={CustomLink} + to="#"> + Federal Contracting + + + + + className="abc" + asCustom={CustomLink} + to="#"> + Contacting assistance programs + + + + Women-owned small business federal contracting program + + + ) +} diff --git a/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.test.tsx b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.test.tsx new file mode 100644 index 0000000000..040da962a6 --- /dev/null +++ b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.test.tsx @@ -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( + + {testParentPageName} + {testPageName} + + ) + expect(queryByText(testParentPageName)).toBeInTheDocument() + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('list')).toHaveClass('usa-breadcrumb__list') + }) + + it('renders properly with BreadcrumbLinks', () => { + const { getByRole, queryByText } = render( + + + {testParentPageName} + + {testPageName} + + ) + 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 = ({ + to, + children, + className, + ...linkProps + }: CustomLinkProps): React.ReactElement => ( + + {children} + + ) + + const { getByRole, queryByText } = render( + + + to="#" asCustom={CustomLink}> + {testParentPageName} + + + {testPageName} + + ) + expect(queryByText(testParentPageName)).toBeInTheDocument() + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('list')).toHaveClass('usa-breadcrumb__list') + }) +}) diff --git a/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.tsx b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.tsx new file mode 100644 index 0000000000..6cf7c36664 --- /dev/null +++ b/src/components/breadcrumb/BreadcrumbBar/BreadcrumbBar.tsx @@ -0,0 +1,38 @@ +import React, { ReactElement } from 'react' +import classnames from 'classnames' +import { BreadcrumbProps } from '../Breadcrumb/Breadcrumb' + +interface BreadcrumbBarProps { + children: ReactElement | ReactElement[] + 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 ( + + ) +} diff --git a/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.test.tsx b/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.test.tsx new file mode 100644 index 0000000000..25f45ef1f0 --- /dev/null +++ b/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.test.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { render } from '@testing-library/react' +import { BreadcrumbLink } from './BreadcrumbLink' + +const testPageName = 'Test Page' + +describe('BreadcrumbLink component', () => { + it('renders without errors', () => { + const { getByRole, queryByText } = render( + {testPageName} + ) + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('link')).toHaveClass('usa-breadcrumb__link') + expect(getByRole('link')).not.toHaveClass('usa-link') + }) + + it('renders with a custom component', () => { + type CustomLinkProps = React.PropsWithChildren<{ + to: string + className?: string + }> & + JSX.IntrinsicElements['a'] + + const CustomLink: React.FunctionComponent = ({ + to, + children, + className, + ...linkProps + }: CustomLinkProps): React.ReactElement => ( + + {children} + + ) + + const { getByRole, queryByText } = render( + + to="#" + className="custom-class" + asCustom={CustomLink}> + {testPageName} + + ) + expect(queryByText(testPageName)).toBeInTheDocument() + expect(getByRole('link')).toHaveClass('custom-class usa-breadcrumb__link') + expect(getByRole('link')).not.toHaveClass('usa-link') + }) +}) diff --git a/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.tsx b/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.tsx new file mode 100644 index 0000000000..57a5826939 --- /dev/null +++ b/src/components/breadcrumb/BreadcrumbLink/BreadcrumbLink.tsx @@ -0,0 +1,23 @@ +import classnames from 'classnames' +import React from 'react' +import { + CustomLinkProps, + DefaultLinkProps, + isCustomProps, + Link, +} from '../../Link/Link' + +export function BreadcrumbLink(props: DefaultLinkProps): React.ReactElement +export function BreadcrumbLink(props: CustomLinkProps): React.ReactElement +export function BreadcrumbLink( + props: DefaultLinkProps | CustomLinkProps +): React.ReactElement { + const { className } = props + const classes = classnames(className, 'usa-breadcrumb__link') + + if (isCustomProps(props)) { + return {...props} className={classes} variant="unstyled" /> + } + + return +} diff --git a/src/index.ts b/src/index.ts index bd13af4a9d..8ad7676680 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,11 @@ export { CardMedia } from './components/card/CardMedia/CardMedia' export { CardBody } from './components/card/CardBody/CardBody' export { CardFooter } from './components/card/CardFooter/CardFooter' +/** Breadcrumb components */ +export { BreadcrumbBar } from './components/breadcrumb/BreadcrumbBar/BreadcrumbBar' +export { Breadcrumb } from './components/breadcrumb/Breadcrumb/Breadcrumb' +export { BreadcrumbLink } from './components/breadcrumb/BreadcrumbLink/BreadcrumbLink' + export { Search } from './components/Search/Search' /** Truss-designed components */