diff --git a/CHANGELOG.md b/CHANGELOG.md index 1125c8b6f4..c0c52f4f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ All notable changes to this project will be documented in this file. This file is generated automatically based on commit messages. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## UNRELEASED +## [1.2.0] - 2020-04-30 +- feat: Added components for the header and extended header (Header, NavList, MegaMenu, Menu, NavMenuButton, NavCloseButton, NavDropDownButton, PrimaryNav, Title, ExtendedNav) - chore: Added yarn audit check to dangerfile (on package change) - feat: Added search component - feat: Added Grid, GridContainer components diff --git a/src/components/forms/Checkbox/Checkbox.stories.tsx b/src/components/forms/Checkbox/Checkbox.stories.tsx index 4d64c8b235..37a3f1326a 100644 --- a/src/components/forms/Checkbox/Checkbox.stories.tsx +++ b/src/components/forms/Checkbox/Checkbox.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Checkbox } from './Checkbox' export default { - title: 'Checkbox', + title: 'Forms/Checkbox', parameters: { info: ` USWDS 2.0 Checkbox component diff --git a/src/components/forms/Dropdown/Dropdown.stories.tsx b/src/components/forms/Dropdown/Dropdown.stories.tsx index f4c90d0109..ca57133e75 100644 --- a/src/components/forms/Dropdown/Dropdown.stories.tsx +++ b/src/components/forms/Dropdown/Dropdown.stories.tsx @@ -4,7 +4,7 @@ import { Dropdown } from './Dropdown' import { Label } from '../Label/Label' export default { - title: 'Dropdown', + title: 'Forms/Dropdown', parameters: { info: ` USWDS 2.0 Dropdown component diff --git a/src/components/forms/ErrorMessage/ErrorMessage.stories.tsx b/src/components/forms/ErrorMessage/ErrorMessage.stories.tsx index cdcfb0a13f..4e6c6d8563 100644 --- a/src/components/forms/ErrorMessage/ErrorMessage.stories.tsx +++ b/src/components/forms/ErrorMessage/ErrorMessage.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { ErrorMessage } from './ErrorMessage' export default { - title: 'ErrorMessage', + title: 'Forms/ErrorMessage', parameters: { info: ` USWDS 2.0 ErrorMessage component diff --git a/src/components/forms/Fieldset/Fieldset.stories.tsx b/src/components/forms/Fieldset/Fieldset.stories.tsx index 8d0a57bce5..fe0426330d 100644 --- a/src/components/forms/Fieldset/Fieldset.stories.tsx +++ b/src/components/forms/Fieldset/Fieldset.stories.tsx @@ -7,7 +7,7 @@ import { Checkbox } from '../Checkbox/Checkbox' import { Radio } from '../Radio/Radio' export default { - title: 'Fieldset', + title: 'Forms/Fieldset', parameters: { info: ` USWDS 2.0 Fieldset component diff --git a/src/components/forms/Form/Form.stories.tsx b/src/components/forms/Form/Form.stories.tsx index f831f6b028..4faf18a9f4 100644 --- a/src/components/forms/Form/Form.stories.tsx +++ b/src/components/forms/Form/Form.stories.tsx @@ -13,7 +13,7 @@ import { TextInput } from '../TextInput/TextInput' import { Textarea } from '../Textarea/Textarea' export default { - title: 'Form', + title: 'Forms/Form', parameters: { info: ` USWDS 2.0 Form component diff --git a/src/components/forms/FormGroup/FormGroup.stories.tsx b/src/components/forms/FormGroup/FormGroup.stories.tsx index 039511c0b5..764c2495c1 100644 --- a/src/components/forms/FormGroup/FormGroup.stories.tsx +++ b/src/components/forms/FormGroup/FormGroup.stories.tsx @@ -6,7 +6,7 @@ import { TextInput } from '../TextInput/TextInput' import { ErrorMessage } from '../ErrorMessage/ErrorMessage' export default { - title: 'FormGroup', + title: 'Forms/FormGroup', parameters: { info: ` USWDS 2.0 FormGroup component diff --git a/src/components/forms/Label/Label.stories.tsx b/src/components/forms/Label/Label.stories.tsx index 868a2e3503..ed9d7f3925 100644 --- a/src/components/forms/Label/Label.stories.tsx +++ b/src/components/forms/Label/Label.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Label } from './Label' export default { - title: 'Label', + title: 'Forms/Label', parameters: { info: ` USWDS 2.0 Label component diff --git a/src/components/forms/Radio/Radio.stories.tsx b/src/components/forms/Radio/Radio.stories.tsx index 0b38b5388c..c80428478e 100644 --- a/src/components/forms/Radio/Radio.stories.tsx +++ b/src/components/forms/Radio/Radio.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Radio } from './Radio' export default { - title: 'Radio', + title: 'Forms/Radio', parameters: { info: ` USWDS 2.0 Radio component diff --git a/src/components/forms/TextInput/TextInput.stories.tsx b/src/components/forms/TextInput/TextInput.stories.tsx index f90b5ec763..08d0241f81 100644 --- a/src/components/forms/TextInput/TextInput.stories.tsx +++ b/src/components/forms/TextInput/TextInput.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { TextInput } from './TextInput' export default { - title: 'TextInput', + title: 'Forms/TextInput', parameters: { info: ` USWDS 2.0 TextInput component diff --git a/src/components/forms/Textarea/Textarea.stories.tsx b/src/components/forms/Textarea/Textarea.stories.tsx index f04c0f80f6..bd08374e85 100644 --- a/src/components/forms/Textarea/Textarea.stories.tsx +++ b/src/components/forms/Textarea/Textarea.stories.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Textarea } from './Textarea' export default { - title: 'Textarea', + title: 'Forms/Textarea', parameters: { info: ` USWDS 2.0 Textarea component diff --git a/src/components/header/ExtendedNav/ExtendedNav.stories.tsx b/src/components/header/ExtendedNav/ExtendedNav.stories.tsx new file mode 100644 index 0000000000..9bbe94360c --- /dev/null +++ b/src/components/header/ExtendedNav/ExtendedNav.stories.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { ExtendedNav } from './ExtendedNav' + +export default { + title: 'Header/ExtendedNav', + parameters: { + info: ` +USWDS 2.0 ExtendedNav component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const mockToggle = (): void => { + /* mock submit fn */ +} + +const testPrimaryItems = [ + + Simple link + , + + Simple link Two + , +] + +const testSecondaryItems = [ + + Simple link + , + + Simple link Two + , +] + +export const defaultExtendedNav = (): React.ReactElement => ( + +) diff --git a/src/components/header/ExtendedNav/ExtendedNav.test.tsx b/src/components/header/ExtendedNav/ExtendedNav.test.tsx new file mode 100644 index 0000000000..f17cec55c1 --- /dev/null +++ b/src/components/header/ExtendedNav/ExtendedNav.test.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' + +import { ExtendedNav } from './ExtendedNav' + +const testPrimaryItems = [ + + Simple link one + , + + Simple link two + , +] + +const testSecondaryItems = [ + + Simple link three + , + + Simple link four + , +] + +const onToggleMobileNav = (): void => { + /* mock submit fn */ +} + +describe('ExtendedNav component', () => { + it('renders without errors', () => { + const { queryByRole } = render( + + ) + expect(queryByRole('navigation')).toBeInTheDocument() + }) + + it('renders primary items', () => { + const { getByText } = render( + + ) + expect(getByText('Simple link one')).toBeInTheDocument() + expect(getByText('Simple link two')).toBeInTheDocument() + }) + + it('renders secondary items', () => { + const { getByText } = render( + + ) + expect(getByText('Simple link three')).toBeInTheDocument() + expect(getByText('Simple link four')).toBeInTheDocument() + }) + + it('renders nav button', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('navCloseButton')).toBeInTheDocument() + }) + + it('implements an onClick handler', () => { + const onToggleMobileNav = jest.fn() + const { getByTestId } = render( + + ) + + fireEvent.click(getByTestId('navCloseButton')) + expect(onToggleMobileNav).toHaveBeenCalledTimes(1) + }) + + it('renders the is-visible class when mobileExpanded is true', () => { + const { container } = render( + + ) + expect(container.querySelector('.is-visible')).toBeInTheDocument() + }) + + it('does not render the is-visible class when mobileExpanded is false', () => { + const { container } = render( + + ) + expect(container.querySelector('.is-visible')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/header/ExtendedNav/ExtendedNav.tsx b/src/components/header/ExtendedNav/ExtendedNav.tsx new file mode 100644 index 0000000000..40da1b10d4 --- /dev/null +++ b/src/components/header/ExtendedNav/ExtendedNav.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import classnames from 'classnames' + +import { NavCloseButton } from '../NavCloseButton/NavCloseButton' +import { NavList } from '../NavList/NavList' + +type ExtendedNavProps = { + primaryItems: React.ReactNode[] + secondaryItems: React.ReactNode[] + onToggleMobileNav?: ( + event: React.MouseEvent + ) => void + mobileExpanded?: boolean +} + +export const ExtendedNav = ( + props: ExtendedNavProps & React.HTMLAttributes +): React.ReactElement => { + const { + primaryItems, + secondaryItems, + mobileExpanded = false, + children, + className, + onToggleMobileNav, + ...navProps + } = props + + const classes = classnames( + 'usa-nav', + { + 'is-visible': mobileExpanded, + }, + className + ) + + return ( + + + + + + + {children} + + + + ) +} + +export default ExtendedNav diff --git a/src/components/header/Header/Header.stories.tsx b/src/components/header/Header/Header.stories.tsx new file mode 100644 index 0000000000..f59384a50b --- /dev/null +++ b/src/components/header/Header/Header.stories.tsx @@ -0,0 +1,340 @@ +import React, { useState } from 'react' +import { Header } from './Header' + +import { Title } from '../Title/Title' +import { PrimaryNav } from '../PrimaryNav/PrimaryNav' +import { Search } from '../../Search/Search' +import { Menu } from '../Menu/Menu' +import { MegaMenu } from '../MegaMenu/MegaMenu' +import { NavMenuButton } from '../NavMenuButton/NavMenuButton' +import { NavDropDownButton } from '../NavDropDownButton/NavDropDownButton' +import { ExtendedNav } from '../ExtendedNav/ExtendedNav' + +export default { + title: 'Header/Header', + parameters: { + info: ` +USWDS 2.0 Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const mockSubmit = (): void => { + /* mock submit fn */ +} + +export const BasicHeader = (): React.ReactElement => { + const [expanded, setExpanded] = useState(false) + const onClick = (): void => setExpanded((prvExpanded) => !prvExpanded) + + const testMenuItems = [ + + Current link + , + + Simple link Two + , + ] + + const [isOpen, setIsOpen] = useState([false, false]) + + const onToggle = (index: number): void => { + setIsOpen((prevIsOpen) => { + const newIsOpen = Array(prevIsOpen.length).fill(false) + newIsOpen[index] = !prevIsOpen[index] + return newIsOpen + }) + } + + const testItemsMenu = [ + <> + { + onToggle(0) + }} + isOpen={isOpen[0]} + label="Nav Label" + isCurrent={true} + /> + + >, + + Parent link + , + + Parent link + , + ] + + return ( + <> + + + + + Project Title + + + + + + + + > + ) +} + +export const BasicHeaderWithMegaMenu = (): React.ReactElement => { + const [expanded, setExpanded] = useState(false) + const onClick = (): void => setExpanded((prvExpanded) => !prvExpanded) + + const testItemsMegaOne = [ + [ + + Simple link one + , + + Simple link two + , + ], + [ + + Simple link three + , + + Simple link four + , + ], + ] + + const testItemsMegaTwo = [ + [ + + Simple link five + , + + Simple link six + , + ], + [ + + Simple link seven + , + + Simple link eight + , + ], + ] + + const [isOpen, setIsOpen] = useState([false, false]) + + const onToggle = (index: number): void => { + setIsOpen((prevIsOpen) => { + const newIsOpen = [false, false] + newIsOpen[index] = !prevIsOpen[index] + return newIsOpen + }) + } + + const testItemsMegaMenu = [ + <> + { + onToggle(0) + }} + menuId="testDropDownOne" + isOpen={isOpen[0]} + label="Nav Label" + isCurrent={true} + /> + + >, + <> + { + onToggle(1) + }} + menuId="testDropDownOne" + isOpen={isOpen[1]} + label="Nav Label" + /> + + >, + + Parent link + , + + Parent link + , + ] + + return ( + <> + + + + + Project Title + + + + + + + + > + ) +} + +export const extendedHeader = (): React.ReactElement => { + const [expanded, setExpanded] = useState(false) + const onClick = (): void => setExpanded((prvExpanded) => !prvExpanded) + const [isOpen, setIsOpen] = useState([false]) + + const onToggle = (index: number): void => { + setIsOpen((prevIsOpen) => { + const newIsOpen = Array(prevIsOpen.length).fill(false) + newIsOpen[index] = !prevIsOpen[index] + return newIsOpen + }) + } + + const testMenuItems = [ + + Simple link one + , + + Simple link two + , + ] + + const testItemsMenu = [ + <> + { + onToggle(0) + }} + menuId="testDropDownOne" + isOpen={isOpen[0]} + label="Nav Label" + isCurrent={true} + /> + + >, + + Parent link + , + + Parent link + , + ] + return ( + <> + + + + Project Title + + + + + + + > + ) +} + +export const extendedHeaderWithMegaMenu = (): React.ReactElement => { + const [expanded, setExpanded] = useState(false) + const onClick = (): void => setExpanded((prvExpanded) => !prvExpanded) + + const testMenuItems = [ + + Simple link one + , + + Simple link two + , + ] + + const testItemsMegaOne = [ + [ + + Simple link one + , + + Simple link two + , + ], + [ + + Simple link three + , + + Simple link four + , + ], + ] + + const [isOpen, setIsOpen] = useState([false, false]) + + const onToggle = (index: number): void => { + setIsOpen((prevIsOpen) => { + const newIsOpen = Array(prevIsOpen.length).fill(false) + newIsOpen[index] = !prevIsOpen[index] + return newIsOpen + }) + } + + const testItemsMenu = [ + <> + { + onToggle(0) + }} + menuId="testDropDownOne" + isOpen={isOpen[0]} + label="Nav Label" + isCurrent={true} + /> + + >, + + Parent link + , + + Parent link + , + ] + return ( + <> + + + + Project Title + + + + + + + > + ) +} diff --git a/src/components/header/Header/Header.test.tsx b/src/components/header/Header/Header.test.tsx new file mode 100644 index 0000000000..20b15b0d91 --- /dev/null +++ b/src/components/header/Header/Header.test.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { render } from '@testing-library/react' + +import { Header } from './Header' + +describe('Header component', () => { + it('renders without errors', () => { + const { queryByTestId } = render(My Header) + expect(queryByTestId('header')).toBeInTheDocument() + }) + + it('renders children', () => { + const { getByText } = render(My Header) + expect(getByText('My Header')).toBeInTheDocument() + }) +}) diff --git a/src/components/header/Header/Header.tsx b/src/components/header/Header/Header.tsx new file mode 100644 index 0000000000..4c81bb90ca --- /dev/null +++ b/src/components/header/Header/Header.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import classnames from 'classnames' + +interface HeaderProps { + basic?: boolean + extended?: boolean + basicWithMegaMenu?: boolean + children: React.ReactNode +} + +export const Header = ( + props: HeaderProps & React.HtmlHTMLAttributes +): React.ReactElement => { + const { + basic, + extended, + basicWithMegaMenu, + children, + className, + ...headerProps + } = props + + const classes = classnames( + 'usa-header', + { + 'usa-header--basic': basic, + 'usa-header--megamenu': basicWithMegaMenu, + 'usa-header--extended': extended, + }, + className + ) + + return ( + + {children} + + ) +} diff --git a/src/components/header/MegaMenu/MegaMenu.stories.tsx b/src/components/header/MegaMenu/MegaMenu.stories.tsx new file mode 100644 index 0000000000..ac39a5f75c --- /dev/null +++ b/src/components/header/MegaMenu/MegaMenu.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { MegaMenu } from './MegaMenu' + +export default { + title: 'Header/MegaMenu', + parameters: { + info: ` +USWDS 2.0 MegaMenu component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const testItems = [ + [ + + Simple link + , + + Simple link + , + ], + [ + + Simple link Two + , + + Simple link Two + , + ], +] + +export const defaultMegaMenu = (): React.ReactElement => ( + +) diff --git a/src/components/header/MegaMenu/MegaMenu.test.tsx b/src/components/header/MegaMenu/MegaMenu.test.tsx new file mode 100644 index 0000000000..b38a458c2b --- /dev/null +++ b/src/components/header/MegaMenu/MegaMenu.test.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { render } from '@testing-library/react' + +import { MegaMenu } from './MegaMenu' + +const testItems = [ + [ + + Simple link one + , + + Simple link two + , + ], + [ + + Simple link three + , + + Simple link four + , + ], +] + +describe('MegaMenu component', () => { + it('renders without errors', () => { + const { container } = render() + + expect(container.querySelector('.usa-megamenu')).toBeInTheDocument() + }) + + it('renders all test items when open', () => { + const { getByText } = render() + expect(getByText('Simple link one')).toBeInTheDocument() + expect(getByText('Simple link two')).toBeInTheDocument() + expect(getByText('Simple link three')).toBeInTheDocument() + expect(getByText('Simple link four')).toBeInTheDocument() + }) + + it('hides the submenu when the isOpen is false', () => { + const { queryByTestId } = render( + + ) + + expect(queryByTestId('megamenu')).toHaveAttribute('hidden') + }) +}) diff --git a/src/components/header/MegaMenu/MegaMenu.tsx b/src/components/header/MegaMenu/MegaMenu.tsx new file mode 100644 index 0000000000..8a70c16338 --- /dev/null +++ b/src/components/header/MegaMenu/MegaMenu.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +import { NavList } from '../NavList/NavList' + +type MegaMenuProps = { + items: React.ReactNode[][] + isOpen: boolean +} + +export const MegaMenu = (props: MegaMenuProps): React.ReactElement => { + const { items, isOpen } = props + return ( + + + {items.map((listItems, i) => ( + + + + ))} + + + ) +} + +export default MegaMenu diff --git a/src/components/header/Menu/Menu.stories.tsx b/src/components/header/Menu/Menu.stories.tsx new file mode 100644 index 0000000000..173a85f3ec --- /dev/null +++ b/src/components/header/Menu/Menu.stories.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { Menu } from './Menu' + +export default { + title: 'Header/Menu', + parameters: { + info: ` +USWDS 2.0 Menu component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const testItems = [ + + Simple link + , + + Simple link Two + , +] + +export const defaultDropDownMenu = (): React.ReactElement => ( + +) diff --git a/src/components/header/Menu/Menu.test.tsx b/src/components/header/Menu/Menu.test.tsx new file mode 100644 index 0000000000..40304f720a --- /dev/null +++ b/src/components/header/Menu/Menu.test.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { render } from '@testing-library/react' + +import { Menu } from './Menu' + +const testItems = [ + + Simple link one + , + + Simple link two + , +] + +describe('Menu component', () => { + it('renders without errors', () => { + const { container } = render() + expect(container.querySelector('.usa-nav__submenu')).toBeInTheDocument() + }) + + it('renders all test items when open', () => { + const { getByText } = render() + expect(getByText('Simple link one')).toBeInTheDocument() + expect(getByText('Simple link two')).toBeInTheDocument() + }) +}) diff --git a/src/components/header/Menu/Menu.tsx b/src/components/header/Menu/Menu.tsx new file mode 100644 index 0000000000..86a667bed0 --- /dev/null +++ b/src/components/header/Menu/Menu.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { NavList } from '../NavList/NavList' + +type MenuProps = { + items: React.ReactNode[] + isOpen: boolean +} + +export const Menu = (props: MenuProps): React.ReactElement => { + const { items, isOpen } = props + return +} + +export default Menu diff --git a/src/components/header/NavCloseButton/NavCloseButton.stories.tsx b/src/components/header/NavCloseButton/NavCloseButton.stories.tsx new file mode 100644 index 0000000000..fc68a146f5 --- /dev/null +++ b/src/components/header/NavCloseButton/NavCloseButton.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { NavCloseButton } from './NavCloseButton' + +export default { + title: 'Header/NavCloseButton', + parameters: { + info: ` +USWDS 2.0 NavCloseButton Component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +export const defaultMenuNavCloseButton = (): React.ReactElement => ( + + + +) diff --git a/src/components/header/NavCloseButton/NavCloseButton.test.tsx b/src/components/header/NavCloseButton/NavCloseButton.test.tsx new file mode 100644 index 0000000000..7cbf44cad9 --- /dev/null +++ b/src/components/header/NavCloseButton/NavCloseButton.test.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' + +import { NavCloseButton } from './NavCloseButton' + +describe('NavCloseButton component', () => { + it('renders without errors', () => { + const { getByTestId } = render() + expect(getByTestId('navCloseButton')).toBeInTheDocument() + }) + + it('implements an onClick handler', () => { + const onClickFn = jest.fn() + const { getByTestId } = render() + + fireEvent.click(getByTestId('navCloseButton')) + + expect(onClickFn).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/components/header/NavCloseButton/NavCloseButton.tsx b/src/components/header/NavCloseButton/NavCloseButton.tsx new file mode 100644 index 0000000000..a215e558ca --- /dev/null +++ b/src/components/header/NavCloseButton/NavCloseButton.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +// assets +import closeImg from 'uswds/src/img/close.svg' + +export const NavCloseButton = ( + props: React.HTMLAttributes +): React.ReactElement => { + const { onClick, ...buttonProps } = props + + return ( + + + + ) +} diff --git a/src/components/header/NavDropDownButton/NavDropDownButton.stories.tsx b/src/components/header/NavDropDownButton/NavDropDownButton.stories.tsx new file mode 100644 index 0000000000..1bcb140e5f --- /dev/null +++ b/src/components/header/NavDropDownButton/NavDropDownButton.stories.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react' +import { NavDropDownButton } from './NavDropDownButton' +import { Header } from '../Header/Header' +import { PrimaryNav } from '../PrimaryNav/PrimaryNav' + +export default { + title: 'Header/NavDropDownButton', + parameters: { + info: ` +USWDS 2.0 NavDropDownButton component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const testLabel = 'Nav Label' + +const mockToggle = (): void => { + /* mock submit fn */ +} + +export const defaultDropDownMenu = (): React.ReactElement => { + const [isOpen, setIsOpen] = useState([false, false]) + + const onToggle = (index: number): void => { + setIsOpen((prevIsOpen) => { + const newIsOpen = [false, false] + newIsOpen[index] = !prevIsOpen[index] + return newIsOpen + }) + } + + return ( + + + + { + onToggle(0) + }} + />, + { + onToggle(1) + }} + isCurrent={true} + />, + ]} + onToggleMobileNav={mockToggle} + /> + + + ) +} diff --git a/src/components/header/NavDropDownButton/NavDropDownButton.test.tsx b/src/components/header/NavDropDownButton/NavDropDownButton.test.tsx new file mode 100644 index 0000000000..9ddd2ab29c --- /dev/null +++ b/src/components/header/NavDropDownButton/NavDropDownButton.test.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' + +import { NavDropDownButton } from './NavDropDownButton' + +const testLabel = 'Nav Label' + +describe('NavDropDownButton component', () => { + it('renders without errors', () => { + const onToggle = (): void => { + /* mock toggle fn */ + } + const { getByTestId } = render( + + ) + expect(getByTestId('navDropDownButton')).toBeInTheDocument() + }) + + it('implements an onClick handler', () => { + const onClickFn = jest.fn() + const { getByText } = render( + + ) + + fireEvent.click(getByText(testLabel)) + expect(onClickFn).toHaveBeenCalledTimes(1) + }) + + it('renders when isOpen is set to false', () => { + const onClickFn = jest.fn() + const { getByTestId } = render( + + ) + expect(getByTestId('navDropDownButton')).toHaveAttribute( + 'aria-expanded', + expect.stringContaining('false') + ) + }) + + it('renders the usa-current class when isCurrent is true', () => { + const onClickFn = jest.fn() + const { getByTestId } = render( + + ) + expect(getByTestId('navDropDownButton')).toHaveClass('usa-current') + }) +}) diff --git a/src/components/header/NavDropDownButton/NavDropDownButton.tsx b/src/components/header/NavDropDownButton/NavDropDownButton.tsx new file mode 100644 index 0000000000..6b7302c26b --- /dev/null +++ b/src/components/header/NavDropDownButton/NavDropDownButton.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import classnames from 'classnames' + +type NavDropDownButtonProps = { + label: string + /* + Element (i.e. menu) id already present in DOM that will be controlled by this button + */ + menuId: string + isOpen: boolean + onToggle: () => void + isCurrent?: boolean +} + +export const NavDropDownButton = ( + props: NavDropDownButtonProps & React.HTMLAttributes +): React.ReactElement => { + const { + label, + menuId, + isOpen, + onToggle, + isCurrent, + className, + ...buttonProps + } = props + + const classes = classnames( + 'usa-accordion__button', + 'usa-nav__link', + { + 'usa-current': isCurrent, + }, + className + ) + + return ( + onToggle()} + {...buttonProps} + type="button"> + {label} + + ) +} + +export default NavDropDownButton diff --git a/src/components/header/NavList/NavList.stories.tsx b/src/components/header/NavList/NavList.stories.tsx new file mode 100644 index 0000000000..7c3911edb9 --- /dev/null +++ b/src/components/header/NavList/NavList.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { NavList } from './NavList' + +export default { + title: 'Header/NavList', + parameters: { + info: ` +USWDS 2.0 NavList component used within the Header component + +`, + }, +} + +const testItems = [ + + Test link + , + + Test Link Two + , +] + +export const BasicNavList = (): React.ReactElement => ( + +) diff --git a/src/components/header/NavList/NavList.test.tsx b/src/components/header/NavList/NavList.test.tsx new file mode 100644 index 0000000000..2af904d80a --- /dev/null +++ b/src/components/header/NavList/NavList.test.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import { render } from '@testing-library/react' + +import { NavList } from './NavList' + +const testItems = ['item 1', 'item 2', 'item 3'] + +describe('NavList component', () => { + it('renders without errors', () => { + const { container } = render() + expect(container.querySelector('.usa-nav__primary')).toBeInTheDocument() + expect( + container.querySelector('.usa-nav__primary-item') + ).toBeInTheDocument() + }) + + it('renders all test items', () => { + const { getByText } = render() + expect(getByText('item 1')).toBeInTheDocument() + expect(getByText('item 2')).toBeInTheDocument() + expect(getByText('item 3')).toBeInTheDocument() + }) +}) diff --git a/src/components/header/NavList/NavList.tsx b/src/components/header/NavList/NavList.tsx new file mode 100644 index 0000000000..b60ebf2778 --- /dev/null +++ b/src/components/header/NavList/NavList.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import classnames from 'classnames' + +interface NavListProps { + items: React.ReactNode[] + primary?: boolean + secondary?: boolean + subnav?: boolean + megamenu?: boolean +} + +export const NavList = ( + props: NavListProps & React.HTMLAttributes +): React.ReactElement => { + const { + items, + primary, + secondary, + subnav, + megamenu, + className, + ...ulProps + } = props + + const ulClasses = classnames( + { + 'usa-nav__primary usa-accordion': primary, + 'usa-nav__secondary-links': secondary, + 'usa-nav__submenu': subnav, + 'usa-nav__submenu-list': megamenu, + }, + className + ) + + const liClasses = classnames({ + 'usa-nav__primary-item': primary, + 'usa-nav__secondary-item': secondary, + 'usa-nav__submenu-item': subnav || megamenu, + }) + + return ( + + {items.map((item, i) => ( + + {item} + + ))} + + ) +} diff --git a/src/components/header/NavMenuButton/NavMenuButton.stories.tsx b/src/components/header/NavMenuButton/NavMenuButton.stories.tsx new file mode 100644 index 0000000000..0fa380de4d --- /dev/null +++ b/src/components/header/NavMenuButton/NavMenuButton.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { NavMenuButton } from './NavMenuButton' + +export default { + title: 'Header/NavMenuButton', + parameters: { + info: ` +USWDS 2.0 NavMenuButton used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +export const defaultMenuNavMenuButton = (): React.ReactElement => ( + +) diff --git a/src/components/header/NavMenuButton/NavMenuButton.test.tsx b/src/components/header/NavMenuButton/NavMenuButton.test.tsx new file mode 100644 index 0000000000..8f285dac17 --- /dev/null +++ b/src/components/header/NavMenuButton/NavMenuButton.test.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' + +import { NavMenuButton } from './NavMenuButton' + +const label = 'Menu' + +describe('NavMenuButton component', () => { + it('renders without errors', () => { + const { getByTestId } = render() + expect(getByTestId('navMenuButton')).toBeInTheDocument() + }) + + it('implements an onClick handler', () => { + const onClickFn = jest.fn() + const { getByText } = render( + + ) + + fireEvent.click(getByText('Menu')) + + expect(onClickFn).toHaveBeenCalledTimes(1) + }) + + it('renders the correct class names', () => { + const { getByTestId } = render() + expect(getByTestId('navMenuButton')).toHaveClass('usa-menu-btn') + }) +}) diff --git a/src/components/header/NavMenuButton/NavMenuButton.tsx b/src/components/header/NavMenuButton/NavMenuButton.tsx new file mode 100644 index 0000000000..e906b9bbc5 --- /dev/null +++ b/src/components/header/NavMenuButton/NavMenuButton.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +interface NavMenuButtonProps { + label: React.ReactNode +} + +export const NavMenuButton = ( + props: NavMenuButtonProps & React.HTMLAttributes +): React.ReactElement => { + const { label, onClick, ...buttonProps } = props + + return ( + + {label} + + ) +} diff --git a/src/components/header/PrimaryNav/PrimaryNav.stories.tsx b/src/components/header/PrimaryNav/PrimaryNav.stories.tsx new file mode 100644 index 0000000000..d8bd09cb85 --- /dev/null +++ b/src/components/header/PrimaryNav/PrimaryNav.stories.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { PrimaryNav } from './PrimaryNav' + +export default { + title: 'Header/PrimaryNav', + parameters: { + info: ` +USWDS 2.0 PrimaryNav component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} +const mockToggle = (): void => { + /* mock submit fn */ +} + +const testBasicHeaderItems = [ + + Current link + , + + Simple link Two + , +] + +export const defaultPrimaryNav = (): React.ReactElement => ( + +) diff --git a/src/components/header/PrimaryNav/PrimaryNav.test.tsx b/src/components/header/PrimaryNav/PrimaryNav.test.tsx new file mode 100644 index 0000000000..3e393b006f --- /dev/null +++ b/src/components/header/PrimaryNav/PrimaryNav.test.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { render, fireEvent } from '@testing-library/react' + +import { PrimaryNav } from './PrimaryNav' + +const testItems = [ + + Simple link one + , + + Simple link two + , +] + +const onToggleMobileNav = (): void => { + /* mock submit fn */ +} + +describe('PrimaryNav component', () => { + it('renders without errors', () => { + const { queryByRole } = render( + + ) + expect(queryByRole('navigation')).toBeInTheDocument() + }) + + it('renders items', () => { + const { getByText } = render( + + ) + + expect(getByText('Simple link one')).toBeInTheDocument() + expect(getByText('Simple link two')).toBeInTheDocument() + }) + + it('renders nav close button', () => { + const { getByTestId } = render( + + ) + expect(getByTestId('navCloseButton')).toBeInTheDocument() + }) + + it('implements an onClick handler for nav close button', () => { + const onToggleMobileNav = jest.fn() + const { getByTestId } = render( + + ) + + fireEvent.click(getByTestId('navCloseButton')) + expect(onToggleMobileNav).toHaveBeenCalledTimes(1) + }) + + it('renders the is-visible class when mobileExpanded is true', () => { + const { container } = render( + + ) + expect(container.querySelector('.is-visible')).toBeInTheDocument() + }) + + it('does not render the is-visible class when mobileExpanded is false', () => { + const { container } = render( + + ) + expect(container.querySelector('.is-visible')).toEqual(null) + }) + + it('renders childern', () => { + const { getByText } = render( + + Test Text + + ) + + expect(getByText('Test Text')).toBeInTheDocument() + }) +}) diff --git a/src/components/header/PrimaryNav/PrimaryNav.tsx b/src/components/header/PrimaryNav/PrimaryNav.tsx new file mode 100644 index 0000000000..ef0b392723 --- /dev/null +++ b/src/components/header/PrimaryNav/PrimaryNav.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import classnames from 'classnames' + +import { NavCloseButton } from '../NavCloseButton/NavCloseButton' +import { NavList } from '../NavList/NavList' + +type PrimaryNavProps = { + items: React.ReactNode[] + onToggleMobileNav?: ( + event: React.MouseEvent + ) => void + mobileExpanded?: boolean +} + +export const PrimaryNav = ( + props: PrimaryNavProps & React.HTMLAttributes +): React.ReactElement => { + const { + items, + onToggleMobileNav, + mobileExpanded, + children, + className, + ...navProps + } = props + + const classes = classnames( + 'usa-nav', + { + 'is-visible': mobileExpanded, + }, + className + ) + + return ( + + + + {children} + + ) +} + +export default PrimaryNav diff --git a/src/components/header/Title/Title.stories.tsx b/src/components/header/Title/Title.stories.tsx new file mode 100644 index 0000000000..100b9984b2 --- /dev/null +++ b/src/components/header/Title/Title.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Title } from './Title' + +export default { + title: 'Header/Title', + parameters: { + info: ` +USWDS 2.0 Title component used within the Header component + +Source: https://designsystem.digital.gov/components/header/ +`, + }, +} + +const testTitle = Project Title + +export const defaultTitle = (): React.ReactElement => {testTitle} diff --git a/src/components/header/Title/Title.test.tsx b/src/components/header/Title/Title.test.tsx new file mode 100644 index 0000000000..e60ae18997 --- /dev/null +++ b/src/components/header/Title/Title.test.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { render } from '@testing-library/react' + +import { Title } from './Title' + +describe('Title component', () => { + it('renders without errors', () => { + const { queryByText } = render(Test Title) + expect(queryByText('Test Title')).toBeInTheDocument() + }) +}) diff --git a/src/components/header/Title/Title.tsx b/src/components/header/Title/Title.tsx new file mode 100644 index 0000000000..111786b0ae --- /dev/null +++ b/src/components/header/Title/Title.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import classnames from 'classnames' + +interface TitleProps { + children: React.ReactNode +} + +export const Title = ( + props: TitleProps & React.HTMLAttributes +): React.ReactElement => { + const { className, children, ...divProps } = props + const classes = classnames('usa-logo', className) + + return ( + + {children} + + ) +} + +export default Title diff --git a/src/index.ts b/src/index.ts index c995e7efc1..1d47c9ce12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,17 @@ export { Radio } from './components/forms/Radio/Radio' export { Textarea } from './components/forms/Textarea/Textarea' export { TextInput } from './components/forms/TextInput/TextInput' +/** Header Components */ +export { ExtendedNav } from './components/header/ExtendedNav/ExtendedNav' +export { Header } from './components/header/Header/Header' +export { MegaMenu } from './components/header/MegaMenu/MegaMenu' +export { Menu } from './components/header/Menu/Menu' +export { NavCloseButton } from './components/header/NavCloseButton/NavCloseButton' +export { NavList } from './components/header/NavList/NavList' +export { NavMenuButton } from './components/header/NavMenuButton/NavMenuButton' +export { PrimaryNav } from './components/header/PrimaryNav/PrimaryNav' +export { Title } from './components/header/Title/Title' + /** Truss-designed components */ export { Modal,