Skip to content
Merged
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
132 changes: 131 additions & 1 deletion src/components/Navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,138 @@
margin-left: auto;
}

/* ── Hamburger button ─────────────────────────── */

.navbar-hamburger {
display: none;
flex-direction: column;
justify-content: center;
gap: 5px;
margin-left: auto;
background: none;
border: none;
cursor: pointer;
min-width: 44px;
min-height: 44px;
padding: 8px;
border-radius: 6px;
color: var(--color-text);
}

.navbar-hamburger:hover {
background: rgba(255, 255, 255, 0.06);
}

.navbar-hamburger-line {
display: block;
width: 22px;
height: 2px;
background: currentColor;
border-radius: 2px;
transition: transform 0.2s ease;
}

/* ── Mobile backdrop ─────────────────────────── */

.navbar-backdrop {
position: fixed;
inset: 0;
z-index: 90;
background: rgba(0, 0, 0, 0.5);
}

/* ── Mobile drawer ─────────────────────────── */

.navbar-drawer {
position: fixed;
top: 0;
right: 0;
z-index: 100;
width: 280px;
height: 100vh;
background: var(--color-surface);
border-left: 1px solid #1f2a3a;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.25s ease;
}

.navbar-drawer--open {
transform: translateX(0);
}

.navbar-drawer-nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem 0.75rem;
flex: 1;
}

.navbar-drawer-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
border-radius: 8px;
color: var(--color-muted);
font-weight: 500;
font-size: 0.925rem;
text-decoration: none;
min-height: 44px;
transition: background 0.15s ease, color 0.15s ease;
}

.navbar-drawer-link:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--color-text);
}

.navbar-drawer-link.active {
background: rgba(99, 102, 241, 0.15);
color: var(--color-primary);
}

.navbar-drawer-link-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
}

.navbar-drawer-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 0.75rem;
border-top: 1px solid #1f2a3a;
}

/* ── Mobile responsive ─────────────────────────── */

@media (max-width: 720px) {
.navbar-links {
display: none;
}

.navbar-actions {
display: none;
}

.navbar-hamburger {
display: flex;
}
}

@media (prefers-contrast: more) {
.navbar {
border-bottom-width: 2px;
}
}

.navbar-drawer {
border-left-width: 2px;
}
}
86 changes: 81 additions & 5 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { useState, useEffect, useCallback } from 'react'
import { NavLink, Link } from 'react-router-dom'
import WalletButton from './WalletButton.jsx'
import LocaleSelect from './LocaleSelect.jsx'
import { useApp } from '../context/AppContext.jsx'
import './Navbar.css'

