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
675 lines
18 KiB
TypeScript
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",
|
|
},
|
|
}
|