diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 34a1c029e65d..14540220b7e4 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -1,7 +1,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Field, FieldGroup, TextInput, TextAreaInput, Box, Icon, PasswordInput, Button } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { CustomFieldsForm, PasswordVerifier } from '@rocket.chat/ui-client'; +import { CustomFieldsForm, PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, useToastMessageDispatch, @@ -91,6 +91,8 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle } }; + const passwordIsValid = useValidatePassword(password); + // FIXME: replace to endpoint const updateOwnBasicInfo = useMethod('saveUserProfile'); @@ -128,6 +130,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle const emailId = useUniqueId(); const passwordId = useUniqueId(); const confirmPasswordId = useUniqueId(); + const passwordVerifierId = useUniqueId(); return ( @@ -292,13 +295,23 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), + })} error={errors.password?.message} flexGrow={1} addon={} disabled={!allowPasswordChange} + aria-describedby={passwordVerifierId} + aria-invalid={errors.password ? 'true' : 'false'} /> + {errors?.password && ( + + {errors.password.message} + + )} + {allowPasswordChange && } {t('Confirm_password')} @@ -311,7 +324,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle error={errors.confirmationPassword?.message} flexGrow={1} addon={} - disabled={!allowPasswordChange} + disabled={!allowPasswordChange || !passwordIsValid} aria-required={password !== '' ? 'true' : 'false'} aria-invalid={errors.confirmationPassword ? 'true' : 'false'} aria-describedby={`${confirmPasswordId}-error ${confirmPasswordId}-hint`} @@ -323,7 +336,6 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {errors.confirmationPassword.message} )} - {allowPasswordChange && } {customFieldsMetadata && } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 762886f191ff..6edabf409f00 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3935,6 +3935,8 @@ "Password_History_Amount_Description": "Amount of most recently used passwords to prevent users from reusing.", "Password_must_have": "Password must have:", "Password_Policy": "Password Policy", + "Password_Policy_Aria_Description": "Below it's listed the password requirement verifications", + "Password_must_meet_the_complexity_requirements": "Password must meet the complexity requirements.", "Password_to_access": "Password to access", "Passwords_do_not_match": "Passwords do not match", "Past_Chats": "Past Chats", diff --git a/packages/ui-client/jest.config.ts b/packages/ui-client/jest.config.ts index 636d50c6a980..d5dd6be2fd45 100644 --- a/packages/ui-client/jest.config.ts +++ b/packages/ui-client/jest.config.ts @@ -1,6 +1,5 @@ export default { errorOnDeprecated: true, - testEnvironment: 'jsdom', modulePathIgnorePatterns: ['/dist/'], testMatch: ['/src/**/**.spec.[jt]s?(x)'], @@ -22,4 +21,7 @@ export default { '\\.css$': 'identity-obj-proxy', '^react($|/.+)': '/../../node_modules/react$1', }, + collectCoverage: true, + collectCoverageFrom: ['src/**/*.{ts,tsx}'], + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], }; diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 21ebbd9a58b7..911953307fad 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -21,6 +21,7 @@ "@storybook/react": "~6.5.16", "@storybook/testing-library": "~0.0.13", "@swc/jest": "^0.2.26", + "@testing-library/jest-dom": "~5.16.5", "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^8.0.1", "@types/babel__core": "~7.20.1", diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx new file mode 100644 index 000000000000..fe3be924ab18 --- /dev/null +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.stories.tsx @@ -0,0 +1,48 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +import { PasswordVerifier } from './PasswordVerifier'; + +type Response = { + enabled: boolean; + policy: [ + name: string, + value?: + | { + [x: string]: number; + } + | undefined, + ][]; +}; + +export default { + title: 'Components/PasswordVerifier', + component: PasswordVerifier, +} as ComponentMeta; + +const response: Response = { + enabled: true, + policy: [ + ['get-password-policy-minLength', { minLength: 10 }], + ['get-password-policy-forbidRepeatingCharactersCount', { maxRepeatingChars: 3 }], + ['get-password-policy-mustContainAtLeastOneLowercase'], + ['get-password-policy-mustContainAtLeastOneUppercase'], + ['get-password-policy-mustContainAtLeastOneNumber'], + ['get-password-policy-mustContainAtLeastOneSpecialCharacter'], + ], +}; + +const Wrapper = mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(); + +export const Default: ComponentStory = (args) => ( + + + +); + +Default.storyName = 'PasswordVerifier'; +Default.args = { + password: 'asd', +}; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx index a9c17f7063db..465d82725d87 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx @@ -1,37 +1,44 @@ -import { Box } from '@rocket.chat/fuselage'; +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useVerifyPassword } from '@rocket.chat/ui-contexts'; import { useTranslation } from 'react-i18next'; -import { PasswordVerifierItemCorrect } from './PasswordVerifierCorrect'; -import { PasswordVerifierItemInvalid } from './PasswordVerifierInvalid'; +import { PasswordVerifierItem } from './PasswordVerifierItem'; type PasswordVerifierProps = { password: string; + id?: string; }; -export const PasswordVerifier = ({ password }: PasswordVerifierProps) => { +export const PasswordVerifier = ({ password, id }: PasswordVerifierProps) => { const { t } = useTranslation(); + const uniqueId = useUniqueId(); - const passwordVerifications = useVerifyPassword(password); + const { data: passwordVerifications, isLoading } = useVerifyPassword(password); - if (!passwordVerifications.length) { - return <>; + if (isLoading) { + return ; + } + + if (!passwordVerifications?.length) { + return null; } return ( - - - {t('Password_must_have')} - - - {passwordVerifications.map(({ isValid, limit, name }) => - isValid ? ( - - ) : ( - - ), - )} + <> + + + + {t('Password_must_have')} + + + {passwordVerifications.map(({ isValid, limit, name }) => ( + + ))} + - + ); }; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierCorrect.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierCorrect.tsx deleted file mode 100644 index 9ce1b7b6c689..000000000000 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierCorrect.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PasswordVerifierItem } from './PasswordVerifierItem'; - -export const PasswordVerifierItemCorrect = ({ text }: { text: string }) => ( - -); diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierInvalid.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierInvalid.tsx deleted file mode 100644 index e47329b4d5a8..000000000000 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierInvalid.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PasswordVerifierItem } from './PasswordVerifierItem'; - -export const PasswordVerifierItemInvalid = ({ text }: { text: string }) => ( - -); diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx index 97091fe03771..97499c0eaf73 100644 --- a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifierItem.tsx @@ -1,16 +1,42 @@ import { Box, Icon } from '@rocket.chat/fuselage'; +import { AllHTMLAttributes, ComponentProps } from 'react'; + +const variants: { + [key: string]: { + icon: ComponentProps['name']; + color: string; + }; +} = { + success: { + icon: 'success-circle', + color: 'status-font-on-success', + }, + error: { + icon: 'error-circle', + color: 'status-font-on-danger', + }, +}; export const PasswordVerifierItem = ({ text, - color, - icon, -}: { - text: string; - color: 'status-font-on-success' | 'status-font-on-danger'; - icon: 'success-circle' | 'error-circle'; -}) => ( - - - {text} - -); + isValid, + ...props +}: { text: string; isValid: boolean } & Omit, 'is'>) => { + const { icon, color } = variants[isValid ? 'success' : 'error']; + return ( + + + {text} + + ); +}; diff --git a/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx new file mode 100644 index 000000000000..95614491c3bd --- /dev/null +++ b/packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx @@ -0,0 +1,140 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { passwordVerificationsTemplate } from '@rocket.chat/ui-contexts/dist/hooks/useVerifyPassword'; +import { render, waitFor } from '@testing-library/react'; + +import { PasswordVerifier } from './PasswordVerifier'; + +type Response = { + enabled: boolean; + policy: [ + name: string, + value?: + | { + [x: string]: number; + } + | undefined, + ][]; +}; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (str: string) => str, + i18n: { + changeLanguage: () => new Promise(() => undefined), + }, + }), +})); + +afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); +}); + +it('should render no policy if its disabled ', () => { + const response: Response = { + enabled: false, + policy: [], + }; + + const { queryByRole } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + expect(queryByRole('list')).toBeNull(); +}); + +it('should render no policy if its enabled but empty', async () => { + const response: Response = { + enabled: true, + policy: [], + }; + + const { queryByRole, queryByTestId } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitFor(() => { + expect(queryByTestId('password-verifier-skeleton')).toBeNull(); + }); + expect(queryByRole('list')).toBeNull(); +}); + +it('should render policy list if its enabled and not empty', async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 10 }]], + }; + + const { queryByRole, queryByTestId } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitFor(() => { + expect(queryByTestId('password-verifier-skeleton')).toBeNull(); + }); + + expect(queryByRole('list')).toBeVisible(); + expect(queryByRole('listitem')).toBeVisible(); +}); + +it('should render all the policies when all policies are enabled', async () => { + const response: Response = { + enabled: true, + policy: Object.keys(passwordVerificationsTemplate).map((item) => [item]), + }; + + const { queryByTestId, queryAllByRole } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitFor(() => { + expect(queryByTestId('password-verifier-skeleton')).toBeNull(); + }); + + expect(queryAllByRole('listitem').length).toEqual(response.policy.length); +}); + +it("should render policy as invalid if password doesn't match the requirements", async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 10 }]], + }; + + const { queryByTestId, getByRole } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitFor(() => { + expect(queryByTestId('password-verifier-skeleton')).toBeNull(); + }); + + expect(getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'true'); +}); + +it('should render policy as valid if password matches the requirements', async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 2 }]], + }; + + const { queryByTestId, getByRole } = render(, { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitFor(() => { + expect(queryByTestId('password-verifier-skeleton')).toBeNull(); + }); + expect(getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'false'); +}); diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 3bf9ffe819b5..6448f37b3e68 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -3,6 +3,7 @@ export * from './ExternalLink'; export * from './DotLeader'; export * from './CustomFieldsForm'; export * from './PasswordVerifier/PasswordVerifier'; +export * from '../hooks/useValidatePassword'; export { default as TextSeparator } from './TextSeparator'; export * from './TooltipComponent'; export * as UserStatus from './UserStatus'; diff --git a/packages/ui-client/src/hooks/useValidatePassword.spec.ts b/packages/ui-client/src/hooks/useValidatePassword.spec.ts new file mode 100644 index 000000000000..5d1c5a635c52 --- /dev/null +++ b/packages/ui-client/src/hooks/useValidatePassword.spec.ts @@ -0,0 +1,66 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useValidatePassword } from './useValidatePassword'; + +type Response = { + enabled: boolean; + policy: [ + name: string, + value?: + | { + [x: string]: number; + } + | undefined, + ][]; +}; + +it("should return `false` if password doesn't match all the requirements", async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 8 }]], + }; + + const { result, waitForValueToChange } = renderHook(async () => useValidatePassword('secret'), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitForValueToChange(() => result.current); + const res = await result.current; + expect(res).toBeFalsy(); +}); + +it('should return `true` if password matches all the requirements', async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 8 }]], + }; + + const { result, waitForValueToChange } = renderHook(async () => useValidatePassword('secret-password'), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + await waitForValueToChange(() => result.current); + const res = await result.current; + expect(res).toBeTruthy(); +}); + +it('should return `undefined` if password validation is still loading', async () => { + const response: Response = { + enabled: true, + policy: [['get-password-policy-minLength', { minLength: 8 }]], + }; + + const { result } = renderHook(async () => useValidatePassword('secret-password'), { + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/pw.getPolicy', () => response) + .build(), + }); + + const res = await result.current; + expect(res).toBeUndefined(); +}); diff --git a/packages/ui-client/src/hooks/useValidatePassword.ts b/packages/ui-client/src/hooks/useValidatePassword.ts new file mode 100644 index 000000000000..3402bbaf8435 --- /dev/null +++ b/packages/ui-client/src/hooks/useValidatePassword.ts @@ -0,0 +1,8 @@ +import { useVerifyPassword } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +export const useValidatePassword = (password: string) => { + const { data: passwordVerifications, isLoading } = useVerifyPassword(password); + + return useMemo(() => (isLoading ? undefined : passwordVerifications.every(({ isValid }) => isValid)), [isLoading, passwordVerifications]); +}; diff --git a/packages/ui-contexts/src/hooks/useVerifyPassword.ts b/packages/ui-contexts/src/hooks/useVerifyPassword.ts index 10a43ee65152..71985e44887f 100644 --- a/packages/ui-contexts/src/hooks/useVerifyPassword.ts +++ b/packages/ui-contexts/src/hooks/useVerifyPassword.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'; import { usePasswordPolicy } from './usePasswordPolicy'; -const passwordVerificationsTemplate: Record boolean> = { +export const passwordVerificationsTemplate: Record boolean> = { 'get-password-policy-minLength': (password: string, minLength?: number) => Boolean(minLength && password.length >= minLength), 'get-password-policy-maxLength': (password: string, maxLength?: number) => Boolean(maxLength && password.length <= maxLength), 'get-password-policy-forbidRepeatingCharactersCount': (password: string, maxRepeatingChars?: number) => { @@ -54,10 +54,16 @@ export const useVerifyPasswordByPolices = (policies?: PasswordPolicies) => { ); }; -export const useVerifyPassword = (password: string): PasswordVerifications => { - const { data } = usePasswordPolicy(); +export const useVerifyPassword = (password: string): { data: PasswordVerifications; isLoading: boolean } => { + const { data, isLoading } = usePasswordPolicy(); const validator = useVerifyPasswordByPolices((data?.enabled && data?.policy) || undefined); - return useMemo(() => validator(password), [password, validator]); + return useMemo( + () => ({ + data: validator(password), + isLoading, + }), + [password, validator, isLoading], + ); }; diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index fcba7c9c1820..db3879d6e027 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -1,7 +1,8 @@ +/* eslint-disable complexity */ import { FieldGroup, TextInput, Field, PasswordInput, ButtonGroup, Button, TextAreaInput, Callout } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form, ActionLink } from '@rocket.chat/layout'; -import { CustomFieldsForm, PasswordVerifier } from '@rocket.chat/ui-client'; +import { CustomFieldsForm, PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; import { useAccountsCustomFields, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useState } from 'react'; @@ -33,6 +34,8 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); const formLabelId = useUniqueId(); + const passwordId = useUniqueId(); + const passwordVerifierId = useUniqueId(); const registerUser = useRegisterMethod(); const customFields = useAccountsCustomFields(); @@ -49,7 +52,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo clearErrors, control, formState: { errors }, - } = useForm(); + } = useForm({ mode: 'onBlur' }); + + const password = watch('password'); + const passwordIsValid = useValidatePassword(password); const handleRegister = async ({ password, passwordConfirmation: _, ...formData }: LoginRegisterPayload) => { registerUser.mutate( @@ -155,13 +161,21 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), })} error={errors.password && (errors.password?.message || t('registration.component.form.requiredField'))} aria-invalid={errors.password ? 'true' : undefined} - id='password' + id={passwordId} placeholder={passwordPlaceholder || t('Create_a_password')} + aria-describedby={passwordVerifierId} /> + {errors?.password && ( + + {errors.password.message} + + )} + {requiresPasswordConfirmation && ( )} @@ -183,7 +198,6 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo {errors.passwordConfirmation?.type === 'required' && requiresPasswordConfirmation && ( {t('registration.component.form.requiredField')} )} - {manuallyApproveNewUsersRequired && ( diff --git a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx index 2acbf30929a8..d3a3e6fa7413 100644 --- a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx +++ b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx @@ -1,6 +1,7 @@ import { Button, Field, Modal, PasswordInput } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; -import { PasswordVerifier } from '@rocket.chat/ui-client'; +import { PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useRouter, useRouteParameter, useUser, useMethod, useTranslation, useLoginWithToken } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -20,6 +21,9 @@ const ResetPasswordPage = (): ReactElement => { const resetPassword = useMethod('resetPassword'); const token = useRouteParameter('token'); + const passwordId = useUniqueId(); + const passwordVerifierId = useUniqueId(); + const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); const router = useRouter(); @@ -32,16 +36,18 @@ const ResetPasswordPage = (): ReactElement => { register, handleSubmit, setError, - formState: { errors }, - formState, + formState: { errors, isValid }, watch, } = useForm<{ password: string; passwordConfirmation: string; }>({ - mode: 'onChange', + mode: 'onBlur', }); + const password = watch('password'); + const passwordIsValid = useValidatePassword(password); + const submit = handleSubmit(async (data) => { try { if (token) { @@ -70,37 +76,45 @@ const ResetPasswordPage = (): ReactElement => { (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), })} error={errors.password?.message} aria-invalid={errors.password ? 'true' : 'false'} - id='password' + id={passwordId} placeholder={t('Create_a_password')} name='password' autoComplete='off' + aria-describedby={passwordVerifierId} /> + {errors?.password && ( + + {errors.password.message} + + )} + {requiresPasswordConfirmation && ( watch('password') === val, + validate: (val: string) => password === val, })} error={errors.passwordConfirmation?.type === 'validate' ? t('registration.component.form.invalidConfirmPass') : undefined} aria-invalid={errors.passwordConfirmation ? 'true' : false} id='passwordConfirmation' placeholder={t('Confirm_password')} + disabled={!passwordIsValid} /> )} {errors && {errors.password?.message}} - - diff --git a/yarn.lock b/yarn.lock index 2cb0275d9a31..1a0da24949c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8853,6 +8853,7 @@ __metadata: "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 "@swc/jest": ^0.2.26 + "@testing-library/jest-dom": ~5.16.5 "@testing-library/react": ^12.1.2 "@testing-library/react-hooks": ^8.0.1 "@types/babel__core": ~7.20.1 @@ -10812,7 +10813,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^5.16.5": +"@testing-library/jest-dom@npm:^5.16.5, @testing-library/jest-dom@npm:~5.16.5": version: 5.16.5 resolution: "@testing-library/jest-dom@npm:5.16.5" dependencies: