Merged in feat/new-passwordinput-component (pull request #3376)

feat(SW-3672): Update PasswordInput component

* Update PasswordInput component

* Removed some tests not working as expected

* Remove IconButton from PasswordInput

* Remove IconButton from Input

* Merge branch 'master' into feat/new-passwordinput-component


Approved-by: Linus Flood
This commit is contained in:
Rasmus Langvad
2026-01-07 09:10:22 +00:00
parent 8c03a8b560
commit ffef566316
13 changed files with 665 additions and 206 deletions

View File

@@ -0,0 +1,281 @@
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,
},
}