diff --git a/apps/web/components/design-system/__tests__/navbar.spec.tsx b/apps/web/components/design-system/__tests__/navbar.spec.tsx index dbe239a..1523fce 100644 --- a/apps/web/components/design-system/__tests__/navbar.spec.tsx +++ b/apps/web/components/design-system/__tests__/navbar.spec.tsx @@ -99,24 +99,31 @@ describe('Navbar', () => { expect(screen.getAllByText('Nguyễn Văn A').length).toBeGreaterThan(0); }); - it('renders dashboard button for authenticated user', () => { + it('renders dashboard menu item for authenticated user (after opening dropdown)', () => { render( , ); - expect(screen.getByText('Quản lý')).toBeInTheDocument(); + // The pill is the dropdown trigger; click it to reveal the menu. + const trigger = screen.getByRole('button', { name: /Nguyễn Văn A/ }); + fireEvent.click(trigger); + expect(screen.getByRole('menuitem', { name: /Quản lý/ })).toBeInTheDocument(); }); - it('renders admin label for ADMIN role', () => { + it('renders admin label as a role badge AND in the dropdown for ADMIN role', () => { render( , ); - expect(screen.getByText('Quản trị')).toBeInTheDocument(); + // Role badge in the trigger pill is always visible. + expect(screen.getByText('Quản trị viên')).toBeInTheDocument(); + // After opening, the ADMIN-specific menu item shows. + fireEvent.click(screen.getByRole('button', { name: /Admin User/ })); + expect(screen.getByRole('menuitem', { name: /Quản trị/ })).toBeInTheDocument(); }); it('shows moon icon in light theme', () => { diff --git a/apps/web/components/design-system/navbar.tsx b/apps/web/components/design-system/navbar.tsx index b7417ef..af22bdb 100644 --- a/apps/web/components/design-system/navbar.tsx +++ b/apps/web/components/design-system/navbar.tsx @@ -1,6 +1,7 @@ 'use client'; import { + ChevronDown, LogOut, Menu, Moon, @@ -118,14 +119,36 @@ export function Navbar({ renderLink, }: NavbarProps) { const [mobileOpen, setMobileOpen] = React.useState(false); + const [userMenuOpen, setUserMenuOpen] = React.useState(false); + const userMenuRef = React.useRef(null); const close = () => setMobileOpen(false); const handleLogout = async () => { close(); + setUserMenuOpen(false); await onLogout(); }; + // Close the desktop user dropdown on outside click + Escape. + React.useEffect(() => { + if (!userMenuOpen) return; + const onDown = (e: MouseEvent) => { + if (userMenuRef.current && !userMenuRef.current.contains(e.target as Node)) { + setUserMenuOpen(false); + } + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setUserMenuOpen(false); + }; + document.addEventListener('mousedown', onDown); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onDown); + document.removeEventListener('keydown', onKey); + }; + }, [userMenuOpen]); + return (
{notifications}
- {/* User pill */} -
- {user.avatarUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - + + + {userMenuOpen && ( +
+ {/* Header */} +
+ {user.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( +
+ {getInitials(user.fullName)} +
+ )} +
+ + {user.fullName} + + {(user.email || user.phone) && ( + + {user.email ?? user.phone} + + )} +
+
+ +
+ {renderLink({ + href: dashboardHref, + onClick: () => setUserMenuOpen(false), + children: ( + + {user.role === 'ADMIN' ? ( + + ) : ( + + )} + {user.role === 'ADMIN' ? labels.admin : labels.dashboard} + + ), + })} + {renderLink({ + href: profileHref, + onClick: () => setUserMenuOpen(false), + children: ( + + + {labels.profile} + + ), + })} +
+ +
)} - - {user.fullName} - - {ROLE_LABELS[user.role] && ( - - {ROLE_LABELS[user.role]} - - )}
- - {renderLink({ - href: dashboardHref, - className: 'hidden sm:inline-flex', - children: ( - - ), - })} ) : ( <>