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" import { z } from "zod" import type { IntlShape } from "react-intl" import { PasswordInput } from "./index" 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 ?? "" // ============================================================================ // 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 interface PasswordInputProps { onSubmit?: (data: PasswordFormData) => void showErrors?: boolean defaultNewPassword?: string } function PasswordInputComponent({ onSubmit, showErrors = false, defaultNewPassword = "", }: PasswordInputProps) { const methods = useForm({ resolver: zodResolver(passwordFormSchema), defaultValues: { currentPassword: "", newPassword: defaultNewPassword, confirmPassword: "", }, mode: "all", criteriaMode: "all", reValidateMode: "onChange", }) // Trigger validation on mount if showErrors is true useEffect(() => { if (showErrors) { methods.trigger() } }, [showErrors, methods]) const handleSubmit = methods.handleSubmit((data) => { onSubmit?.(data) }) return (

Change Password

) } const passwordFormMeta: Meta = { title: "Core Components/PasswordInput", component: PasswordInputComponent, parameters: { layout: "padded", }, argTypes: { showErrors: { control: "boolean", description: "Show validation errors on mount", }, defaultNewPassword: { control: "text", description: "Default value for new password field", }, }, } export default passwordFormMeta type PasswordFormStory = StoryObj export const Default: PasswordFormStory = { render: (args) => , args: { onSubmit: fn(), showErrors: false, defaultNewPassword: "", }, } // ============================================================================ // Password Validation Showcase // ============================================================================ const showcasePasswordSchema = z.object({ empty: z.literal("").optional().or(passwordValidator()), weak: passwordValidator(), partial: passwordValidator(), valid: passwordValidator(), tooLong: passwordValidator(), }) type ShowcasePasswordFormData = z.infer function PasswordValidationShowcase() { const methods = useForm({ resolver: zodResolver(showcasePasswordSchema), defaultValues: { empty: "", weak: "weak", partial: "Password1", valid: "ValidPassword123!", tooLong: "A".repeat(41) + "1!", }, mode: "all", criteriaMode: "all", reValidateMode: "onChange", }) // Trigger validation on mount to show validation states useEffect(() => { methods.trigger(["weak", "partial", "valid", "tooLong"]) }, [methods]) return (

New Password Validation States

Empty Password

Weak Password (too short)

Partial Password (missing requirements)

Valid Password

Too Long Password

) } const showcaseMeta: Meta = { title: "Core Components/PasswordInput", component: PasswordValidationShowcase, parameters: { layout: "fullscreen", }, } type ShowcaseStory = StoryObj export const AllValidationStates: ShowcaseStory = { render: () => , parameters: { ...showcaseMeta.parameters, }, }