import type { Meta, StoryObj } from "@storybook/nextjs-vite" import { zodResolver } from "@hookform/resolvers/zod" import { useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" import { z } from "zod" import { expect } from "storybook/test" import { FormInput } from "../Form/FormInput" import { MaterialIcon } from "../Icons/MaterialIcon" import type { MaterialIconProps } from "../Icons/MaterialIcon" import { Typography } from "../Typography" import { MaterialIconName } from "../Icons/MaterialIcon/generated" interface FormInputStoryProps { label?: string labelPosition?: "floating" | "top" placeholder?: string description?: string descriptionIcon?: MaterialIconProps["icon"] required?: boolean disabled?: boolean readOnly?: boolean showClearContentIcon?: boolean showLeftIcon?: boolean showRightIcon?: boolean leftIconName?: string rightIconName?: string showWarning?: boolean errorMessage?: string defaultValue?: string autoComplete?: string } function FormInputComponent({ label = "Label", labelPosition = "floating", placeholder, description, descriptionIcon = "info", required = false, disabled = false, readOnly = false, showClearContentIcon = false, showLeftIcon = false, showRightIcon = false, leftIconName = "person", rightIconName = "lock", showWarning = false, errorMessage, defaultValue = "", autoComplete, }: FormInputStoryProps) { const schema = z.object({ input: errorMessage ? z.string().min(1, errorMessage) : required ? z.string().min(1, "This field is required") : z.string().optional(), }) const methods = useForm<{ input: string }>({ resolver: zodResolver(schema), defaultValues: { input: defaultValue, }, mode: "onChange", }) useEffect(() => { if (errorMessage) { methods.setError("input", { type: "manual", message: errorMessage, }) methods.trigger("input") } else { methods.clearErrors("input") } }, [errorMessage, methods]) const validationState = showWarning ? "warning" : undefined return (
) : undefined } rightIcon={ showRightIcon && rightIconName ? ( ) : undefined } validationState={validationState} autoComplete={autoComplete} />
) } const meta: Meta = { title: "Core Components/Input/TextInput", component: FormInputComponent, argTypes: { label: { control: "text", name: "Label", description: "Label text", table: { type: { summary: "string" }, order: 1, }, }, labelPosition: { control: "select", name: "Label Position", options: ["floating", "top"], description: "Label position", table: { type: { summary: "'floating' | 'top'" }, defaultValue: { summary: "'floating'" }, order: 2, }, }, placeholder: { control: "text", name: "Placeholder", description: "Placeholder text", table: { type: { summary: "string" }, defaultValue: { summary: "undefined" }, order: 3, }, }, description: { control: "text", name: "Description", description: "Helper text (hidden when error is shown)", table: { type: { summary: "string" }, defaultValue: { summary: "undefined" }, order: 4, }, }, descriptionIcon: { control: "select", name: "Description Icon", options: ["info", "warning", "error"], description: "Description icon", table: { type: { summary: "string" }, defaultValue: { summary: "'info'" }, order: 5, }, }, required: { control: "boolean", name: "Required", description: "Required field", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 6, }, }, disabled: { control: "boolean", name: "Disabled", description: "Disabled state", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 7, }, }, readOnly: { control: "boolean", name: "Read Only", description: "Read-only state", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 8, }, }, showClearContentIcon: { control: "boolean", name: "Show Clear Icon", description: "Show clear button", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 9, }, }, showLeftIcon: { control: "boolean", name: "Show Left Icon", description: "Show left icon", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 10, }, }, leftIconName: { control: "select", name: "Left Icon", options: [ "calendar_month", "credit_card", "mail", "location_on", "info", "lock", "phone", "search", "sell", "visibility", "visibility_off", ], description: "Left icon name", table: { type: { summary: "string" }, defaultValue: { summary: "'person'" }, order: 11, }, }, showRightIcon: { control: "boolean", name: "Show Right Icon", description: "Show right icon", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 12, }, }, rightIconName: { control: "select", name: "Right Icon", options: [ "calendar_month", "credit_card", "mail", "location_on", "info", "lock", "phone", "search", "sell", "visibility", "visibility_off", ], description: "Right icon name", table: { type: { summary: "string" }, defaultValue: { summary: "'lock'" }, order: 13, }, }, showWarning: { control: "boolean", name: "Show Warning", description: "Warning state", table: { type: { summary: "boolean" }, defaultValue: { summary: "false" }, order: 14, }, }, errorMessage: { control: "text", name: "Error Message", description: "Error message", table: { type: { summary: "string" }, defaultValue: { summary: "undefined" }, order: 15, }, }, autoComplete: { control: "select", name: "Autocomplete", description: "HTML autocomplete attribute value", options: [ undefined, "name", "given-name", "family-name", "mail", "username", "tel", "tel-national", "organization", "address-line1", "address-line2", "city", "postal-code", "country", "off", ], table: { type: { summary: "string" }, defaultValue: { summary: "undefined" }, order: 16, }, }, }, } export default meta type Story = StoryObj export const Default: Story = { args: { label: "Label", labelPosition: "floating", placeholder: undefined, description: undefined, descriptionIcon: "info", required: false, disabled: false, readOnly: false, showClearContentIcon: false, showLeftIcon: false, showRightIcon: false, leftIconName: "person", rightIconName: "lock", showWarning: false, errorMessage: undefined, autoComplete: undefined, }, play: async ({ canvas, userEvent }) => { const textbox = canvas.getByRole("textbox") expect(textbox).not.toBeDisabled() expect(textbox).toHaveValue("") await userEvent.type(textbox, "Hello World") expect(textbox).toHaveValue("Hello World") await userEvent.clear(textbox) expect(textbox).toHaveValue("") }, } // ============================================================================ // Input Variations Showcase // ============================================================================ const showcaseSchema = z.object({ default: z.string().optional(), placeholder: z.string().optional(), filled: z.string().optional(), required: z.string().min(1, "This field is required"), disabled: z.string().optional(), disabledFilled: z.string().optional(), warningState: z.string().optional(), warningFilled: z.string().optional(), emailIcon: z.string().optional(), searchIcon: z.string().optional(), locked: z.string().optional(), bothIcons: z.string().optional(), emailIconTop: z.string().optional(), searchIconTop: z.string().optional(), lockedTop: z.string().optional(), bothIconsTop: z.string().optional(), emailClear: z.string().optional(), searchClear: z.string().optional(), clearLeftRight: z.string().optional(), emptyClear: z.string().optional(), error: z.string().min(10, "Must be at least 10 characters"), errorFilled: z.string().email("Invalid email"), warning: z.string().optional(), warningFilledValidation: z.string().optional(), text: z.string().optional(), emailType: z.string().optional(), telType: z.string().optional(), number: z.string().optional(), passwordType: z.string().optional(), urlType: z.string().optional(), combined1: z.string().optional(), combined2: z.string().email("Invalid email"), combined3: z.string().optional(), }) type ShowcaseFormData = z.infer function InputShowcase() { const methods = useForm({ resolver: zodResolver(showcaseSchema), defaultValues: { filled: "Sample text", disabledFilled: "Cannot edit", warningFilled: "Needs attention", emailClear: "user@example.com", searchClear: "", clearLeftRight: "user@example.com", error: "Short", errorFilled: "Invalid input", warningFilledValidation: "Needs attention", combined1: "user@example.com", combined2: "Invalid email", }, mode: "onChange", }) // Trigger validation for error examples on mount useEffect(() => { methods.trigger(["error", "errorFilled", "combined2"]) }, [methods]) return (
{/* Basic States */}

Basic States

{/* With Icons */}

With Icons

} /> } /> } /> } rightIcon={} />
} labelPosition="top" /> } labelPosition="top" /> } labelPosition="top" /> } rightIcon={} labelPosition="top" />
{/* Clear Button */}

Clear Button

} showClearContentIcon /> } showClearContentIcon /> } rightIcon={} showClearContentIcon />
{/* Validation States */}

Validation States

{/* Input Types */}

Input Types

) } export const AllVariations: StoryObj = { render: () => , parameters: { layout: "fullscreen", }, }