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