import { Button, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "@ai-ui/ui"; import type { Meta, StoryObj } from "@storybook/react"; async function waitForCondition( predicate: () => boolean, message: string, timeoutMs = 1500 ) { const startedAt = Date.now(); while (Date.now() - startedAt < timeoutMs) { if (predicate()) { return; } await new Promise((resolve) => window.setTimeout(resolve, 16)); } throw new Error(message); } type ReleaseMenuProps = { triggerLabel?: string; }; function ReleaseMenu({ triggerLabel = "Open menu" }: ReleaseMenuProps) { return ( Launch actions Review summary R Share preview S Retry checks ⌘R Notify stakeholders Staged rollout Global rollout More actions Duplicate release Archive release ); } const meta = { title: "Components/DropdownMenu", component: DropdownMenu, parameters: { docs: { description: { component: "DropdownMenu is the compact action surface for contextual commands, quick toggles, and short decision trees. It supports labels, separators, nested submenus, checkbox and radio items, destructive emphasis, and keyboard-first navigation without introducing a separate API style." } }, layout: "centered" }, tags: ["autodocs"] } satisfies Meta; export default meta; type Story = StoryObj; export const Playground: Story = { render: () => , play: async ({ canvasElement }) => { const trigger = [...canvasElement.querySelectorAll("button")].find((element) => element.textContent?.includes("Open menu") ); if (!(trigger instanceof HTMLButtonElement)) { throw new Error("Expected the dropdown trigger to render."); } trigger.click(); await waitForCondition( () => document.body.querySelector('[role="menu"]') instanceof HTMLElement, "Expected the dropdown menu to open." ); const item = [...document.body.querySelectorAll('[role="menuitem"]')].find((element) => element.textContent?.includes("Review summary") ); if (!(item instanceof HTMLElement)) { throw new Error("Expected the Review summary item to render."); } item.click(); await waitForCondition( () => document.body.querySelector('[role="menu"]') === null, "Expected the dropdown menu to close after selection." ); } }; export const States: Story = { parameters: { docs: { description: { story: "Open the menu to inspect the checked checkbox item, the selected radio item, a disabled action, the inset submenu trigger, and the destructive nested action." } } }, render: () => (
) }; export const Anatomy: Story = { render: () => (

Dropdown menu anatomy

data-slot="content" frames the floating panel and exposes sizing for denser menus.

data-slot="item",{" "} data-slot="trigger", and{" "} data-slot="shortcut" map the action rows, nested trigger, and keyboard hint.

data-slot="label",{" "} data-slot="separator", and{" "} data-slot="icon" support grouping, dividers, and selection markers.

) }; export const Accessibility: Story = { parameters: { docs: { description: { story: "Dropdown menus are optimized for keyboard and pointer parity. Focus moves with arrow keys, typeahead remains available through Radix semantics, and destructive options should stay visually distinct from neutral commands." } } }, render: () => (

Keyboard guidance

Use labels and separators to group commands into short scannable clusters.

Keep checkbox and radio items in menus only when the state change is immediate and local to the current context.

Prefer concise labels. Long explanatory copy belongs in a dialog or popover, not in a menu row.

) };