feat(web): ui/menu Base UI dropdown wrapper + story (#54)
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { expect, within } from 'storybook/test'
|
||||
|
||||
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from './menu'
|
||||
import { Button } from './button'
|
||||
|
||||
const meta = {
|
||||
component: Menu,
|
||||
tags: ['ai-generated'],
|
||||
} satisfies Meta<typeof Menu>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Menu>
|
||||
<MenuTrigger render={<Button variant="ghost">Open</Button>} />
|
||||
<MenuContent>
|
||||
<MenuItem>First</MenuItem>
|
||||
<MenuSeparator />
|
||||
<MenuItem>Second</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
),
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
await userEvent.click(canvas.getByRole('button', { name: 'Open' }))
|
||||
await expect(
|
||||
await within(document.body).findByText('First'),
|
||||
).toBeInTheDocument()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Menu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="menu" {...props} />
|
||||
}
|
||||
|
||||
function MenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} />
|
||||
}
|
||||
|
||||
function MenuContent({
|
||||
className,
|
||||
sideOffset = 6,
|
||||
align = "end",
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props & {
|
||||
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]
|
||||
align?: MenuPrimitive.Positioner.Props["align"]
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
className="z-50"
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="menu-content"
|
||||
className={cn(
|
||||
"min-w-44 rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none",
|
||||
"data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuItem({ className, ...props }: MenuPrimitive.Item.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="menu-item"
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none",
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Menu, MenuTrigger, MenuContent, MenuItem, MenuSeparator }
|
||||
Reference in New Issue
Block a user