Files
web/packages/design-system/lib/components/Input/Input.stories.tsx
Rasmus Langvad b966cf1d53 Merged in fix/3693-group-inputs-storybook (pull request #3402)
fix(SW-3693): Refactor Input stories to use FormInput component and showcase all controls

* Refactor Input stories to use FormInput component and showcase all controls

* Updated stories and added autocomplete prop to PasswordInput

* Merge branch 'master' into fix/3693-group-inputs-storybook

* Use FormTextArea in stories for TextArea to show description and error texts

* Merged master into fix/3693-group-inputs-storybook

* Merge branch 'master' into fix/3693-group-inputs-storybook

* Removed redundant font name and fixed broken icons in stories

* Merge branch 'fix/3693-group-inputs-storybook' of bitbucket.org:scandic-swap/web into fix/3693-group-inputs-storybook

* Merged master into fix/3693-group-inputs-storybook

* Merge branch 'master' into fix/3693-group-inputs-storybook


Approved-by: Bianca Widstam
2026-01-21 16:20:04 +00:00

675 lines
18 KiB
TypeScript

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 (
<FormProvider {...methods}>
<div style={{ padding: "1rem" }}>
<FormInput
name="input"
label={label}
labelPosition={labelPosition}
placeholder={placeholder}
description={description}
descriptionIcon={descriptionIcon}
disabled={disabled}
readOnly={readOnly}
required={required}
registerOptions={required ? { required: true } : undefined}
showClearContentIcon={showClearContentIcon}
leftIcon={
showLeftIcon && leftIconName ? (
<MaterialIcon icon={leftIconName as MaterialIconName} />
) : undefined
}
rightIcon={
showRightIcon && rightIconName ? (
<MaterialIcon icon={rightIconName as MaterialIconName} />
) : undefined
}
validationState={validationState}
autoComplete={autoComplete}
/>
</div>
</FormProvider>
)
}
const meta: Meta<typeof FormInputComponent> = {
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<typeof FormInputComponent>
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<typeof showcaseSchema>
function InputShowcase() {
const methods = useForm<ShowcaseFormData>({
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 (
<FormProvider {...methods}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "3rem",
maxWidth: "800px",
padding: "2rem",
}}
>
{/* Basic States */}
<section>
<Typography variant="Title/sm">
<h2>Basic States</h2>
</Typography>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput name="default" label="Default" />
<FormInput
name="placeholder"
label="Placeholder"
placeholder="Enter text here..."
/>
<FormInput
name="default2"
label="Default"
description="This is a default input with a description"
/>
<FormInput
name="default3"
label="Default"
placeholder="Enter text here..."
description="This is a default input with a description"
/>
<FormInput name="filled" label="Filled" />
<FormInput
name="required"
label="Required"
registerOptions={{ required: true }}
hideError
/>
<FormInput name="disabled" label="Disabled" disabled />
<FormInput name="disabledFilled" label="Disabled Filled" disabled />
<FormInput
name="warningState"
label="Warning State"
validationState="warning"
/>
<FormInput
name="warningFilled"
label="Warning with Value"
validationState="warning"
/>
</div>
</section>
{/* With Icons */}
<section>
<Typography variant="Title/sm">
<h2>With Icons</h2>
</Typography>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput
name="emailIcon"
label="Email"
type="email"
leftIcon={<MaterialIcon icon="mail" />}
/>
<FormInput
name="searchIcon"
label="Search"
leftIcon={<MaterialIcon icon="search" />}
/>
<FormInput
name="locked"
label="Locked icon"
rightIcon={<MaterialIcon icon="lock" />}
/>
<FormInput
name="bothIcons"
label="With Both Icons"
leftIcon={<MaterialIcon icon="person" />}
rightIcon={<MaterialIcon icon="check_circle" />}
/>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput
name="emailIconTop"
label="Email"
type="email"
leftIcon={<MaterialIcon icon="mail" />}
labelPosition="top"
/>
<FormInput
name="searchIconTop"
label="Search"
leftIcon={<MaterialIcon icon="search" />}
labelPosition="top"
/>
<FormInput
name="lockedTop"
label="Locked icon"
rightIcon={<MaterialIcon icon="lock" />}
labelPosition="top"
/>
<FormInput
name="bothIconsTop"
label="With Both Icons"
leftIcon={<MaterialIcon icon="person" />}
rightIcon={<MaterialIcon icon="check_circle" />}
labelPosition="top"
/>
</div>
</section>
{/* Clear Button */}
<section>
<Typography variant="Title/sm">
<h2>Clear Button</h2>
</Typography>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput name="emptyClear" label="Empty" showClearContentIcon />
<FormInput
name="emailClear"
label="Email with Clear"
type="email"
showClearContentIcon
/>
<FormInput
name="clearLeftRight"
label="Clear with Left Icon"
leftIcon={<MaterialIcon icon="mail" />}
showClearContentIcon
/>
<FormInput
name="clearRightIcon"
label="Clear with Right Icon"
rightIcon={<MaterialIcon icon="lock" />}
showClearContentIcon
/>
<FormInput
name="clearBothIcons"
label="Clear with Both Icons"
leftIcon={<MaterialIcon icon="mail" />}
rightIcon={<MaterialIcon icon="lock" />}
showClearContentIcon
/>
</div>
</section>
{/* Validation States */}
<section>
<Typography variant="Title/sm">
<h2>Validation States</h2>
</Typography>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput
name="error"
label="Error State"
registerOptions={{ minLength: 10 }}
/>
<FormInput
name="errorFilled"
label="Error with Value"
registerOptions={{ required: true }}
/>
<FormInput
name="warning"
label="Warning State"
validationState="warning"
/>
<FormInput
name="warningFilledValidation"
label="Warning with Value"
validationState="warning"
/>
</div>
</section>
{/* Input Types */}
<section>
<Typography variant="Title/sm">
<h2>Input Types</h2>
</Typography>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.5rem",
marginTop: "1rem",
}}
>
<FormInput name="text" label="Text" type="text" />
<FormInput name="emailType" label="Email" type="email" />
<FormInput name="number" label="Number" type="number" />
</div>
</section>
</div>
</FormProvider>
)
}
export const AllVariations: StoryObj<typeof InputShowcase> = {
render: () => <InputShowcase />,
parameters: {
layout: "fullscreen",
},
}