/*
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/
import type { Meta, StoryObj } from "@storybook/react-vite";
import { storybookLayoutDecorator } from "@storybook-common";
import { useArgs, useCallback } from "storybook/preview-api";
import { expect, screen, waitFor } from "storybook/test";
import { Button } from "../button/buttons";
import { Menu } from "../menu/menu";
import { MenuDivider } from "../menu/menuDivider";
import { MenuItem } from "../menu/menuItem";
import { ContextMenuPopover, type ContextMenuPopoverProps } from "./contextMenuPopover";
const SAMPLE_MENU = (
);
const meta: Meta = {
title: "Core/Context Menu/ContextMenuPopover",
component: ContextMenuPopover,
decorators: [storybookLayoutDecorator],
args: {
content: SAMPLE_MENU,
isOpen: false,
transitionDuration: 0,
isDarkTheme: false,
},
argTypes: {
content: { control: false },
isOpen: { control: "boolean" },
isDarkTheme: { control: "boolean" },
targetOffset: { table: { disable: true } },
onClose: { action: "closed" },
onOpening: { table: { disable: true } },
onOpened: { table: { disable: true } },
onClosing: { table: { disable: true } },
onClosed: { table: { disable: true } },
popoverClassName: { table: { disable: true } },
popoverRef: { table: { disable: true } },
rootBoundary: { table: { disable: true } },
transitionDuration: { table: { disable: true } },
},
} satisfies Meta;
export default meta;
type Story = StoryObj;
function ContextMenuPopoverStoryRender(args: ContextMenuPopoverProps) {
const [, updateArgs] = useArgs();
const handleOpen = useCallback(
(event: React.MouseEvent) =>
updateArgs({
isOpen: true,
targetOffset: { left: event.clientX, top: event.clientY },
}),
[updateArgs],
);
const handleClose = useCallback(() => updateArgs({ isOpen: false }), [updateArgs]);
return (
<>
>
);
}
/**
* A basic controlled context menu popover. Click the button to open the menu
* anchored at the click location. Unlike `ContextMenu`, this lower-level component
* does not wire up its own right-click handler — you control its open state directly.
*/
export const Default: Story = {
render: ContextMenuPopoverStoryRender,
};
/**
* Override `placement` via the prop to control which corner of the menu anchors
* to the click point. The default is `right-start` (menu opens below and to the right).
*/
export const CustomPlacement: Story = {
name: "Custom Placement",
args: {
placement: "top-end",
},
render: ContextMenuPopoverStoryRender,
};
/**
* Pass `isDarkTheme` to render the popover with dark-theme styling. This is
* needed when calling the imperative `showContextMenu()` API outside of a
* `.bp6-dark` ancestor, since the singleton renders in its own React root.
*/
export const DarkTheme: Story = {
name: "Dark Theme",
args: {
isDarkTheme: true,
},
render: ContextMenuPopoverStoryRender,
};
/**
* Interactive playground — click the button to open the menu, then adjust props via the controls panel.
*/
export const Playground: Story = {
render: ContextMenuPopoverStoryRender,
};
/**
* Demonstrates the popover opening on click. The play function simulates a left-click
* at the center of the trigger button, which sets `targetOffset` to those coordinates.
*/
export const ClickToOpen: Story = {
name: "Click To Open",
render: ContextMenuPopoverStoryRender,
play: async ({ canvas, userEvent, step }) => {
await step("Click button to open menu at click location", async () => {
const target = canvas.getByRole("button", { name: "Click to open menu" });
const rect = target.getBoundingClientRect();
const coords = { clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 };
await userEvent.pointer({ keys: "[MouseLeft]", target, coords });
await waitFor(() => expect(screen.getByText("Select all")).toBeVisible());
});
},
};