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