From 81717eddbf8ef3c6b186a9e5ed62e54af3c5858e Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Sat, 12 Sep 2026 18:18:38 -0400 Subject: [PATCH] Use Headless UI v2 data attributes and anchor positioning for UserMenu and ProfileTabs --- .../components/ui/navbar/Navbar.stories.tsx | 9 ++-- .../app/components/ui/navbar/UserMenu.tsx | 41 ++++++++++-------- .../users/components/profile/ProfileTabs.tsx | 43 +++++++++---------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/web/frontend/app/components/ui/navbar/Navbar.stories.tsx b/web/frontend/app/components/ui/navbar/Navbar.stories.tsx index 00e660c2..6ff5345e 100644 --- a/web/frontend/app/components/ui/navbar/Navbar.stories.tsx +++ b/web/frontend/app/components/ui/navbar/Navbar.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { expect, userEvent, within } from 'storybook/test'; +import { expect, screen, userEvent, within } from 'storybook/test'; import Navbar from './Navbar'; const navbarDescription = `Top-level navigation for the Biergarten website. These stories cover guest, authenticated, and mobile states so you can review branding, route visibility, account menu behavior, and responsive collapse without leaving Storybook.`; @@ -47,9 +47,10 @@ export const Authenticated: Story = { const userButton = canvas.getByRole('button', { name: /hans/i }); await expect(userButton).toBeInTheDocument(); await userEvent.click(userButton); - await expect(canvas.getByRole('menuitem', { name: /my profile/i })).toBeInTheDocument(); - await expect(canvas.getByRole('menuitem', { name: /dashboard/i })).toBeInTheDocument(); - await expect(canvas.getByRole('menuitem', { name: /logout/i })).toBeInTheDocument(); + // MenuItems is anchor-positioned, which portals it outside canvasElement. + await expect(screen.getByRole('menuitem', { name: /my profile/i })).toBeInTheDocument(); + await expect(screen.getByRole('menuitem', { name: /dashboard/i })).toBeInTheDocument(); + await expect(screen.getByRole('menuitem', { name: /logout/i })).toBeInTheDocument(); }, }; diff --git a/web/frontend/app/components/ui/navbar/UserMenu.tsx b/web/frontend/app/components/ui/navbar/UserMenu.tsx index 0fbf2246..1ed73e0e 100644 --- a/web/frontend/app/components/ui/navbar/UserMenu.tsx +++ b/web/frontend/app/components/ui/navbar/UserMenu.tsx @@ -7,29 +7,36 @@ interface UserMenuProps { } const UserMenu = ({ username, userAccountId }: UserMenuProps) => ( - + {username} - + - {({ focus }) => ( - - My profile - - )} + + My profile + - {({ focus }) => ( - - Dashboard - - )} + + Dashboard + - {({ focus }) => ( - - Logout - - )} + + Logout + diff --git a/web/frontend/app/features/users/components/profile/ProfileTabs.tsx b/web/frontend/app/features/users/components/profile/ProfileTabs.tsx index ec0a8c0a..174b7690 100644 --- a/web/frontend/app/features/users/components/profile/ProfileTabs.tsx +++ b/web/frontend/app/features/users/components/profile/ProfileTabs.tsx @@ -1,3 +1,5 @@ +import { Tab, TabGroup, TabList } from '@headlessui/react'; + export type ProfileTab = 'activity' | 'following' | 'liked'; const TABS: { id: ProfileTab; label: string }[] = [ @@ -11,28 +13,23 @@ interface ProfileTabsProps { onChange: (tab: ProfileTab) => void; } -const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => ( -
- {TABS.map((tab) => { - const active = tab.id === activeTab; - return ( - - ); - })} -
-); +const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => { + const selectedIndex = TABS.findIndex((tab) => tab.id === activeTab); + + return ( + onChange(TABS[index].id)}> + + {TABS.map((tab) => ( + + {tab.label} + + ))} + + + ); +}; export default ProfileTabs;