fix(SW-3691): Setup one prettier config for whole repo * Setup prettierrc in root and remove other configs Approved-by: Joakim Jäderberg Approved-by: Linus Flood
282 lines
7.4 KiB
TypeScript
282 lines
7.4 KiB
TypeScript
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<typeof passwordFormSchema>
|
|
|
|
interface PasswordInputProps {
|
|
onSubmit?: (data: PasswordFormData) => void
|
|
showErrors?: boolean
|
|
defaultNewPassword?: string
|
|
}
|
|
|
|
function PasswordInputComponent({
|
|
onSubmit,
|
|
showErrors = false,
|
|
defaultNewPassword = "",
|
|
}: PasswordInputProps) {
|
|
const methods = useForm<PasswordFormData>({
|
|
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 (
|
|
<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 passwordFormMeta: Meta<typeof PasswordInputComponent> = {
|
|
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<typeof PasswordInputComponent>
|
|
|
|
export const Default: PasswordFormStory = {
|
|
render: (args) => <PasswordInputComponent {...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<typeof showcasePasswordSchema>
|
|
|
|
function PasswordValidationShowcase() {
|
|
const methods = useForm<ShowcasePasswordFormData>({
|
|
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 (
|
|
<FormProvider {...methods}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "3rem",
|
|
maxWidth: "800px",
|
|
padding: "2rem",
|
|
}}
|
|
>
|
|
<section>
|
|
<Typography variant="Title/md">
|
|
<h2>New Password Validation States</h2>
|
|
</Typography>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(auto-fit, minmax(350px, 1fr))",
|
|
gap: "2rem",
|
|
marginTop: "1rem",
|
|
}}
|
|
>
|
|
<div>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>Empty Password</p>
|
|
</Typography>
|
|
<PasswordInput
|
|
name="empty"
|
|
isNewPassword
|
|
errorFormatter={defaultErrorFormatter}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>Weak Password (too short)</p>
|
|
</Typography>
|
|
<PasswordInput
|
|
name="weak"
|
|
isNewPassword
|
|
errorFormatter={defaultErrorFormatter}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>Partial Password (missing requirements)</p>
|
|
</Typography>
|
|
<PasswordInput
|
|
name="partial"
|
|
isNewPassword
|
|
errorFormatter={defaultErrorFormatter}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>Valid Password</p>
|
|
</Typography>
|
|
<PasswordInput
|
|
name="valid"
|
|
isNewPassword
|
|
errorFormatter={defaultErrorFormatter}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Typography variant="Body/Paragraph/mdRegular">
|
|
<p>Too Long Password</p>
|
|
</Typography>
|
|
<PasswordInput
|
|
name="tooLong"
|
|
isNewPassword
|
|
errorFormatter={defaultErrorFormatter}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</FormProvider>
|
|
)
|
|
}
|
|
|
|
const showcaseMeta: Meta<typeof PasswordValidationShowcase> = {
|
|
title: "Core Components/PasswordInput",
|
|
component: PasswordValidationShowcase,
|
|
parameters: {
|
|
layout: "fullscreen",
|
|
},
|
|
}
|
|
|
|
type ShowcaseStory = StoryObj<typeof PasswordValidationShowcase>
|
|
|
|
export const AllValidationStates: ShowcaseStory = {
|
|
render: () => <PasswordValidationShowcase />,
|
|
parameters: {
|
|
...showcaseMeta.parameters,
|
|
},
|
|
}
|