diff --git a/src/widgets/Navigation/ui/MobileNav.test.tsx b/src/widgets/Navigation/ui/MobileNav.test.tsx index d00c34e..3d4036a 100644 --- a/src/widgets/Navigation/ui/MobileNav.test.tsx +++ b/src/widgets/Navigation/ui/MobileNav.test.tsx @@ -3,7 +3,12 @@ import userEvent from '@testing-library/user-event'; import type { NavItem } from '../model/types'; import { MobileNav } from './MobileNav'; -const ITEMS: NavItem[] = [{ id: 'about', label: 'About', number: '01' }]; +vi.mock('next/navigation', () => ({ usePathname: vi.fn(() => '/') })); + +const ITEMS: NavItem[] = [ + { id: 'intro', label: 'Intro', number: '01' }, + { id: 'bio', label: 'Bio', number: '02' }, +]; describe('MobileNav', () => { describe('rendering', () => { @@ -19,27 +24,39 @@ describe('MobileNav', () => { it('menu items are hidden initially', () => { render(); - expect(screen.queryByRole('button', { name: /about/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('link', { name: /intro/i })).not.toBeInTheDocument(); + }); + }); + + describe('navigation items', () => { + it('shows items as links with correct hrefs when open', async () => { + render(); + await userEvent.click(screen.getByRole('button', { name: 'Menu' })); + expect(screen.getByRole('link', { name: /01.*Intro/i })).toHaveAttribute('href', '/intro'); + expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio'); }); }); describe('interactions', () => { - it('click toggle shows item buttons and changes label to "Close"', async () => { + it('click toggle shows links and changes label to "Close"', async () => { render(); await userEvent.click(screen.getByRole('button', { name: 'Menu' })); expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); - expect(screen.getByText('About')).toBeInTheDocument(); + expect(screen.getByText('Intro')).toBeInTheDocument(); }); - it('click item button closes the menu', async () => { - render(); + it('closes menu when pathname changes', async () => { + const { usePathname } = await import('next/navigation'); + vi.mocked(usePathname).mockReturnValue('/'); + const { rerender } = render(); await userEvent.click(screen.getByRole('button', { name: 'Menu' })); - // item button label contains number + label text; find by accessible name fragment - const itemBtn = screen.getAllByRole('button').find((b) => b.textContent?.includes('About')); - expect(itemBtn).toBeDefined(); - await userEvent.click(itemBtn as HTMLElement); - expect(screen.queryByText('Close')).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); + + vi.mocked(usePathname).mockReturnValue('/bio'); + rerender(); + expect(screen.getByRole('button', { name: 'Menu' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument(); }); }); }); diff --git a/src/widgets/Navigation/ui/MobileNav.tsx b/src/widgets/Navigation/ui/MobileNav.tsx index 12142ca..dca3422 100644 --- a/src/widgets/Navigation/ui/MobileNav.tsx +++ b/src/widgets/Navigation/ui/MobileNav.tsx @@ -1,9 +1,14 @@ 'use client'; -import { useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useEffect, useState } from 'react'; import { cn } from '$shared/lib'; import type { NavItem } from '../model/types'; +/** + * Props for MobileNav. + */ interface Props { /** * Navigation items to render @@ -13,21 +18,16 @@ interface Props { /** * Mobile navigation overlay, hidden on lg+ screens. + * Closes automatically when the URL pathname changes after navigation. */ export function MobileNav({ items }: Props) { const [isOpen, setIsOpen] = useState(false); + const pathname = usePathname(); - /** - * Scrolls to the section by id with a 100px offset, then closes the menu. - */ - function scrollToSection(id: string) { - const el = document.getElementById(id); - if (el) { - const top = el.getBoundingClientRect().top + window.scrollY - 100; - window.scrollTo({ top, behavior: 'smooth' }); - } + // biome-ignore lint/correctness/useExhaustiveDependencies: pathname is the trigger, not a value used inside the callback + useEffect(() => { setIsOpen(false); - } + }, [pathname]); return (
@@ -44,12 +44,7 @@ export function MobileNav({ items }: Props) { {isOpen && (
{items.map((item) => ( - + ))}
)}