Files
web/components/TempDesignSystem/Form/NewPassword/index.tsx
2025-01-13 13:35:03 +01:00

169 lines
4.7 KiB
TypeScript

"use client"
import { useState } from "react"
import { Text, TextField } from "react-aria-components"
import { Controller, useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import {
CheckIcon,
CloseIcon,
EyeHideIcon,
EyeShowIcon,
InfoCircleIcon,
} from "@/components/Icons"
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { passwordValidators } from "@/utils/passwordValidator"
import Button from "../../Button"
import { type IconProps, type NewPasswordProps } from "./newPassword"
import styles from "./newPassword.module.css"
import type { PasswordValidatorKey } from "@/types/components/form/newPassword"
export default function NewPassword({
name = "newPassword",
"aria-label": ariaLabel,
disabled = false,
placeholder = "",
registerOptions = {},
label,
visibilityToggleable = true,
}: NewPasswordProps) {
const { control } = useFormContext()
const intl = useIntl()
const [isPasswordVisible, setIsPasswordVisible] = useState(false)
return (
<Controller
disabled={disabled}
control={control}
name={name}
rules={registerOptions}
render={({ field, fieldState, formState }) => {
const errors = Object.values(formState.errors[name]?.types ?? []).flat()
return (
<TextField
aria-label={ariaLabel}
isDisabled={field.disabled}
isInvalid={fieldState.invalid}
isRequired={!!registerOptions.required}
name={field.name}
onBlur={field.onBlur}
onChange={field.onChange}
validationBehavior="aria"
value={field.value}
type={
visibilityToggleable && isPasswordVisible ? "text" : "password"
}
>
<div className={styles.inputWrapper}>
<AriaInputWithLabel
{...field}
aria-labelledby={field.name}
id={field.name}
label={intl.formatMessage({ id: "New password" })}
placeholder={placeholder}
type={
visibilityToggleable && isPasswordVisible
? "text"
: "password"
}
/>
{visibilityToggleable ? (
<Button
className={styles.eyeIcon}
type="button"
variant="icon"
size="small"
intent="tertiary"
onClick={() => setIsPasswordVisible((value) => !value)}
>
{isPasswordVisible ? <EyeHideIcon /> : <EyeShowIcon />}
</Button>
) : null}
</div>
<PasswordValidation value={field.value} errors={errors} />
{!field.value && fieldState.error ? (
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />
{fieldState.error.message}
</Caption>
) : null}
</TextField>
)
}}
/>
)
}
function Icon({ errorMessage, errors }: IconProps) {
return errors.includes(errorMessage) ? (
<CloseIcon color="red" height={20} width={20} />
) : (
<CheckIcon color="green" height={20} width={20} />
)
}
function PasswordValidation({
value,
errors,
}: {
value: string
errors: string[]
}) {
const intl = useIntl()
if (!value) return null
function getErrorMessage(key: PasswordValidatorKey) {
switch (key) {
case "length":
return intl.formatMessage(
{
id: "{min} to {max} characters",
},
{
min: 10,
max: 40,
}
)
case "hasUppercase":
return intl.formatMessage(
{ id: "{count} uppercase letter" },
{ count: 1 }
)
case "hasLowercase":
return intl.formatMessage(
{ id: "{count} lowercase letter" },
{ count: 1 }
)
case "hasNumber":
return intl.formatMessage({ id: "{count} number" }, { count: 1 })
case "hasSpecialChar":
return intl.formatMessage(
{ id: "{count} special character" },
{ count: 1 }
)
}
}
return (
<div className={styles.errors}>
{Object.entries(passwordValidators).map(([key, { message }]) => (
<Caption asChild color="black" key={key}>
<Text className={styles.helpText} slot="description">
<Icon errorMessage={message} errors={errors} />
{getErrorMessage(key as PasswordValidatorKey)}
</Text>
</Caption>
))}
</div>
)
}