/**
* Top navigation bar with links, the locale preference and the wallet
* connect button.
*/
export default function Navbar() {
const { locale, setLocale } = useApp()
const [menuOpen, setMenuOpen] = useState(false)

const closeMenu = useCallback(() => setMenuOpen(false), [])

useEffect(() => {
if (!menuOpen) return
const handler = (e) => { if (e.key === 'Escape') closeMenu() }
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [menuOpen, closeMenu])

useEffect(() => {
if (menuOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => { document.body.style.overflow = '' }
}, [menuOpen])

return (
<header className="navbar">
Expand Down Expand Up @@ -39,6 +55,66 @@ export default function Navbar() {
/>
<WalletButton />
</div>

<button
type="button"
className="navbar-hamburger"
onClick={() => setMenuOpen((prev) => !prev)}
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={menuOpen}
>
<span className="navbar-hamburger-line" />
<span className="navbar-hamburger-line" />
<span className="navbar-hamburger-line" />
</button>

{menuOpen && (
<div className="navbar-backdrop" onClick={closeMenu} />
)}

<aside
className={`navbar-drawer${menuOpen ? ' navbar-drawer--open' : ''}`}
aria-label="Mobile navigation"
>
<nav className="navbar-drawer-nav">
<NavLink to="/" end className="navbar-drawer-link" onClick={closeMenu}>
<span className="navbar-drawer-link-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M3 7L10 2L17 7V17H12V11H8V17H3V7Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/>
</svg>
</span>
Home
</NavLink>
<NavLink to="/send" className="navbar-drawer-link" onClick={closeMenu}>
<span className="navbar-drawer-link-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M4 16L16 4M16 4H7M16 4V13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</span>
Send Money
</NavLink>
<NavLink to="/transfers" className="navbar-drawer-link" onClick={closeMenu}>
<span className="navbar-drawer-link-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<rect x="2" y="4" width="16" height="13" rx="2" stroke="currentColor" strokeWidth="1.5"/>
<path d="M2 8H18" stroke="currentColor" strokeWidth="1.5"/>
<path d="M6 12H11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
</span>
Transfers
</NavLink>
</nav>

<div className="navbar-drawer-actions">
<LocaleSelect
value={locale}
onChange={setLocale}
id="drawer-locale"
ariaLabel="Language & region"
/>
<WalletButton />
</div>
</aside>
</header>
)
}
}
2 changes: 1 addition & 1 deletion src/components/Navbar.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export default {

export const Default = {
render: () => <Navbar />
}
}
5 changes: 0 additions & 5 deletions src/responsive.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
/* Responsive tweaks for narrow viewports. */

@media (max-width: 720px) {
.navbar {
flex-wrap: wrap;
gap: 0.75rem;
}

.features {
grid-template-columns: 1fr;
}
Expand Down
98 changes: 98 additions & 0 deletions test/components/Navbar.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { describe, expect, it } from 'vitest'
import Navbar from '../../src/components/Navbar.jsx'
import { AppProvider } from '../../src/context/AppContext.jsx'

function renderNavbar(initialRoute = '/') {
return render(
<AppProvider>
<MemoryRouter initialEntries={[initialRoute]}>
<Navbar />
</MemoryRouter>
</AppProvider>
)
}

describe('Navbar', () => {
it('renders the brand name', () => {
renderNavbar()
expect(screen.getByText('RemitFlow')).toBeInTheDocument()
})

it('renders navigation links', () => {
renderNavbar()
const links = screen.getAllByText('Home')
expect(links.length).toBe(2)
expect(screen.getAllByText('Send Money').length).toBe(2)
expect(screen.getAllByText('Transfers').length).toBe(2)
})

it('highlights the active link based on current route', () => {
renderNavbar('/send')
const sendLinks = screen.getAllByText('Send Money')
const activeLinks = sendLinks.filter((l) => l.className.includes('active'))
expect(activeLinks.length).toBe(2)
})

it('has a hamburger button with aria-label', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
expect(hamburger).toBeInTheDocument()
expect(hamburger).toHaveAttribute('aria-expanded', 'false')
})

it('opens the mobile drawer when hamburger is clicked', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
fireEvent.click(hamburger)
expect(screen.getByLabelText('Close menu')).toBeInTheDocument()
expect(hamburger.getAttribute('aria-expanded')).toBe('true')
const drawer = document.querySelector('.navbar-drawer')
expect(drawer).toHaveClass('navbar-drawer--open')
})

it('closes the mobile drawer when a nav link is clicked', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
fireEvent.click(hamburger)

const homeLink = screen.getAllByText('Home')[1]
fireEvent.click(homeLink)

expect(screen.getByLabelText('Open menu')).toBeInTheDocument()
const drawer = document.querySelector('.navbar-drawer')
expect(drawer).not.toHaveClass('navbar-drawer--open')
})

it('closes the mobile drawer on Escape key', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
fireEvent.click(hamburger)

fireEvent.keyDown(document, { key: 'Escape' })

expect(screen.getByLabelText('Open menu')).toBeInTheDocument()
})

it('closes the mobile drawer when backdrop is clicked', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
fireEvent.click(hamburger)

const backdrop = document.querySelector('.navbar-backdrop')
fireEvent.click(backdrop)

expect(screen.getByLabelText('Open menu')).toBeInTheDocument()
})

it('renders the LocaleSelect and WalletButton in the drawer on mobile', () => {
renderNavbar()
const hamburger = screen.getByLabelText('Open menu')
fireEvent.click(hamburger)

const localeSelects = screen.getAllByLabelText('Language & region')
expect(localeSelects.length).toBe(2)
expect(screen.getAllByText('Connect Wallet').length).toBe(2)
})
})
9 changes: 9 additions & 0 deletions test/touch-targets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ describe('touch target minimum sizes (44px)', () => {
it('.navbar-link has min-height 44px', () => {
expect(css).toMatch(/\.navbar-link\s*\{[^}]*min-height:\s*44px/);
});

it('.navbar-hamburger has min-height and min-width 44px', () => {
expect(css).toMatch(/\.navbar-hamburger\s*\{[^}]*min-width:\s*44px/);
expect(css).toMatch(/\.navbar-hamburger\s*\{[^}]*min-height:\s*44px/);
});

it('.navbar-drawer-link has min-height 44px', () => {
expect(css).toMatch(/\.navbar-drawer-link\s*\{[^}]*min-height:\s*44px/);
});
});

describe('Footer', () => {
Expand Down
Loading