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