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

269 lines
8.2 KiB
TypeScript

import {
Button,
Form,
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Switch,
Textarea
} from "@ai-ui/ui";
import type { Meta, StoryObj } from "@storybook/react";
import { Controller, useForm } from "react-hook-form";
import { useState } from "react";
type LaunchFormValues = {
email: string;
notifications: boolean;
role: string;
summary: string;
};
async function waitForCondition(
predicate: () => boolean,
message: string,
timeoutMs = 2000
) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (predicate()) {
return;
}
await new Promise((resolve) => window.setTimeout(resolve, 16));
}
throw new Error(message);
}
function LaunchSettingsForm() {
const [submitted, setSubmitted] = useState<LaunchFormValues | null>(null);
const form = useForm<LaunchFormValues>({
defaultValues: {
email: "",
notifications: true,
role: "design",
summary: ""
}
});
return (
<Form {...form}>
<form
className="grid w-[620px] gap-5 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]"
noValidate
onSubmit={form.handleSubmit((values) => {
setSubmitted(values);
})}
>
<div className="space-y-2">
<h2 className="m-0 text-xl font-semibold tracking-[var(--tracking-tight)]">
Launch settings
</h2>
<p className="m-0 text-sm leading-6 text-[var(--color-muted-foreground)]">
This story uses <code>react-hook-form</code> with the new form primitives.
</p>
</div>
<FormItem name="email" required>
<FormLabel>Email address</FormLabel>
<FormControl>
<Input
placeholder="team@cadence.dev"
{...form.register("email", {
required: "Email is required.",
pattern: {
value: /\S+@\S+\.\S+/,
message: "Enter a valid email address."
}
})}
/>
</FormControl>
<FormDescription>Release notes and alerts will be sent here.</FormDescription>
<FormMessage />
</FormItem>
<Controller
control={form.control}
name="role"
rules={{
required: "Choose a review lane."
}}
render={({ field }) => (
<FormItem name="role">
<FormLabel>Review lane</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger aria-label="Review lane">
<SelectValue placeholder="Choose a lane" />
</SelectTrigger>
<SelectContent>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="engineering">Engineering</SelectItem>
<SelectItem value="legal">Legal</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
The selected team becomes the primary reviewer for this launch.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormItem name="summary">
<FormLabel>Launch summary</FormLabel>
<FormControl>
<Textarea
placeholder="Summarize the rollout, scope, and known risks."
{...form.register("summary", {
minLength: {
value: 24,
message: "Summary must be at least 24 characters."
}
})}
/>
</FormControl>
<FormDescription>Shown in the changelog card and Slack digest.</FormDescription>
<FormMessage />
</FormItem>
<Controller
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem name="notifications">
<div className="flex items-start justify-between gap-4 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
<div className="space-y-1">
<FormLabel>Weekly digest</FormLabel>
<FormDescription>
Send a Friday summary of all rollout changes to stakeholders.
</FormDescription>
</div>
<FormControl className="gap-0">
<Switch
aria-label="Weekly digest"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center justify-end gap-3">
<Button
type="button"
variant="ghost"
onClick={() => {
form.reset();
setSubmitted(null);
}}
>
Reset
</Button>
<Button type="submit">Save settings</Button>
</div>
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
<p className="m-0 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
Submitted payload
</p>
<pre className="m-0 mt-3 overflow-x-auto text-sm leading-6 text-[var(--color-foreground)]">
<code>{submitted ? JSON.stringify(submitted, null, 2) : "Submit the form to inspect values."}</code>
</pre>
</div>
</form>
</Form>
);
}
const meta = {
title: "Components/Form",
component: LaunchSettingsForm,
parameters: {
layout: "centered"
},
tags: ["autodocs"]
} satisfies Meta<typeof LaunchSettingsForm>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LaunchSettings: Story = {
play: async ({ canvasElement }) => {
const emailInput = canvasElement.querySelector('input[placeholder="team@cadence.dev"]');
if (!(emailInput instanceof HTMLInputElement)) {
throw new Error("Expected the email input to render.");
}
emailInput.focus();
emailInput.value = "team@cadence.dev";
emailInput.dispatchEvent(new Event("input", { bubbles: true }));
emailInput.dispatchEvent(new Event("change", { bubbles: true }));
const roleTrigger = [...canvasElement.querySelectorAll('[data-slot="trigger"]')].find(
(element) => element.textContent?.includes("Design")
);
if (!(roleTrigger instanceof HTMLElement)) {
throw new Error("Expected the role select trigger to render.");
}
roleTrigger.click();
await waitForCondition(
() => document.body.querySelector('[role="listbox"]') instanceof HTMLElement,
"Expected the role select content to open."
);
const legalOption = [...document.body.querySelectorAll('[role="option"]')].find((element) =>
element.textContent?.includes("Legal")
);
if (!(legalOption instanceof HTMLElement)) {
throw new Error("Expected to find the Legal option.");
}
legalOption.click();
const summaryInput = canvasElement.querySelector("textarea");
if (!(summaryInput instanceof HTMLTextAreaElement)) {
throw new Error("Expected the launch summary textarea to render.");
}
summaryInput.value = "This release coordinates approvals, copy, and rollout risks.";
summaryInput.dispatchEvent(new Event("input", { bubbles: true }));
summaryInput.dispatchEvent(new Event("change", { bubbles: true }));
const submitButton = [...canvasElement.querySelectorAll("button")].find((element) =>
element.textContent?.includes("Save settings")
);
if (!(submitButton instanceof HTMLButtonElement)) {
throw new Error("Expected the form submit button to render.");
}
submitButton.click();
await waitForCondition(
() => canvasElement.textContent?.includes('"email": "team@cadence.dev"') ?? false,
"Expected the submitted payload preview to update."
);
}
};