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

207 lines
6.9 KiB
TypeScript

import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} 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 LaunchDialogProps = {
description?: string;
size?: "sm" | "md" | "lg";
title?: string;
triggerLabel?: string;
};
function LaunchDialog({
description = "This will notify the routing team and publish the release note to the activity feed.",
size = "md",
title = "Launch this release?",
triggerLabel = "Open approval dialog"
}: LaunchDialogProps) {
return (
<Dialog>
<DialogTrigger asChild>
<Button>{triggerLabel}</Button>
</DialogTrigger>
<DialogContent size={size}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost">Cancel</Button>
<Button>Confirm launch</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
const meta = {
title: "Components/Dialog",
component: Dialog,
parameters: {
docs: {
description: {
component:
"Dialog is the system's blocking overlay for focused decisions, confirmation flows, and dense tasks that must temporarily interrupt the surrounding page. It ships with a portal, overlay, close affordance, semantic title and description wiring, and token-driven motion on both the surface and backdrop."
}
},
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Playground: Story = {
render: () => <LaunchDialog />,
play: async ({ canvasElement }) => {
const trigger = [...canvasElement.querySelectorAll("button")].find((element) =>
element.textContent?.includes("Open approval dialog")
);
if (!(trigger instanceof HTMLButtonElement)) {
throw new Error("Expected the dialog trigger to render.");
}
trigger.click();
await waitForCondition(
() => document.body.querySelector('[role="dialog"]') instanceof HTMLElement,
"Expected the dialog to open."
);
const closeButton = document.body.querySelector('[aria-label="Close dialog"]');
if (!(closeButton instanceof HTMLButtonElement)) {
throw new Error("Expected the dialog close control to render.");
}
closeButton.click();
await waitForCondition(
() => document.body.querySelector('[role="dialog"]') === null,
"Expected the dialog to close."
);
}
};
export const Sizes: Story = {
render: () => (
<div className="grid w-[720px] gap-3 sm:grid-cols-2">
<LaunchDialog
description="Use the compact size for short confirmations that only need a title, one supporting sentence, and one primary action."
size="sm"
title="Publish summary?"
triggerLabel="Compact dialog"
/>
<LaunchDialog
description="Use the large size when the flow needs denser copy, audit context, or multi-step review detail before a final action."
size="lg"
title="Review rollout checklist"
triggerLabel="Large dialog"
/>
</div>
)
};
export const Anatomy: Story = {
render: () => (
<div className="w-[700px] 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)]">
Dialog anatomy
</p>
<LaunchDialog triggerLabel="Preview dialog structure" />
<div className="grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>
<code className="text-[var(--color-foreground)]">data-slot="overlay"</code> sits
behind the surface and carries the backdrop motion.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="content"</code> wraps
the modal panel and exposes <code className="text-[var(--color-foreground)]">data-size</code>.
</p>
<p>
<code className="text-[var(--color-foreground)]">data-slot="header"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot="footer"</code>,{" "}
<code className="text-[var(--color-foreground)]">data-slot="label"</code>, and{" "}
<code className="text-[var(--color-foreground)]">data-slot="description"</code>
provide stable hooks for structure and docs.
</p>
<p>
The close button is built into <code className="text-[var(--color-foreground)]">DialogContent</code>,
so every dialog gets a dismiss affordance even when the footer stays minimal.
</p>
</div>
</div>
</div>
)
};
export const Accessibility: Story = {
parameters: {
docs: {
description: {
story:
"Use dialog only when the user must resolve or dismiss a blocking task. Focus is trapped while open, Escape closes the surface, and the title and description are announced through the Radix dialog semantics."
}
}
},
render: () => (
<div className="grid w-[760px] gap-4 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
<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)]">
Accessibility notes
</h3>
<div className="mt-4 grid gap-3 text-sm leading-6 text-[var(--color-muted-foreground)]">
<p>Keep the title outcome-oriented so assistive tech announces the decision clearly.</p>
<p>
Use the description for the consequence or next step, not decorative copy.
</p>
<p>
Keep the trigger specific. &quot;Open approval dialog&quot; is more useful than a generic
&quot;Open&quot;.
</p>
<p>
Reserve dialogs for blocking work. If the content should not trap focus, prefer
a popover instead.
</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">
<LaunchDialog
description="Keyboard focus moves into the surface, Escape closes it, and the trigger regains focus after dismissal."
triggerLabel="Open accessible dialog"
/>
</div>
</div>
)
};