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