import { StoryObj, Meta } from "@storybook/react-vite"; import { within, expect, userEvent } from "storybook/test"; import { useState } from "react"; import { Popover } from "./popover"; import type {} from "../../types/popover"; import "./popover.scss"; const meta: Meta = { title: "FP.React Components/Popover", component: Popover, tags: ["stable"], parameters: { layout: "centered", docs: { description: { component: "Native HTML Popover API component with automatic top-layer rendering, light dismiss, and accessibility features. Requires Chrome 125+, Edge 125+, or Safari 17.4+.", }, }, }, argTypes: { mode: { control: "select", options: ["auto", "manual"], description: "Popover dismiss behavior", }, placement: { control: "select", options: ["top", "bottom", "left", "right"], description: "Preferred placement position", }, showArrow: { control: "boolean", description: "Show positioning arrow", }, showCloseButton: { control: "boolean", description: "Show close button", }, }, } as Meta; export default meta; type Story = StoryObj; /** * Default auto-dismiss popover with bottom placement */ export const Default: Story = { args: { id: "default-popover", triggerLabel: "Open Popover", children: ( <>

Popover Title

This popover dismisses automatically when you click outside or press Escape.

), }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); // Feature guard: the native Popover API needs Chrome 125+/Edge 125+/ // Safari 17.4+. Storybook test-runner's pinned Chromium can lag those // versions depending on Playwright build, so calling showPopover() // throws and the interaction cascade fails. Skip gracefully instead. const supportsPopover = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype; if (!supportsPopover) return; await step("Render trigger button", async () => { const trigger = canvas.getByRole("button", { name: "Open Popover" }); expect(trigger).toBeInTheDocument(); }); await step("Click trigger opens popover", async () => { const trigger = canvas.getByRole("button", { name: "Open Popover" }); await userEvent.click(trigger); expect(canvas.getByText("Popover Title")).toBeInTheDocument(); }); await step("Escape key closes popover (auto mode)", async () => { await userEvent.keyboard("{Escape}"); // Wait for animation await new Promise((resolve) => setTimeout(resolve, 300)); expect(canvas.queryByText("Popover Title")).not.toBeInTheDocument(); }); }, }; /** * Manual mode requires explicit close action */ export const ManualMode: Story = { args: { id: "manual-popover", triggerLabel: "Open Manual Popover", mode: "manual", children: ( <>

Manual Popover

This popover requires clicking the close button or trigger to dismiss. It includes a backdrop overlay.

), }, play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); // Same Popover-API feature guard as the Default story above. const supportsPopover = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype; if (!supportsPopover) return; await step("Click trigger opens popover", async () => { const trigger = canvas.getByRole("button", { name: "Open Manual Popover", }); await userEvent.click(trigger); expect(canvas.getByText("Manual Popover")).toBeInTheDocument(); }); await step("Close button dismisses popover", async () => { const closeButton = canvas.getByRole("button", { name: "Close" }); await userEvent.click(closeButton); // Wait for animation await new Promise((resolve) => setTimeout(resolve, 300)); expect(canvas.queryByText("Manual Popover")).not.toBeInTheDocument(); }); }, }; /** * Popover with top placement */ export const TopPlacement: Story = { args: { id: "top-popover", triggerLabel: "Open Above", placement: "top", children: (

This popover appears above the trigger

), }, }; /** * Popover with left placement */ export const LeftPlacement: Story = { args: { id: "left-popover", triggerLabel: "Open Left", placement: "left", children:

This popover appears to the left

, }, }; /** * Popover with right placement */ export const RightPlacement: Story = { args: { id: "right-popover", triggerLabel: "Open Right", placement: "right", children:

This popover appears to the right

, }, }; /** * Custom trigger element */ export const CustomTrigger: Story = { args: { id: "custom-trigger-popover", trigger: ( ), children: ( <>

Custom Trigger

You can use any React element as trigger

), }, }; /** * Popover without arrow */ export const NoArrow: Story = { args: { id: "no-arrow-popover", triggerLabel: "No Arrow", showArrow: false, children:

This popover has no arrow indicator

, }, }; /** * Popover with custom styling via CSS variables */ export const CustomStyling: Story = { args: { id: "custom-styled-popover", triggerLabel: "Custom Style", styles: { "--popover-bg": "#1a1a2e", "--popover-border": "0.125rem solid #16213e", "--popover-border-radius": "0.75rem", "--popover-padding": "1.5rem", "--popover-shadow": "0 0.5rem 1rem rgba(0, 0, 0, 0.3)", color: "#eee", } as React.CSSProperties, children: ( <>

Dark Theme

Customize appearance using CSS custom properties

), }, }; /** * Controlled popover with external state */ const ControlledExample = () => { const [isOpen, setIsOpen] = useState(false); return (

Controlled Popover

State is managed externally

Current state: {isOpen ? "Open" : "Closed"}

); }; export const Controlled: Story = { render: () => , }; /** * Popover with form content */ export const WithForm: Story = { args: { id: "form-popover", triggerLabel: "Show Form", mode: "manual", children: (
{ e.preventDefault(); alert("Form submitted!"); }} >

Contact Form