Files
cadence-ui/apps/docs/src/components/dropdown-menu.stories.tsx
T

216 lines
7.4 KiB
TypeScript

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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">{triggerLabel}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Launch actions</DropdownMenuLabel>
<DropdownMenuItem>
Review summary
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Share preview
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem disabled>
Retry checks
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Notify stakeholders</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup value="staged">
<DropdownMenuRadioItem value="staged">Staged rollout</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="global">Global rollout</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger inset>More actions</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Duplicate release</DropdownMenuItem>
<DropdownMenuItem variant="destructive">Archive release</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
}
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<typeof DropdownMenu>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => <ReleaseMenu />,
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: () => (
<div className="grid w-[680px] gap-3 sm:grid-cols-2">
<ReleaseMenu triggerLabel="Review lane menu" />
<ReleaseMenu triggerLabel="Launch action menu" />
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[720px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 text-[var(--color-foreground)] shadow-[var(--shadow-sm)]">
<div className="space-y-3">
<p className="text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Dropdown menu anatomy
</p>
<ReleaseMenu triggerLabel="Inspect menu structure" />
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
<code className="text-[var(--color-foreground)]">data-slot="content"</code> frames
the floating panel and exposes sizing for denser menus.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="item"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot="trigger"</code>, and{" "}
<code className="text-[var(--color-foreground)]">data-slot="shortcut"</code> map the
action rows, nested trigger, and keyboard hint.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="label"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot="separator"</code>, and{" "}
<code className="text-[var(--color-foreground)]">data-slot="icon"</code> support
grouping, dividers, and selection markers.
</p>
</div>
</div>
</div>
)
};
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: () => (
<div className="grid w-[760px] gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<article className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]">
<h3 className="text-lg font-semibold tracking-[var(--tracking-tight)]">
Keyboard guidance
</h3>
<div className="mt-4 grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>Use labels and separators to group commands into short scannable clusters.</p>
<p>
Keep checkbox and radio items in menus only when the state change is immediate
and local to the current context.
</p>
<p>
Prefer concise labels. Long explanatory copy belongs in a dialog or popover,
not in a menu row.
</p>
</div>
</article>
<div className="flex items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-[var(--color-border-strong)] bg-[var(--color-background)] p-6">
<ReleaseMenu triggerLabel="Open keyboard-friendly menu" />
</div>
</div>
)
};