diff --git a/.yarn/cache/@esbuild-darwin-x64-npm-0.18.20-767fe27d1b-10.zip b/.yarn/cache/@esbuild-darwin-x64-npm-0.18.20-767fe27d1b-10.zip new file mode 100644 index 00000000..e9c87279 Binary files /dev/null and b/.yarn/cache/@esbuild-darwin-x64-npm-0.18.20-767fe27d1b-10.zip differ diff --git a/.yarn/cache/@esbuild-darwin-x64-npm-0.19.12-b0a4fc6ed4-10.zip b/.yarn/cache/@esbuild-darwin-x64-npm-0.19.12-b0a4fc6ed4-10.zip new file mode 100644 index 00000000..7450effe Binary files /dev/null and b/.yarn/cache/@esbuild-darwin-x64-npm-0.19.12-b0a4fc6ed4-10.zip differ diff --git a/.yarn/cache/@esbuild-linux-x64-npm-0.18.20-de8e99b449-10.zip b/.yarn/cache/@esbuild-linux-x64-npm-0.18.20-de8e99b449-10.zip deleted file mode 100644 index 67ac3efe..00000000 Binary files a/.yarn/cache/@esbuild-linux-x64-npm-0.18.20-de8e99b449-10.zip and /dev/null differ diff --git a/.yarn/cache/@esbuild-linux-x64-npm-0.19.12-59062fdb38-10.zip b/.yarn/cache/@esbuild-linux-x64-npm-0.19.12-59062fdb38-10.zip deleted file mode 100644 index 2f5ee528..00000000 Binary files a/.yarn/cache/@esbuild-linux-x64-npm-0.19.12-59062fdb38-10.zip and /dev/null differ diff --git a/.yarn/cache/@remix-run-router-npm-1.19.2-3b51b8b09c-31b62b66ea.zip b/.yarn/cache/@remix-run-router-npm-1.19.2-3b51b8b09c-31b62b66ea.zip new file mode 100644 index 00000000..71220101 Binary files /dev/null and b/.yarn/cache/@remix-run-router-npm-1.19.2-3b51b8b09c-31b62b66ea.zip differ diff --git a/.yarn/cache/@rollup-rollup-darwin-x64-npm-4.9.6-0a99a5487b-10.zip b/.yarn/cache/@rollup-rollup-darwin-x64-npm-4.9.6-0a99a5487b-10.zip new file mode 100644 index 00000000..a224d301 Binary files /dev/null and b/.yarn/cache/@rollup-rollup-darwin-x64-npm-4.9.6-0a99a5487b-10.zip differ diff --git a/.yarn/cache/@rollup-rollup-linux-x64-gnu-npm-4.9.6-c67944ac17-10.zip b/.yarn/cache/@rollup-rollup-linux-x64-gnu-npm-4.9.6-c67944ac17-10.zip deleted file mode 100644 index fb5849b0..00000000 Binary files a/.yarn/cache/@rollup-rollup-linux-x64-gnu-npm-4.9.6-c67944ac17-10.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-query-core-npm-4.13.4-06c6632de9-a0ecb41d4a.zip b/.yarn/cache/@tanstack-query-core-npm-4.13.4-06c6632de9-a0ecb41d4a.zip deleted file mode 100644 index 8e25b161..00000000 Binary files a/.yarn/cache/@tanstack-query-core-npm-4.13.4-06c6632de9-a0ecb41d4a.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-query-core-npm-4.36.1-7594b7a096-7c648872cd.zip b/.yarn/cache/@tanstack-query-core-npm-4.36.1-7594b7a096-7c648872cd.zip new file mode 100644 index 00000000..bc83efde Binary files /dev/null and b/.yarn/cache/@tanstack-query-core-npm-4.36.1-7594b7a096-7c648872cd.zip differ diff --git a/.yarn/cache/@tanstack-react-query-npm-4.13.5-d478a3e963-56bb73e036.zip b/.yarn/cache/@tanstack-react-query-npm-4.13.5-d478a3e963-56bb73e036.zip deleted file mode 100644 index 4f0356ac..00000000 Binary files a/.yarn/cache/@tanstack-react-query-npm-4.13.5-d478a3e963-56bb73e036.zip and /dev/null differ diff --git a/.yarn/cache/@tanstack-react-query-npm-4.36.1-435eddf619-764b860c3a.zip b/.yarn/cache/@tanstack-react-query-npm-4.36.1-435eddf619-764b860c3a.zip new file mode 100644 index 00000000..4bfd01bb Binary files /dev/null and b/.yarn/cache/@tanstack-react-query-npm-4.36.1-435eddf619-764b860c3a.zip differ diff --git a/.yarn/cache/esbuild-darwin-64-npm-0.15.18-c3c12de20e-10.zip b/.yarn/cache/esbuild-darwin-64-npm-0.15.18-c3c12de20e-10.zip new file mode 100644 index 00000000..c4577506 Binary files /dev/null and b/.yarn/cache/esbuild-darwin-64-npm-0.15.18-c3c12de20e-10.zip differ diff --git a/.yarn/cache/esbuild-linux-64-npm-0.15.18-b7675c5a72-10.zip b/.yarn/cache/esbuild-linux-64-npm-0.15.18-b7675c5a72-10.zip deleted file mode 100644 index 632da311..00000000 Binary files a/.yarn/cache/esbuild-linux-64-npm-0.15.18-b7675c5a72-10.zip and /dev/null differ diff --git a/.yarn/cache/fsevents-patch-6b67494872-10.zip b/.yarn/cache/fsevents-patch-6b67494872-10.zip new file mode 100644 index 00000000..9887ada7 Binary files /dev/null and b/.yarn/cache/fsevents-patch-6b67494872-10.zip differ diff --git a/.yarn/cache/history-npm-5.3.0-00136b6a63-52ba685b84.zip b/.yarn/cache/history-npm-5.3.0-00136b6a63-52ba685b84.zip deleted file mode 100644 index 5cdcf3ce..00000000 Binary files a/.yarn/cache/history-npm-5.3.0-00136b6a63-52ba685b84.zip and /dev/null differ diff --git a/.yarn/cache/jest-matchmedia-mock-npm-1.1.0-645cb1ecbf-e812b9dce5.zip b/.yarn/cache/jest-matchmedia-mock-npm-1.1.0-645cb1ecbf-e812b9dce5.zip new file mode 100644 index 00000000..d28def13 Binary files /dev/null and b/.yarn/cache/jest-matchmedia-mock-npm-1.1.0-645cb1ecbf-e812b9dce5.zip differ diff --git a/.yarn/cache/react-router-dom-npm-6.26.2-f8b4afffaf-4eee37839b.zip b/.yarn/cache/react-router-dom-npm-6.26.2-f8b4afffaf-4eee37839b.zip new file mode 100644 index 00000000..0256a69b Binary files /dev/null and b/.yarn/cache/react-router-dom-npm-6.26.2-f8b4afffaf-4eee37839b.zip differ diff --git a/.yarn/cache/react-router-dom-npm-6.3.0-508f6547e5-84fe5cd522.zip b/.yarn/cache/react-router-dom-npm-6.3.0-508f6547e5-84fe5cd522.zip deleted file mode 100644 index 61ebf0ba..00000000 Binary files a/.yarn/cache/react-router-dom-npm-6.3.0-508f6547e5-84fe5cd522.zip and /dev/null differ diff --git a/.yarn/cache/react-router-npm-6.26.2-3f4f7686d6-496e855b53.zip b/.yarn/cache/react-router-npm-6.26.2-3f4f7686d6-496e855b53.zip new file mode 100644 index 00000000..5d55f398 Binary files /dev/null and b/.yarn/cache/react-router-npm-6.26.2-3f4f7686d6-496e855b53.zip differ diff --git a/.yarn/cache/react-router-npm-6.3.0-5ffd519487-eadb572d1d.zip b/.yarn/cache/react-router-npm-6.3.0-5ffd519487-eadb572d1d.zip deleted file mode 100644 index 2fc7a5ec..00000000 Binary files a/.yarn/cache/react-router-npm-6.3.0-5ffd519487-eadb572d1d.zip and /dev/null differ diff --git a/package.json b/package.json index ed81baeb..84b7051b 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,12 @@ "@cfpb/cfpb-expandables": "^0.35.0", "@cfpb/cfpb-forms": "^0.35.0", "@cfpb/cfpb-icons": "^1.2.0", - "@tanstack/react-query": "4.13.5", + "@tanstack/react-query": "^4.13.5", "classnames": "^2.3.2", "display-element-css": "cfpb/storybook-addon-display-element-css", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "6.3.0", + "react-router-dom": "^6.3.0", "react-select": "^5.7.2" }, "devDependencies": { @@ -108,6 +108,7 @@ "eslint-plugin-testing-library": "5.9.1", "eslint-plugin-unicorn": "45.0.2", "husky": "8.0.2", + "jest-matchmedia-mock": "1.1.0", "jsdom": "21.0.0", "less": "^4.1.3", "lint-staged": "13.0.3", diff --git a/src/components/Banner/Banner.less b/src/components/Banner/Banner.less index 5ba3da3a..066fec06 100644 --- a/src/components/Banner/Banner.less +++ b/src/components/Banner/Banner.less @@ -41,6 +41,13 @@ float: left; } + .wrapper__match-content { + padding-left: @space-sm; + @media (min-width: 600px) { + padding-left: @space-md; + } + } + .m-global-eyebrow_languages { text-align: right; } diff --git a/src/components/Footer/Footer.less b/src/components/Footer/Footer.less index fce2bff0..83dc699f 100644 --- a/src/components/Footer/Footer.less +++ b/src/components/Footer/Footer.less @@ -195,4 +195,18 @@ .o-footer .cf-icon-svg__external-link { margin-left: 3px; -} \ No newline at end of file +} + +@media (max-width: @bp-sm-min - 1) { + .o-footer { + .m-list_link, + .o-footer-post, + .m-social-media { + padding-left: @space-sm; + } + + .o-footer_top-button { + width: 95%; + } + } +} diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index c0b06a69..c2403aaf 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -41,7 +41,7 @@ interface NavbarProperties { export default function Navbar({ links, href }: NavbarProperties): JSX.Element { return (
-
+
diff --git a/src/components/Navbar/navbar.less b/src/components/Navbar/navbar.less index e800721e..900a4cab 100644 --- a/src/components/Navbar/navbar.less +++ b/src/components/Navbar/navbar.less @@ -2,17 +2,19 @@ @breakpoint: 56.3125em; -.navbar { +.navbar-static { display: flex; flex-direction: column; align-items: flex-start; font-size: 16px; padding-bottom: @space-sm; + padding-left: @space-sm; @media (min-width: @breakpoint) { flex-direction: row; justify-content: space-between; align-items: center; + padding-left: 0; .o-header_logo { min-width: 237px; diff --git a/src/components/PageHeader/PageHeader.tsx b/src/components/PageHeader/PageHeader.tsx index 7c6e5623..8b68861f 100644 --- a/src/components/PageHeader/PageHeader.tsx +++ b/src/components/PageHeader/PageHeader.tsx @@ -1,7 +1,7 @@ import classnames from 'classnames'; import { Banner } from '../Banner/Banner'; -import type { User } from '../Navbar/Navbar'; -import Navbar from '../Navbar/Navbar'; +import type { User } from '../ResponsiveMenu/ResponsiveMenu'; +import ResponsiveMenu from '../ResponsiveMenu/ResponsiveMenu'; import './header.less'; interface PageHeaderProperties { @@ -23,7 +23,7 @@ export default function PageHeader({ return (
- +
); } diff --git a/src/components/PageHeader/header.less b/src/components/PageHeader/header.less index fc5b0802..32b6bc1c 100644 --- a/src/components/PageHeader/header.less +++ b/src/components/PageHeader/header.less @@ -33,7 +33,7 @@ } @media only all and (min-width: 56.3125em) { .o-header_content { - padding-top: 0.9375em; + padding: 0.9375em 0; } .o-header_content > .wrapper > .m-global-header-cta, .o-header_content > .content_wrapper > .m-global-header-cta { diff --git a/src/components/ResponsiveMenu/ResponsiveMenu.stories.tsx b/src/components/ResponsiveMenu/ResponsiveMenu.stories.tsx new file mode 100644 index 00000000..034e6ccd --- /dev/null +++ b/src/components/ResponsiveMenu/ResponsiveMenu.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ResponsiveMenu } from '~/src/index'; +import '../PageHeader/header.less'; +import { ExampleLinks } from './ResponsiveMenu'; + +const meta: Meta = { + title: 'Components (Draft)/ResponsiveMenu', + tags: ['autodocs'], + component: ResponsiveMenu, + argTypes: {} +}; + +export default meta; + +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + links: ExampleLinks + } +}; + +export const LoggedOut: Story = { + args: {} +}; + +export const NoUser: Story = { + args: {} +}; diff --git a/src/components/ResponsiveMenu/ResponsiveMenu.test.tsx b/src/components/ResponsiveMenu/ResponsiveMenu.test.tsx new file mode 100644 index 00000000..d9666131 --- /dev/null +++ b/src/components/ResponsiveMenu/ResponsiveMenu.test.tsx @@ -0,0 +1,114 @@ +import '@testing-library/jest-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; +import matchMediaMock from 'jest-matchmedia-mock'; +import ResponsiveMenu, { ExampleLinks } from './ResponsiveMenu'; + +let matchMedia: matchMediaMock; + +describe('ResponsiveMenu', () => { + beforeAll(() => { + matchMedia = new matchMediaMock(); + }); + + afterEach(() => { + matchMedia.clear(); + }); + + const resizeScreenSize = (width: number) => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: width + }); + window.dispatchEvent(new Event('resize')); + }; + + it('does not render the menu without links', () => { + resizeScreenSize(500); + render(); + + expect(screen.queryByTestId('menu-toggle')).not.toBeInTheDocument(); + expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); + + expect(screen.getByTestId('CfpbLogoLink')).toBeInTheDocument(); + expect(screen.getByAltText('CFPB Logo')).toBeInTheDocument(); + }); + + it('renders with custom links', () => { + resizeScreenSize(500); + render(); + + // Menu is rendered + const menuToggle = screen.getAllByRole('button')[0]; + expect(menuToggle).toBeInTheDocument(); + expect(screen.getByRole('navigation')).toBeInTheDocument(); + + // Links are rendered + expect(screen.getByText('Home')).toBeInTheDocument(); + expect(screen.getByText('Filing')).toBeInTheDocument(); + expect(screen.getByText('John Sample')).toBeInTheDocument(); + expect(screen.getByText('LOG OUT')).toBeInTheDocument(); + }); + + it('toggles menu visibility on button click', () => { + resizeScreenSize(500); + + render(); + const menuToggle = screen.getAllByRole('button')[0]; + const navItems = screen.getByRole('navigation'); + expect(navItems).not.toHaveClass('open'); + expect(menuToggle).toHaveAttribute('aria-expanded', 'false'); + + fireEvent.click(menuToggle); + + expect(navItems).toHaveClass('open'); + expect(menuToggle).toHaveAttribute('aria-expanded', 'true'); + expect(menuToggle.querySelector('.sr-only')).toHaveTextContent( + 'Close menu' + ); + + fireEvent.click(menuToggle); + + expect(navItems).not.toHaveClass('open'); + expect(menuToggle).toHaveAttribute('aria-expanded', 'false'); + expect(menuToggle.querySelector('.sr-only')).toHaveTextContent('Open menu'); + }); + + it('closes menu when clicking overlay', () => { + render(); + const menuToggle = screen.getAllByRole('button')[0]; + fireEvent.click(menuToggle); + + const overlay = screen.getByLabelText('Close menu'); + fireEvent.click(overlay); + + expect(screen.getByRole('navigation')).not.toHaveClass('open'); + }); + + it('closes menu when clicking a link', () => { + render(); + const menuToggle = screen.getAllByRole('button')[0]; + fireEvent.click(menuToggle); + + const link = screen.getByText('Home'); + fireEvent.click(link); + + expect(screen.getByRole('navigation')).not.toHaveClass('open'); + }); + + it('applies active class to the current page link', () => { + render(); + const menuToggle = screen.getAllByRole('button')[0]; + + const activeLink = screen.getByText('Filing'); + expect(activeLink).toHaveClass('active'); + }); + + it('renders CFPB logo with custom href', () => { + const customHref = 'https://example.com'; + render(); + + const logoLink = screen.getByTestId('CfpbLogoLink'); + expect(logoLink).toHaveAttribute('href', customHref); + }); +}); diff --git a/src/components/ResponsiveMenu/ResponsiveMenu.tsx b/src/components/ResponsiveMenu/ResponsiveMenu.tsx new file mode 100644 index 00000000..b7081697 --- /dev/null +++ b/src/components/ResponsiveMenu/ResponsiveMenu.tsx @@ -0,0 +1,144 @@ +import React, { useCallback, useState } from 'react'; +import CFPBLogo from '../../assets/images/cfpb-logo.png'; +import { Button } from '../Buttons/Button'; +import { Icon } from '../Icon/Icon'; +import Link from '../Link/Link'; +import Navbar from '../Navbar/Navbar'; +import './responsivemenu.less'; + +interface CfpbLogoProperties { + href?: string; +} + +export function CfpbLogo({ + href = 'https://www.consumerfinance.gov' +}: CfpbLogoProperties): JSX.Element { + return ( + + CFPB Logo + + ); +} + +const Links = ({ + elements, + onLinkClick +}: { + elements: React.ReactNode[] | undefined; + onLinkClick: () => void; +}): JSX.Element | null => { + if (!elements?.length) return null; + + return ( +
+ {elements.map((element, index) => { + if ( + React.isValidElement<{ onClick?: (event: React.MouseEvent) => void }>( + element + ) + ) { + return React.cloneElement(element, { + ...element.props, + key: element.key ?? index, + onClick: (event: React.MouseEvent) => { + if (element.props.onClick) { + element.props.onClick(event); + } + onLinkClick(); + } + }); + } + return element; + })} +
+ ); +}; + +interface ResponsiveMenuProperties { + links?: React.ReactNode[]; + href?: string; +} + +export default function ResponsiveMenu({ + links, + href +}: ResponsiveMenuProperties): JSX.Element { + const [isMenuOpen, setIsMenuOpen] = useState(false); + + const onToggleMenu = (): void => { + setIsMenuOpen(!isMenuOpen); + }; + + const onLinkClick = (): void => { + setIsMenuOpen(false); + }; + + const onHandleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + setIsMenuOpen(false); + } + }, + [] + ); + + if (!links?.length) return ; + + return ( + <> + {isMenuOpen ? ( +
+ ) : null} +
+
+ + + +
+
+ + ); +} + +export const ExampleLinks: React.ReactNode[] = [ + + Home + , + + Filing + , + + John Sample + , +