Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/features/landing/components/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import { I18nProvider } from '../../../shared/i18n'
Expand Down Expand Up @@ -81,3 +81,43 @@ describe('Navbar i18n strings', () => {
expect(screen.getAllByText('Sign Out').length).toBeGreaterThanOrEqual(2)
})
})

describe('Navbar mobile menu focus trap', () => {
beforeEach(() => {
vi.clearAllMocks()
document.body.innerHTML = ''
})

it('moves focus into the mobile menu when opened', async () => {
const user = userEvent.setup()
mockUseAuth.mockReturnValue({ isAuthenticated: false, logout: vi.fn() })
renderNavbar()

await user.click(screen.getByLabelText('Open menu'))

const mobileMenu = document.getElementById('mobile-menu')
expect(mobileMenu).toBeInTheDocument()
// The first focusable element inside the menu should receive focus.
expect(mobileMenu?.contains(document.activeElement)).toBe(true)
})

it('closes the mobile menu when Escape is pressed and returns focus to the toggle', async () => {
const user = userEvent.setup()
mockUseAuth.mockReturnValue({ isAuthenticated: false, logout: vi.fn() })
renderNavbar()

const toggle = screen.getByLabelText('Open menu')
await user.click(toggle)

// Mobile menu should be visible.
expect(screen.getByLabelText('Close menu')).toBeInTheDocument()

// Press Escape to close the menu.
fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' })

// Menu should close and the toggle button should regain focus.
expect(screen.getByLabelText('Open menu')).toBeInTheDocument()
// The toggle button should have focus (returnFocus from useFocusTrap).
expect(screen.getByLabelText('Open menu')).toHaveFocus()
})
})
9 changes: 8 additions & 1 deletion src/features/landing/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from 'react'
import { useModeAnimation } from 'react-theme-switch-animation'
import { useTheme } from '../../../shared/contexts/ThemeContext'
import { useAuth } from '../../../shared/contexts/AuthContext'
import { useFocusTrap } from '../../../shared/utils/focusTrap'
import grainlifyLogo from '../../../assets/grainlify_log.svg'

export function Navbar() {
Expand All @@ -15,6 +16,12 @@ export function Navbar() {
isDarkMode: theme === 'dark',
onDarkModeChange: (isDark) => setThemeFromAnimation(isDark),
})
const mobileMenuRef = useFocusTrap<HTMLDivElement>(mobileMenuOpen, {
onEscape: () => {
setMobileMenuOpen(false)
// Focus returns to the hamburger toggle automatically via useFocusTrap's returnFocus
},
})

return (
<nav
Expand Down Expand Up @@ -156,7 +163,7 @@ export function Navbar() {

{/* Mobile Menu */}
{mobileMenuOpen && (
<div id="mobile-menu" className="md:hidden mt-4 pb-4 space-y-4">
<div id="mobile-menu" ref={mobileMenuRef} className="md:hidden mt-4 pb-4 space-y-4">
<a
href="#features"
className={`block transition-colors font-medium ${
Expand Down
Loading