/* ! * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. */ import type { Meta, StoryObj } from "@storybook/react-vite"; import { storybookLayoutDecorator, StoryLabel } from "@storybook-common"; import { expect, screen, waitFor } from "storybook/test"; import { Flex } from "@blueprintjs/labs"; import { Intent, Size } from "../../common"; import { Button } from "../button/buttons"; import { PopoverNext } from "../popover-next/popoverNext"; import { Menu } from "./menu"; import { MenuDivider } from "./menuDivider"; import { MenuItem } from "./menuItem"; type MenuItemArg = React.ComponentProps; // Story args combine Menu's own props with the MenuItem props exposed in the controls table so that the // stories below can flow them down to their MenuItem children. type MenuStoryArgs = React.ComponentProps & { text: MenuItemArg["text"]; icon: MenuItemArg["icon"]; label: MenuItemArg["label"]; intent: MenuItemArg["intent"]; active: MenuItemArg["active"]; disabled: MenuItemArg["disabled"]; multiline: MenuItemArg["multiline"]; selected: MenuItemArg["selected"]; roleStructure: MenuItemArg["roleStructure"]; shouldDismissPopover: MenuItemArg["shouldDismissPopover"]; }; // These props are deprecated on Menu — hide them from the Storybook controls panel. const disabledArgs = ["large", "small"] as const satisfies ReadonlyArray>; const meta: Meta = { title: "Core/Menu", component: Menu, subcomponents: { MenuItem, MenuDivider }, decorators: [storybookLayoutDecorator], parameters: { layout: "centered", }, tags: ["autodocs"], args: { size: "medium", active: false, disabled: false, multiline: false, shouldDismissPopover: true, }, argTypes: { size: { control: "select", options: Object.values(Size), table: { category: "Menu" }, }, text: { control: "text", table: { category: "MenuItem" } }, icon: { control: "text", table: { category: "MenuItem" } }, label: { control: "text", table: { category: "MenuItem" } }, intent: { control: "select", options: Object.values(Intent), table: { category: "MenuItem" }, }, active: { control: "boolean", table: { category: "MenuItem" } }, disabled: { control: "boolean", table: { category: "MenuItem" } }, multiline: { control: "boolean", table: { category: "MenuItem" } }, selected: { control: "boolean", table: { category: "MenuItem" } }, roleStructure: { control: "select", options: ["menuitem", "listoption", "listitem", "none"], table: { category: "MenuItem" }, }, shouldDismissPopover: { control: "boolean", table: { category: "MenuItem" } }, ...disabledArgs.reduce( (acc, argName) => { acc[argName] = { table: { disable: true } }; return acc; }, {} as Record<(typeof disabledArgs)[number], { table: { disable: boolean } }>, ), }, } satisfies Meta; export default meta; type Story = StoryObj; // Split combined story args into the props that belong on vs. the shared props applied to each // . This lets the MenuItem controls in the table drive the items without leaking onto the
    . function splitArgs({ text, icon, label, intent, active, disabled, multiline, selected, roleStructure, shouldDismissPopover, ...menuProps }: MenuStoryArgs) { return { menuProps, itemArgs: { intent, active, disabled, multiline, selected, roleStructure, shouldDismissPopover }, }; } export const Default: Story = { render: args => { const { menuProps, itemArgs } = splitArgs(args); return ( ); }, }; export const IntentExample: Story = { name: "Intent", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); return ( {Object.values(Intent).map(intent => ( {intent}} intent={intent} /> ))} ); }, }; export const SizeExample: Story = { name: "Size", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); return ( {Object.values(Size).map(size => ( ))} ); }, }; export const StateExample: Story = { name: "State", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); return ( ); }, }; export const IconExample: Story = { name: "Icons", render: args => { const { menuProps } = splitArgs(args); return ( Ctrl+N} /> ); }, }; /** * Use a `MenuItem`'s `label` prop for a string label (e.g. a keyboard shortcut), or `labelElement` for a JSX label. */ export const LabelExample: Story = { name: "Label", render: args => { const { menuProps } = splitArgs(args); return ( ⌘S} /> ); }, }; /** * Pass `MenuItem` children to render a submenu that appears in a popover on hover or click. */ export const SubmenuExample: Story = { name: "Submenu", render: args => { const { menuProps } = splitArgs(args); return ( ); }, }; /** * Use a `MenuItem`'s `multiline` prop to allow long text to wrap to multiple lines instead of being * truncated with an ellipsis. The parent menu must have a constrained width for wrapping to be observable. */ export const MultilineExample: Story = { name: "Multiline", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); const longText = "This is a very long menu item label that will not fit on a single line"; return (
    ); }, }; /** * Use a `MenuItem`'s `roleStructure` to adapt its ARIA role to the parent. `"menuitem"` (default) suits a * `
      `, `"listoption"` suits a `
        ` and enables the selected tick icon, * and `"listitem"` suits a plain `
          `. */ export const RoleStructureExample: Story = { name: "Role Structure", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); return ( ); }, }; /** * Use a `MenuItem`'s `tagName` to change the HTML tag that wraps it. Defaults to `"a"`; set to `"button"` * for items that don't navigate, or `"div"` when nesting inside another interactive element. */ export const TagNameExample: Story = { name: "Tag Name", render: args => { const { menuProps } = splitArgs(args); return ( ); }, }; /** * By default, clicking a non-submenu item dismisses its parent popover. Set `shouldDismissPopover` to * `false` to keep the popover open after click — useful for multi-select menus or persistent toggles. */ export const ShouldDismissPopoverExample: Story = { name: "Should Dismiss Popover", argTypes: { size: { table: { disable: true } }, }, render: args => { const { menuProps } = splitArgs(args); return (
} placement="bottom" >