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
This commit is contained in:
@@ -6,7 +6,6 @@ import { fn } from "storybook/test"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Button } from "../../Button"
|
||||
import { MaterialIcon } from "../../Icons/MaterialIcon"
|
||||
import { Typography } from "../../Typography"
|
||||
import { FormInput } from "../FormInput"
|
||||
|
||||
@@ -376,333 +375,3 @@ export const WithErrors: SignupStory = {
|
||||
...signupMeta.parameters,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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: "+46 70 123 45 67",
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<Typography variant="Title/lg">
|
||||
<h1>FormInput Component Showcase</h1>
|
||||
</Typography>
|
||||
|
||||
{/* Basic States */}
|
||||
<section>
|
||||
<Typography variant="Title/md">
|
||||
<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/md">
|
||||
<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/md">
|
||||
<h2>Clear Button</h2>
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||
gap: "1.5rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
name="emailClear"
|
||||
label="Email with Clear"
|
||||
type="email"
|
||||
showClearContentIcon
|
||||
/>
|
||||
<FormInput
|
||||
name="searchClear"
|
||||
label="Search with Clear"
|
||||
leftIcon={<MaterialIcon icon="search" />}
|
||||
showClearContentIcon
|
||||
/>
|
||||
<FormInput
|
||||
name="clearLeftRight"
|
||||
label="Clear with Left Icon"
|
||||
leftIcon={<MaterialIcon icon="call" />}
|
||||
showClearContentIcon
|
||||
/>
|
||||
<FormInput
|
||||
name="clearRightIcon"
|
||||
label="Clear with Right Icon"
|
||||
rightIcon={<MaterialIcon icon="lock" />}
|
||||
showClearContentIcon
|
||||
/>
|
||||
<FormInput
|
||||
name="clearBothIcons"
|
||||
label="Clear with Both Icon"
|
||||
leftIcon={<MaterialIcon icon="mail" />}
|
||||
rightIcon={<MaterialIcon icon="lock" />}
|
||||
showClearContentIcon
|
||||
/>
|
||||
<FormInput name="emptyClear" label="Empty" showClearContentIcon />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Validation States */}
|
||||
<section>
|
||||
<Typography variant="Title/md">
|
||||
<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/md">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
const showcaseMeta: Meta<typeof InputShowcase> = {
|
||||
title: "Compositions/Form/InputShowcase",
|
||||
component: InputShowcase,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
type ShowcaseStory = StoryObj<typeof InputShowcase>
|
||||
|
||||
export const AllVariations: ShowcaseStory = {
|
||||
render: () => <InputShowcase />,
|
||||
parameters: {
|
||||
...showcaseMeta.parameters,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import { PasswordInput } from "../../PasswordInput"
|
||||
import { Button } from "../../Button"
|
||||
import { Typography } from "../../Typography"
|
||||
import { passwordValidator } from "@scandic-hotels/common/utils/zod/passwordValidator"
|
||||
|
||||
// Simple error formatter for Storybook
|
||||
const defaultErrorFormatter = (
|
||||
_intl: IntlShape,
|
||||
errorMessage?: string
|
||||
): string => errorMessage ?? ""
|
||||
|
||||
const passwordFormSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().optional(),
|
||||
newPassword: z.literal("").optional().or(passwordValidator()),
|
||||
confirmPassword: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword && data.newPassword !== data.confirmPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type PasswordFormData = z.infer<typeof passwordFormSchema>
|
||||
|
||||
interface PasswordFormProps {
|
||||
onSubmit?: (data: PasswordFormData) => void
|
||||
}
|
||||
|
||||
function PasswordFormComponent({ onSubmit }: PasswordFormProps) {
|
||||
const methods = useForm<PasswordFormData>({
|
||||
resolver: zodResolver(passwordFormSchema),
|
||||
defaultValues: {
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
mode: "all",
|
||||
criteriaMode: "all",
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
const handleSubmit = methods.handleSubmit((data) => {
|
||||
onSubmit?.(data)
|
||||
})
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1.5rem",
|
||||
maxWidth: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="Title/md">
|
||||
<h2>Change Password</h2>
|
||||
</Typography>
|
||||
|
||||
<PasswordInput
|
||||
name="currentPassword"
|
||||
label="Current password"
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
name="newPassword"
|
||||
isNewPassword
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
|
||||
<Button type="submit" variant="Primary" size="lg">
|
||||
Update password
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof PasswordFormComponent> = {
|
||||
title: "Compositions/Form/PasswordForm",
|
||||
component: PasswordFormComponent,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof PasswordFormComponent>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => <PasswordFormComponent {...args} />,
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
gap: var(--Space-x05);
|
||||
margin-top: var(--Space-x1);
|
||||
font-size: var(--Body-Supporting-text-Size);
|
||||
font-family: var(--Body-Supporting-text-Font-family, "Fira Sans");
|
||||
font-family: var(--Body-Supporting-text-Font-family);
|
||||
font-style: normal;
|
||||
font-weight: var(--Body-Supporting-text-Font-weight);
|
||||
letter-spacing: var(--Body-Supporting-text-Letter-spacing);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
gap: var(--Space-x05);
|
||||
margin-top: var(--Space-x1);
|
||||
font-size: var(--Body-Supporting-text-Size);
|
||||
font-family: var(--Body-Supporting-text-Font-family, "Fira Sans");
|
||||
font-family: var(--Body-Supporting-text-Font-family);
|
||||
font-style: normal;
|
||||
font-weight: var(--Body-Supporting-text-Font-weight);
|
||||
letter-spacing: var(--Body-Supporting-text-Letter-spacing);
|
||||
|
||||
@@ -1,179 +1,101 @@
|
||||
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 { Input } from "./Input"
|
||||
import { TextField } from "react-aria-components"
|
||||
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"
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: "Core Components/Input",
|
||||
// @ts-expect-error Input does not support this, but wrapping <TextField> does
|
||||
component: ({ isInvalid, validationState, ...props }) => (
|
||||
<TextField isInvalid={isInvalid} data-validation-state={validationState}>
|
||||
<Input {...props} data-validation-state={validationState} />
|
||||
</TextField>
|
||||
),
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "The label text displayed for the input field",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
labelPosition: {
|
||||
control: "select",
|
||||
options: ["floating", "top"],
|
||||
description: "Position of the label relative to the input",
|
||||
table: {
|
||||
type: { summary: "'floating' | 'top'" },
|
||||
defaultValue: { summary: "'floating'" },
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text shown when input is empty",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
},
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Whether the input is disabled",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showClearContentIcon: {
|
||||
control: "boolean",
|
||||
description: "Whether the clear content icon is shown",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showLeftIcon: {
|
||||
control: "boolean",
|
||||
description: "Whether to show a left icon",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showRightIcon: {
|
||||
control: "boolean",
|
||||
description: "Whether to show a right icon",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
leftIconName: {
|
||||
control: "select",
|
||||
options: [
|
||||
"calendar_month",
|
||||
"credit_card",
|
||||
"email",
|
||||
"info_circle",
|
||||
"location_on",
|
||||
"lock",
|
||||
"phone",
|
||||
"search",
|
||||
"sell",
|
||||
"visibility",
|
||||
"visibility_off",
|
||||
],
|
||||
description: "Icon name for the left icon",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "'person'" },
|
||||
},
|
||||
},
|
||||
rightIconName: {
|
||||
control: "select",
|
||||
options: [
|
||||
"calendar_month",
|
||||
"credit_card",
|
||||
"email",
|
||||
"info_circle",
|
||||
"location_on",
|
||||
"lock",
|
||||
"phone",
|
||||
"search",
|
||||
"sell",
|
||||
"visibility",
|
||||
"visibility_off",
|
||||
],
|
||||
description: "Icon name for the right icon",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "'lock'" },
|
||||
},
|
||||
},
|
||||
showWarning: {
|
||||
control: "boolean",
|
||||
description: "Whether to show warning validation state",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
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
|
||||
}
|
||||
|
||||
export default meta
|
||||
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(),
|
||||
})
|
||||
|
||||
type Story = StoryObj<typeof Input>
|
||||
const methods = useForm<{ input: string }>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
input: defaultValue,
|
||||
},
|
||||
mode: "onChange",
|
||||
})
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: "Label",
|
||||
name: "foo",
|
||||
required: false,
|
||||
showLeftIcon: false,
|
||||
showRightIcon: false,
|
||||
leftIconName: "person",
|
||||
rightIconName: "lock",
|
||||
showWarning: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
render: (args) => {
|
||||
// Extract custom Storybook args
|
||||
const {
|
||||
showLeftIcon,
|
||||
showRightIcon,
|
||||
leftIconName,
|
||||
rightIconName,
|
||||
showWarning,
|
||||
...inputProps
|
||||
} = args as typeof args & {
|
||||
showLeftIcon?: boolean
|
||||
showRightIcon?: boolean
|
||||
leftIconName?: string
|
||||
rightIconName?: string
|
||||
showWarning?: boolean
|
||||
useEffect(() => {
|
||||
if (errorMessage) {
|
||||
methods.setError("input", {
|
||||
type: "manual",
|
||||
message: errorMessage,
|
||||
})
|
||||
methods.trigger("input")
|
||||
} else {
|
||||
methods.clearErrors("input")
|
||||
}
|
||||
}, [errorMessage, methods])
|
||||
|
||||
const validationState = showWarning ? "warning" : undefined
|
||||
const validationState = showWarning ? "warning" : undefined
|
||||
|
||||
return (
|
||||
<TextField data-validation-state={validationState}>
|
||||
<Input
|
||||
{...inputProps}
|
||||
data-validation-state={validationState}
|
||||
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} />
|
||||
@@ -184,9 +106,247 @@ export const Default: Story = {
|
||||
<MaterialIcon icon={rightIconName as MaterialIconName} />
|
||||
) : undefined
|
||||
}
|
||||
validationState={validationState}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
</TextField>
|
||||
)
|
||||
</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")
|
||||
@@ -201,3 +361,314 @@ export const Default: Story = {
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
import { fn } from "storybook/test"
|
||||
import { useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
@@ -7,8 +6,8 @@ import { z } from "zod"
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
import { PasswordInput } from "./index"
|
||||
import { Button } from "../Button"
|
||||
import { Typography } from "../Typography"
|
||||
import type { MaterialIconProps } from "../Icons/MaterialIcon"
|
||||
import { passwordValidator } from "@scandic-hotels/common/utils/zod/passwordValidator"
|
||||
|
||||
// Simple error formatter for Storybook
|
||||
@@ -17,131 +16,230 @@ const defaultErrorFormatter = (
|
||||
errorMessage?: string
|
||||
): string => errorMessage ?? ""
|
||||
|
||||
// ============================================================================
|
||||
// Password Form with New Password Validation
|
||||
// ============================================================================
|
||||
|
||||
const passwordFormSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().optional(),
|
||||
newPassword: z.literal("").optional().or(passwordValidator()),
|
||||
confirmPassword: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.newPassword && data.newPassword !== data.confirmPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type PasswordFormData = z.infer<typeof passwordFormSchema>
|
||||
|
||||
interface PasswordInputProps {
|
||||
onSubmit?: (data: PasswordFormData) => void
|
||||
showErrors?: boolean
|
||||
defaultNewPassword?: string
|
||||
interface PasswordInputStoryProps {
|
||||
label?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
visibilityToggleable?: boolean
|
||||
isNewPassword?: boolean
|
||||
required?: boolean
|
||||
errorMessage?: string
|
||||
defaultValue?: string
|
||||
description?: string
|
||||
descriptionIcon?: MaterialIconProps["icon"]
|
||||
autoComplete?: string
|
||||
}
|
||||
|
||||
function PasswordInputComponent({
|
||||
onSubmit,
|
||||
showErrors = false,
|
||||
defaultNewPassword = "",
|
||||
}: PasswordInputProps) {
|
||||
const methods = useForm<PasswordFormData>({
|
||||
resolver: zodResolver(passwordFormSchema),
|
||||
label,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
visibilityToggleable = true,
|
||||
isNewPassword = false,
|
||||
required = false,
|
||||
errorMessage,
|
||||
defaultValue = "",
|
||||
description,
|
||||
descriptionIcon = "info",
|
||||
autoComplete,
|
||||
}: PasswordInputStoryProps) {
|
||||
const schema = z.object({
|
||||
password: errorMessage
|
||||
? isNewPassword
|
||||
? z.literal("").optional().or(passwordValidator())
|
||||
: z.string().min(1, errorMessage)
|
||||
: required
|
||||
? isNewPassword
|
||||
? passwordValidator()
|
||||
: z.string().min(1, "This field is required")
|
||||
: isNewPassword
|
||||
? z.literal("").optional().or(passwordValidator())
|
||||
: z.string().optional(),
|
||||
})
|
||||
|
||||
const methods = useForm<{ password: string }>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
currentPassword: "",
|
||||
newPassword: defaultNewPassword,
|
||||
confirmPassword: "",
|
||||
password: defaultValue,
|
||||
},
|
||||
mode: "all",
|
||||
criteriaMode: "all",
|
||||
reValidateMode: "onChange",
|
||||
mode: isNewPassword ? "all" : "onChange",
|
||||
criteriaMode: isNewPassword ? "all" : undefined,
|
||||
reValidateMode: isNewPassword ? "onChange" : undefined,
|
||||
})
|
||||
|
||||
// Trigger validation on mount if showErrors is true
|
||||
useEffect(() => {
|
||||
if (showErrors) {
|
||||
methods.trigger()
|
||||
if (errorMessage && !isNewPassword) {
|
||||
methods.setError("password", {
|
||||
type: "manual",
|
||||
message: errorMessage,
|
||||
})
|
||||
methods.trigger("password")
|
||||
} else if (isNewPassword && defaultValue) {
|
||||
methods.trigger("password")
|
||||
} else {
|
||||
methods.clearErrors("password")
|
||||
}
|
||||
}, [showErrors, methods])
|
||||
|
||||
const handleSubmit = methods.handleSubmit((data) => {
|
||||
onSubmit?.(data)
|
||||
})
|
||||
}, [errorMessage, methods, isNewPassword, defaultValue])
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1.5rem",
|
||||
maxWidth: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="Title/md">
|
||||
<h2>Change Password</h2>
|
||||
</Typography>
|
||||
|
||||
<div style={{ padding: "1rem" }}>
|
||||
<PasswordInput
|
||||
name="currentPassword"
|
||||
label="Current password"
|
||||
name="password"
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
visibilityToggleable={visibilityToggleable}
|
||||
isNewPassword={isNewPassword}
|
||||
registerOptions={required ? { required: true } : undefined}
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
description={description}
|
||||
descriptionIcon={descriptionIcon}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
name="newPassword"
|
||||
isNewPassword
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
|
||||
<Button type="submit" variant="Primary" size="lg">
|
||||
Update password
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const passwordFormMeta: Meta<typeof PasswordInputComponent> = {
|
||||
title: "Core Components/PasswordInput",
|
||||
const meta: Meta<typeof PasswordInputComponent> = {
|
||||
title: "Core Components/Input/PasswordInput",
|
||||
component: PasswordInputComponent,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
argTypes: {
|
||||
showErrors: {
|
||||
control: "boolean",
|
||||
description: "Show validation errors on mount",
|
||||
},
|
||||
defaultNewPassword: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "Default value for new password field",
|
||||
name: "Label",
|
||||
description: "Label text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
name: "Placeholder",
|
||||
description: "Placeholder text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
name: "Disabled",
|
||||
description: "Disabled state",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
visibilityToggleable: {
|
||||
control: "boolean",
|
||||
name: "Visibility Toggleable",
|
||||
description: "Show/hide password toggle button",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "true" },
|
||||
order: 4,
|
||||
},
|
||||
},
|
||||
isNewPassword: {
|
||||
control: "boolean",
|
||||
name: "Is New Password",
|
||||
description: "Enable new password validation",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
order: 5,
|
||||
},
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
name: "Required",
|
||||
description: "Required field",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
order: 6,
|
||||
},
|
||||
},
|
||||
errorMessage: {
|
||||
control: "text",
|
||||
name: "Error Message",
|
||||
description: "Error message (only for non-new-password fields)",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
order: 7,
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
control: "text",
|
||||
name: "Default Value",
|
||||
description: "Default password value",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "''" },
|
||||
order: 8,
|
||||
},
|
||||
},
|
||||
description: {
|
||||
control: "text",
|
||||
name: "Description",
|
||||
description: "Helper text (hidden when error is shown)",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
order: 9,
|
||||
},
|
||||
},
|
||||
descriptionIcon: {
|
||||
control: "select",
|
||||
name: "Description Icon",
|
||||
options: ["info", "warning", "error"],
|
||||
description: "Description icon",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "'info'" },
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
autoComplete: {
|
||||
control: "select",
|
||||
name: "Autocomplete",
|
||||
description:
|
||||
"HTML autocomplete attribute value. If not provided, automatically sets to 'new-password' when isNewPassword is true, or 'current-password' when isNewPassword is false.",
|
||||
options: [undefined, "current-password", "new-password", "off"],
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: {
|
||||
summary: "undefined (auto-set based on isNewPassword)",
|
||||
},
|
||||
order: 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default passwordFormMeta
|
||||
export default meta
|
||||
|
||||
type PasswordFormStory = StoryObj<typeof PasswordInputComponent>
|
||||
type Story = StoryObj<typeof PasswordInputComponent>
|
||||
|
||||
export const Default: PasswordFormStory = {
|
||||
render: (args) => <PasswordInputComponent {...args} />,
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
onSubmit: fn(),
|
||||
showErrors: false,
|
||||
defaultNewPassword: "",
|
||||
label: undefined,
|
||||
placeholder: undefined,
|
||||
disabled: false,
|
||||
visibilityToggleable: true,
|
||||
isNewPassword: false,
|
||||
required: false,
|
||||
errorMessage: undefined,
|
||||
defaultValue: "",
|
||||
description: undefined,
|
||||
descriptionIcon: "info",
|
||||
autoComplete: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -191,8 +289,8 @@ function PasswordValidationShowcase() {
|
||||
}}
|
||||
>
|
||||
<section>
|
||||
<Typography variant="Title/md">
|
||||
<h2>New Password Validation States</h2>
|
||||
<Typography variant="Title/sm">
|
||||
<h2>Password Validation States</h2>
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
@@ -213,28 +311,6 @@ function PasswordValidationShowcase() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>Weak Password (too short)</p>
|
||||
</Typography>
|
||||
<PasswordInput
|
||||
name="weak"
|
||||
isNewPassword
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>Partial Password (missing requirements)</p>
|
||||
</Typography>
|
||||
<PasswordInput
|
||||
name="partial"
|
||||
isNewPassword
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>Valid Password</p>
|
||||
@@ -246,6 +322,17 @@ function PasswordValidationShowcase() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>Too Short Password</p>
|
||||
</Typography>
|
||||
<PasswordInput
|
||||
name="weak"
|
||||
isNewPassword
|
||||
errorFormatter={defaultErrorFormatter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>Too Long Password</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { TextField } from "react-aria-components"
|
||||
import { Text, TextField } from "react-aria-components"
|
||||
import {
|
||||
Controller,
|
||||
type RegisterOptions,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "react-hook-form"
|
||||
import { useIntl, type IntlShape } from "react-intl"
|
||||
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||
import { MaterialIcon, type MaterialIconProps } from "../Icons/MaterialIcon"
|
||||
import { Input } from "../Input"
|
||||
import { Typography } from "../Typography"
|
||||
|
||||
@@ -28,6 +28,10 @@ interface PasswordInputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
||||
visibilityToggleable?: boolean
|
||||
isNewPassword?: boolean
|
||||
errorFormatter?: (intl: IntlShape, errorMessage?: string) => string
|
||||
/** Helper text displayed below the input (hidden when there's an error) */
|
||||
description?: string
|
||||
descriptionIcon?: MaterialIconProps["icon"]
|
||||
autoComplete?: string
|
||||
}
|
||||
|
||||
export const PasswordInput = ({
|
||||
@@ -41,6 +45,9 @@ export const PasswordInput = ({
|
||||
isNewPassword = false,
|
||||
className = "",
|
||||
errorFormatter,
|
||||
description = "",
|
||||
descriptionIcon = "info",
|
||||
autoComplete,
|
||||
}: PasswordInputProps) => {
|
||||
const { control } = useFormContext()
|
||||
const intl = useIntl()
|
||||
@@ -48,6 +55,10 @@ export const PasswordInput = ({
|
||||
|
||||
const formatErrorMessage = errorFormatter ?? defaultErrorFormatter
|
||||
|
||||
// Automatically set autocomplete based on isNewPassword if not explicitly provided
|
||||
const autocompleteValue =
|
||||
autoComplete ?? (isNewPassword ? "new-password" : "current-password")
|
||||
|
||||
return (
|
||||
<Controller
|
||||
disabled={disabled}
|
||||
@@ -75,6 +86,7 @@ export const PasswordInput = ({
|
||||
|
||||
const hasError = !!fieldState.error
|
||||
const showRequirements = isNewPassword && !!field.value
|
||||
const showDescription = description && !fieldState.error
|
||||
|
||||
return (
|
||||
<TextField
|
||||
@@ -115,6 +127,7 @@ export const PasswordInput = ({
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
autoComplete={autocompleteValue}
|
||||
/>
|
||||
{visibilityToggleable ? (
|
||||
<button
|
||||
@@ -143,6 +156,13 @@ export const PasswordInput = ({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showDescription ? (
|
||||
<Text className={styles.description} slot="description">
|
||||
<MaterialIcon icon={descriptionIcon} size={20} />
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{showRequirements ? (
|
||||
<NewPasswordValidation
|
||||
value={field.value}
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x05);
|
||||
margin-top: var(--Space-x1);
|
||||
font-size: var(--Body-Supporting-text-Size);
|
||||
font-family: var(--Body-Supporting-text-Font-family);
|
||||
font-style: normal;
|
||||
font-weight: var(--Body-Supporting-text-Font-weight);
|
||||
letter-spacing: var(--Body-Supporting-text-Letter-spacing);
|
||||
}
|
||||
|
||||
.error {
|
||||
align-items: center;
|
||||
color: var(--Text-Interactive-Error);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod"
|
||||
|
||||
import { FormTextArea } from "../Form/FormTextArea"
|
||||
import type { MaterialIconProps } from "../Icons/MaterialIcon"
|
||||
import { Typography } from "../Typography"
|
||||
|
||||
interface FormTextAreaStoryProps {
|
||||
label?: string
|
||||
@@ -90,87 +91,107 @@ function FormTextAreaComponent({
|
||||
}
|
||||
|
||||
const meta: Meta<typeof FormTextAreaComponent> = {
|
||||
title: "Core Components/TextArea",
|
||||
title: "Core Components/Input/TextArea",
|
||||
component: FormTextAreaComponent,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "text",
|
||||
description: "The label text displayed above the textarea field",
|
||||
name: "Label",
|
||||
description: "Label text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
control: "text",
|
||||
description: "Placeholder text shown when textarea is empty",
|
||||
name: "Placeholder",
|
||||
description: "Placeholder text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
},
|
||||
},
|
||||
description: {
|
||||
control: "text",
|
||||
description: "Supporting text displayed below the textarea",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
},
|
||||
},
|
||||
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
description: "Whether the textarea is disabled",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
readOnly: {
|
||||
control: "boolean",
|
||||
description: "Whether the textarea is read-only",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
required: {
|
||||
control: "boolean",
|
||||
description: "Whether the textarea is required",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showLabel: {
|
||||
control: "boolean",
|
||||
description: "Whether to show the label",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "true" },
|
||||
order: 2,
|
||||
},
|
||||
},
|
||||
showDescription: {
|
||||
control: "boolean",
|
||||
description: "Whether to show the description/supporting text",
|
||||
name: "Show Description",
|
||||
description: "Show description",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "true" },
|
||||
order: 3,
|
||||
},
|
||||
},
|
||||
defaultValue: {
|
||||
description: {
|
||||
control: "text",
|
||||
description: "Default value when filled is true",
|
||||
name: "Description",
|
||||
description: "Helper text (hidden when error is shown)",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "" },
|
||||
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,
|
||||
},
|
||||
},
|
||||
showLabel: {
|
||||
control: "boolean",
|
||||
name: "Show Label",
|
||||
description: "Show label",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "true" },
|
||||
order: 9,
|
||||
},
|
||||
},
|
||||
errorMessage: {
|
||||
control: "text",
|
||||
description: "Error message to display (triggers error state)",
|
||||
name: "Error Message",
|
||||
description: "Error message",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
order: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -184,13 +205,167 @@ export const Default: Story = {
|
||||
args: {
|
||||
label: "Label",
|
||||
placeholder: "Placeholder",
|
||||
description: "Supporting text",
|
||||
required: false,
|
||||
disabled: false,
|
||||
readOnly: false,
|
||||
required: false,
|
||||
showLabel: true,
|
||||
showDescription: true,
|
||||
defaultValue: "",
|
||||
errorMessage: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TextArea Variations Showcase
|
||||
// ============================================================================
|
||||
|
||||
const showcaseSchema = z.object({
|
||||
default: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
withDescription: z.string().optional(),
|
||||
withDescriptionPlaceholder: z.string().optional(),
|
||||
filled: z.string().optional(),
|
||||
required: z.string().min(1, "This field is required"),
|
||||
disabled: z.string().optional(),
|
||||
disabledFilled: z.string().optional(),
|
||||
error: z.string().min(10, "Must be at least 10 characters"),
|
||||
errorFilled: z.string().min(10, "Invalid content"),
|
||||
descriptionInfo: z.string().optional(),
|
||||
descriptionWarning: z.string().optional(),
|
||||
})
|
||||
|
||||
type ShowcaseFormData = z.infer<typeof showcaseSchema>
|
||||
|
||||
function TextAreaShowcase() {
|
||||
const methods = useForm<ShowcaseFormData>({
|
||||
resolver: zodResolver(showcaseSchema),
|
||||
defaultValues: {
|
||||
filled:
|
||||
"This is a sample text that fills the textarea with some content.",
|
||||
disabledFilled: "This textarea is disabled and cannot be edited.",
|
||||
error: "Short",
|
||||
errorFilled: "Invalid content",
|
||||
},
|
||||
mode: "onChange",
|
||||
})
|
||||
|
||||
// Trigger validation for error examples on mount
|
||||
useEffect(() => {
|
||||
methods.trigger(["error", "errorFilled"])
|
||||
}, [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",
|
||||
}}
|
||||
>
|
||||
<FormTextArea name="default" label="Default" />
|
||||
<FormTextArea
|
||||
name="placeholder"
|
||||
label="Placeholder"
|
||||
placeholder="Enter your message here..."
|
||||
/>
|
||||
<FormTextArea
|
||||
name="withDescription"
|
||||
label="With Description"
|
||||
description="This is a default textarea with a description"
|
||||
/>
|
||||
<FormTextArea
|
||||
name="withDescriptionPlaceholder"
|
||||
label="With Description"
|
||||
placeholder="Enter your message here..."
|
||||
description="This is a default textarea with a description"
|
||||
/>
|
||||
<FormTextArea name="filled" label="Filled" />
|
||||
<FormTextArea
|
||||
name="required"
|
||||
label="Required"
|
||||
registerOptions={{ required: true }}
|
||||
hideError
|
||||
/>
|
||||
<FormTextArea name="disabled" label="Disabled" disabled />
|
||||
<FormTextArea
|
||||
name="disabledFilled"
|
||||
label="Disabled Filled"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Supporting Text Variations */}
|
||||
<section>
|
||||
<Typography variant="Title/sm">
|
||||
<h2>Supporting Text</h2>
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||
gap: "1.5rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
<FormTextArea
|
||||
name="descriptionInfo"
|
||||
label="Info Description"
|
||||
description="This is an informational message"
|
||||
descriptionIcon="info"
|
||||
/>
|
||||
<FormTextArea
|
||||
name="descriptionWarning"
|
||||
label="Warning Description"
|
||||
description="This is a warning message"
|
||||
descriptionIcon="warning"
|
||||
/>
|
||||
</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",
|
||||
}}
|
||||
>
|
||||
<FormTextArea
|
||||
name="error"
|
||||
label="Error State"
|
||||
registerOptions={{ minLength: 10 }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const AllVariations: StoryObj<typeof TextAreaShowcase> = {
|
||||
render: () => <TextAreaShowcase />,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user