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:
Rasmus Langvad
2026-01-21 16:20:04 +00:00
parent 993f6c4377
commit b966cf1d53
9 changed files with 1219 additions and 673 deletions

View File

@@ -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,
},
}

View File

@@ -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} />,
}