diff --git a/src/app/AppLayout.module.scss b/src/app/AppLayout.module.scss index 5ee1284..70b5586 100644 --- a/src/app/AppLayout.module.scss +++ b/src/app/AppLayout.module.scss @@ -15,6 +15,36 @@ position: sticky; top: 0; z-index: 10; + + @media (max-width: 720px) { + padding: var(--space-4) var(--space-5); + } +} + +.navToggle { + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.3rem; + width: 2.25rem; + height: 2.25rem; + padding: 0; + background: transparent; + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + + @media (max-width: 720px) { + display: flex; + } +} + +.navToggleBar { + display: block; + width: 1.1rem; + height: 2px; + background: var(--text); + border-radius: 1px; } .brand { @@ -29,6 +59,26 @@ .navLinks { display: flex; gap: var(--space-2); + + @media (max-width: 720px) { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + gap: 0; + padding: var(--space-3); + background: var(--surface); + border-bottom: 1px solid var(--border); + box-shadow: var(--shadow-md); + } +} + +.navLinksOpen { + @media (max-width: 720px) { + display: flex; + } } .navLink { @@ -44,6 +94,12 @@ color: var(--text); background: var(--surface-raised); } + + @media (max-width: 720px) { + width: 100%; + padding: var(--space-3); + border-radius: 0; + } } .navLinkActive { @@ -61,6 +117,10 @@ flex: 1; padding: var(--space-6) var(--space-7); max-width: 100%; + + @media (max-width: 720px) { + padding: var(--space-5) var(--space-4); + } } .disclaimer { @@ -69,4 +129,8 @@ font-size: var(--text-xs); color: var(--text-muted); text-align: center; + + @media (max-width: 720px) { + padding: var(--space-4); + } } diff --git a/src/app/AppLayout.test.tsx b/src/app/AppLayout.test.tsx new file mode 100644 index 0000000..61ec8d8 --- /dev/null +++ b/src/app/AppLayout.test.tsx @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router'; +import AppLayout from './AppLayout.tsx'; + +describe('AppLayout', () => { + it('renders all nav links', () => { + render( + + + , + ); + + expect(screen.getByRole('link', { name: 'Évaluation' })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Discovery' })).toBeInTheDocument(); + }); + + it('toggles the mobile menu open and closed', async () => { + const user = userEvent.setup(); + render( + + + , + ); + + const toggle = screen.getByTestId('nav-toggle-button'); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + + await user.click(toggle); + expect(toggle).toHaveAttribute('aria-expanded', 'true'); + + await user.click(toggle); + expect(toggle).toHaveAttribute('aria-expanded', 'false'); + }); + + it('closes the mobile menu after clicking a nav link', async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByTestId('nav-toggle-button')); + expect(screen.getByTestId('nav-toggle-button')).toHaveAttribute('aria-expanded', 'true'); + + await user.click(screen.getByRole('link', { name: 'Tirages' })); + expect(screen.getByTestId('nav-toggle-button')).toHaveAttribute('aria-expanded', 'false'); + }); + + it('closes the mobile menu when clicking outside of it', async () => { + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByTestId('nav-toggle-button')); + expect(screen.getByTestId('nav-toggle-button')).toHaveAttribute('aria-expanded', 'true'); + + await user.click(screen.getByTestId('app-content')); + expect(screen.getByTestId('nav-toggle-button')).toHaveAttribute('aria-expanded', 'false'); + }); +}); diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index 0260d66..df84744 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react'; import { NavLink, Outlet } from 'react-router'; import styles from './AppLayout.module.scss'; @@ -10,15 +11,47 @@ const NAV_ITEMS = [ ]; const AppLayout = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const navRef = useRef(null); + + useEffect(() => { + if (!isMenuOpen) return; + + const closeIfOutside = (event: MouseEvent) => { + if (navRef.current && !navRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + }; + + document.addEventListener('mousedown', closeIfOutside); + return () => document.removeEventListener('mousedown', closeIfOutside); + }, [isMenuOpen]); + return (
-