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