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 ( + + ) +} + +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 ( + + ) +} + +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