import type { Meta, StoryObj } from "@storybook/nextjs-vite" 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 { Typography } from "../Typography" import type { MaterialIconProps } from "../Icons/MaterialIcon" import { passwordValidator } from "@scandic-hotels/common/utils/zod/passwordValidator" // Simple error formatter for Storybook const defaultErrorFormatter = ( _intl: IntlShape, errorMessage?: string ): string => errorMessage ?? "" 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({ 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: { password: defaultValue, }, mode: isNewPassword ? "all" : "onChange", criteriaMode: isNewPassword ? "all" : undefined, reValidateMode: isNewPassword ? "onChange" : undefined, }) useEffect(() => { if (errorMessage && !isNewPassword) { methods.setError("password", { type: "manual", message: errorMessage, }) methods.trigger("password") } else if (isNewPassword && defaultValue) { methods.trigger("password") } else { methods.clearErrors("password") } }, [errorMessage, methods, isNewPassword, defaultValue]) return (
) } const meta: Meta = { title: "Core Components/Input/PasswordInput", component: PasswordInputComponent, argTypes: { label: { control: "text", 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 meta type Story = StoryObj export const Default: Story = { args: { label: undefined, placeholder: undefined, disabled: false, visibilityToggleable: true, isNewPassword: false, required: false, errorMessage: undefined, defaultValue: "", description: undefined, descriptionIcon: "info", autoComplete: undefined, }, } // ============================================================================ // 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 (

Password Validation States

Empty Password

Valid Password

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