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:
@@ -12,8 +12,8 @@ import { FormSelect } from "@scandic-hotels/design-system/Form/Select"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { getLocalizedLanguageOptions } from "@/constants/languages"
|
||||
import { PasswordInput } from "@scandic-hotels/design-system/PasswordInput"
|
||||
|
||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getFormattedCountryList } from "@/utils/countries"
|
||||
import {
|
||||
@@ -165,8 +165,14 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
|
||||
defaultMessage: "Current password",
|
||||
})}
|
||||
name="password"
|
||||
errorFormatter={formatFormErrorMessage}
|
||||
/>
|
||||
<PasswordInput
|
||||
data-hj-suppress
|
||||
isNewPassword
|
||||
name="newPassword"
|
||||
errorFormatter={formatFormErrorMessage}
|
||||
/>
|
||||
<PasswordInput data-hj-suppress isNewPassword name="newPassword" />
|
||||
<PasswordInput
|
||||
data-hj-suppress
|
||||
label={intl.formatMessage({
|
||||
@@ -174,6 +180,7 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
|
||||
defaultMessage: "Retype new password",
|
||||
})}
|
||||
name="retypeNewPassword"
|
||||
errorFormatter={formatFormErrorMessage}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -21,6 +21,7 @@ import CountrySelect from "@scandic-hotels/design-system/Form/Country"
|
||||
import DateSelect from "@scandic-hotels/design-system/Form/Date"
|
||||
import { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
|
||||
import Phone from "@scandic-hotels/design-system/Form/Phone"
|
||||
import { PasswordInput } from "@scandic-hotels/design-system/PasswordInput"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { TextLinkButton } from "@scandic-hotels/design-system/TextLinkButton"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
} from "@scandic-hotels/trpc/routers/user/schemas"
|
||||
|
||||
import ProfilingConsentModalReadOnly from "@/components/MyPages/ProfilingConsent/Modal/ReadOnly"
|
||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getFormattedCountryList } from "@/utils/countries"
|
||||
import {
|
||||
@@ -289,6 +289,7 @@ export default function SignupForm({
|
||||
defaultMessage: "Password",
|
||||
})}
|
||||
isNewPassword
|
||||
errorFormatter={formatFormErrorMessage}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { passwordValidators } from "@scandic-hotels/common/utils/zod/passwordValidator"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./passwordInput.module.css"
|
||||
|
||||
import type { PasswordValidatorKey } from "@/types/components/form/newPassword"
|
||||
|
||||
export function NewPasswordValidation({
|
||||
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: "passwordInput.lengthRequirement",
|
||||
defaultMessage: "{min} to {max} characters",
|
||||
},
|
||||
{
|
||||
min: 10,
|
||||
max: 40,
|
||||
}
|
||||
)
|
||||
case "hasUppercase":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "passwordInput.uppercaseRequirement",
|
||||
defaultMessage: "{count} uppercase letter",
|
||||
},
|
||||
{ count: 1 }
|
||||
)
|
||||
case "hasLowercase":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "passwordInput.lowercaseRequirement",
|
||||
defaultMessage: "{count} lowercase letter",
|
||||
},
|
||||
{ count: 1 }
|
||||
)
|
||||
case "hasNumber":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "passwordInput.numberRequirement",
|
||||
defaultMessage: "{count} number",
|
||||
},
|
||||
{ count: 1 }
|
||||
)
|
||||
case "hasSpecialChar":
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: "passwordInput.specialCharacterRequirement",
|
||||
defaultMessage: "{count} special character",
|
||||
},
|
||||
{ count: 1 }
|
||||
)
|
||||
case "allowedCharacters":
|
||||
return intl.formatMessage({
|
||||
id: "passwordInput.allowedCharactersRequirement",
|
||||
defaultMessage: "Only allowed characters",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.errors} role="status" id="password-requirements">
|
||||
{Object.entries(passwordValidators).map(([key, { message }]) => (
|
||||
<Typography variant="Label/xsRegular" key={key}>
|
||||
<span className={styles.helpText}>
|
||||
<Icon errorMessage={message} errors={errors} />
|
||||
{getErrorMessage(key as PasswordValidatorKey)}
|
||||
</span>
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface IconProps {
|
||||
errorMessage: string
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
function Icon({ errorMessage, errors }: IconProps) {
|
||||
const intl = useIntl()
|
||||
return errors.includes(errorMessage) ? (
|
||||
<MaterialIcon
|
||||
icon="close"
|
||||
color="Icon/Feedback/Error"
|
||||
size={20}
|
||||
role="img"
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.error",
|
||||
defaultMessage: "Error",
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcon
|
||||
icon="check"
|
||||
color="Icon/Feedback/Success"
|
||||
size={20}
|
||||
role="img"
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.success",
|
||||
defaultMessage: "Success",
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { TextField } from "react-aria-components"
|
||||
import {
|
||||
Controller,
|
||||
type RegisterOptions,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { Input } from "@scandic-hotels/design-system/Input"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { getErrorMessage } from "@/utils/getErrorMessage"
|
||||
|
||||
import { NewPasswordValidation } from "./NewPasswordValidation"
|
||||
|
||||
import styles from "./passwordInput.module.css"
|
||||
|
||||
interface PasswordInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
registerOptions?: RegisterOptions
|
||||
visibilityToggleable?: boolean
|
||||
isNewPassword?: boolean
|
||||
}
|
||||
|
||||
export default function PasswordInput({
|
||||
name = "password",
|
||||
label,
|
||||
"aria-label": ariaLabel,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
registerOptions = {},
|
||||
visibilityToggleable = true,
|
||||
isNewPassword = false,
|
||||
className = "",
|
||||
}: PasswordInputProps) {
|
||||
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 = isNewPassword
|
||||
? Object.values(formState.errors[name]?.types ?? []).flat()
|
||||
: []
|
||||
|
||||
return (
|
||||
<TextField
|
||||
className={className}
|
||||
aria-label={ariaLabel}
|
||||
aria-invalid={!!fieldState.error}
|
||||
aria-describedby="password-error password-requirements"
|
||||
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}>
|
||||
<Input
|
||||
{...field}
|
||||
aria-labelledby={field.name}
|
||||
id={field.name}
|
||||
label={
|
||||
label ||
|
||||
(isNewPassword
|
||||
? intl.formatMessage({
|
||||
id: "passwordInput.newPasswordLabel",
|
||||
defaultMessage: "New password",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "common.password",
|
||||
defaultMessage: "Password",
|
||||
}))
|
||||
}
|
||||
placeholder={placeholder}
|
||||
type={
|
||||
visibilityToggleable && isPasswordVisible
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
/>
|
||||
{visibilityToggleable ? (
|
||||
<IconButton
|
||||
variant="Muted"
|
||||
emphasis
|
||||
onPress={() => setIsPasswordVisible((value) => !value)}
|
||||
aria-label={
|
||||
isPasswordVisible
|
||||
? intl.formatMessage({
|
||||
id: "passwordInput.hidePassword",
|
||||
defaultMessage: "Hide password",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "passwordInput.showPassword",
|
||||
defaultMessage: "Show password",
|
||||
})
|
||||
}
|
||||
aria-controls={field.name}
|
||||
className={styles.toggleButton}
|
||||
iconName={isPasswordVisible ? "visibility_off" : "visibility"}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{isNewPassword ? (
|
||||
<NewPasswordValidation value={field.value} errors={errors} />
|
||||
) : null}
|
||||
|
||||
{isNewPassword ? (
|
||||
!field.value && fieldState.error ? (
|
||||
<ErrorMessage errorMessage={fieldState.error.message} />
|
||||
) : null
|
||||
) : fieldState.error ? (
|
||||
<ErrorMessage errorMessage={fieldState.error.message} />
|
||||
) : null}
|
||||
</TextField>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorMessage({ errorMessage }: { errorMessage?: string }) {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
className={styles.error}
|
||||
>
|
||||
<p role="alert" id="password-error">
|
||||
<MaterialIcon
|
||||
icon="info"
|
||||
color="Icon/Feedback/Error"
|
||||
aria-label={intl.formatMessage({
|
||||
id: "common.error",
|
||||
defaultMessage: "Error",
|
||||
})}
|
||||
/>
|
||||
{getErrorMessage(intl, errorMessage)}
|
||||
</p>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
.helpText {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
.error {
|
||||
align-items: center;
|
||||
color: var(--Text-Interactive-Error);
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
margin: var(--Space-x1) 0 0;
|
||||
}
|
||||
|
||||
.errors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--Space-x15) var(--Space-x1);
|
||||
padding-top: var(--Space-x1);
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputWrapper .toggleButton {
|
||||
position: absolute;
|
||||
right: var(--Space-x2);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Hide the built-in password reveal icon in Microsoft Edge.
|
||||
* See: https://learn.microsoft.com/en-us/microsoft-edge/web-platform/password-reveal
|
||||
*/
|
||||
.inputWrapper input::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
.inputWrapper input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { passwordValidators } from "@scandic-hotels/common/utils/zod/passwordValidator"
|
||||
|
||||
export type PasswordValidatorKey = keyof typeof passwordValidators
|
||||
Reference in New Issue
Block a user