mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Use Headless UI v2 data attributes and anchor positioning for UserMenu and ProfileTabs
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
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';
|
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.`;
|
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 });
|
const userButton = canvas.getByRole('button', { name: /hans/i });
|
||||||
await expect(userButton).toBeInTheDocument();
|
await expect(userButton).toBeInTheDocument();
|
||||||
await userEvent.click(userButton);
|
await userEvent.click(userButton);
|
||||||
await expect(canvas.getByRole('menuitem', { name: /my profile/i })).toBeInTheDocument();
|
// MenuItems is anchor-positioned, which portals it outside canvasElement.
|
||||||
await expect(canvas.getByRole('menuitem', { name: /dashboard/i })).toBeInTheDocument();
|
await expect(screen.getByRole('menuitem', { name: /my profile/i })).toBeInTheDocument();
|
||||||
await expect(canvas.getByRole('menuitem', { name: /logout/i })).toBeInTheDocument();
|
await expect(screen.getByRole('menuitem', { name: /dashboard/i })).toBeInTheDocument();
|
||||||
|
await expect(screen.getByRole('menuitem', { name: /logout/i })).toBeInTheDocument();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,29 +7,36 @@ interface UserMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserMenu = ({ username, userAccountId }: UserMenuProps) => (
|
const UserMenu = ({ username, userAccountId }: UserMenuProps) => (
|
||||||
<Menu as="div" className="relative">
|
<Menu>
|
||||||
<MenuButton className="btn btn-ghost btn-sm">{username}</MenuButton>
|
<MenuButton className="btn btn-ghost btn-sm">{username}</MenuButton>
|
||||||
<MenuItems className="menu absolute right-0 z-60 mt-2 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-xl focus:outline-none">
|
<MenuItems
|
||||||
|
anchor="bottom end"
|
||||||
|
transition
|
||||||
|
className="menu z-60 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-xl [--anchor-gap:8px] focus:outline-none transition duration-100 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||||
|
>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ focus }) => (
|
<Link
|
||||||
<Link to={`/users/${userAccountId}`} className={focus ? 'active' : ''}>
|
to={`/users/${userAccountId}`}
|
||||||
|
className="rounded-field block px-3 py-1.5 data-[focus]:bg-base-200"
|
||||||
|
>
|
||||||
My profile
|
My profile
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ focus }) => (
|
<Link
|
||||||
<Link to="/dashboard" className={focus ? 'active' : ''}>
|
to="/dashboard"
|
||||||
|
className="rounded-field block px-3 py-1.5 data-[focus]:bg-base-200"
|
||||||
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
{({ focus }) => (
|
<Link
|
||||||
<Link to="/logout" className={focus ? 'active' : ''}>
|
to="/logout"
|
||||||
|
className="rounded-field block px-3 py-1.5 data-[focus]:bg-base-200"
|
||||||
|
>
|
||||||
Logout
|
Logout
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Tab, TabGroup, TabList } from '@headlessui/react';
|
||||||
|
|
||||||
export type ProfileTab = 'activity' | 'following' | 'liked';
|
export type ProfileTab = 'activity' | 'following' | 'liked';
|
||||||
|
|
||||||
const TABS: { id: ProfileTab; label: string }[] = [
|
const TABS: { id: ProfileTab; label: string }[] = [
|
||||||
@@ -11,28 +13,23 @@ interface ProfileTabsProps {
|
|||||||
onChange: (tab: ProfileTab) => void;
|
onChange: (tab: ProfileTab) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => (
|
const ProfileTabs = ({ activeTab, onChange }: ProfileTabsProps) => {
|
||||||
<div className="flex gap-6 border-b border-base-300" role="tablist">
|
const selectedIndex = TABS.findIndex((tab) => tab.id === activeTab);
|
||||||
{TABS.map((tab) => {
|
|
||||||
const active = tab.id === activeTab;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<TabGroup selectedIndex={selectedIndex} onChange={(index) => onChange(TABS[index].id)}>
|
||||||
|
<TabList className="flex gap-6 border-b border-base-300">
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<Tab
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
type="button"
|
className="-mb-px border-b-2 border-transparent py-3 text-sm font-semibold text-base-content/60 transition-colors hover:text-base-content focus:outline-none data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-primary data-[selected]:border-primary data-[selected]:text-primary"
|
||||||
role="tab"
|
|
||||||
aria-selected={active}
|
|
||||||
onClick={() => onChange(tab.id)}
|
|
||||||
className={`py-3 text-sm font-semibold border-b-2 -mb-px transition-colors ${
|
|
||||||
active
|
|
||||||
? 'text-primary border-primary'
|
|
||||||
: 'text-base-content/60 border-transparent hover:text-base-content'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</Tab>
|
||||||
);
|
))}
|
||||||
})}
|
</TabList>
|
||||||
</div>
|
</TabGroup>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileTabs;
|
export default ProfileTabs;
|
||||||
|
|||||||
Reference in New Issue
Block a user