Skip to content

Commit 2297bea

Browse files
committed
fix: password verifier accessibility
1 parent b72382b commit 2297bea

File tree

6 files changed

+46
-25
lines changed

6 files changed

+46
-25
lines changed

apps/meteor/client/views/account/security/ChangePassphrase.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,26 @@ export const ChangePassphrase = (): JSX.Element => {
5959
reset,
6060
resetField,
6161
control,
62+
trigger,
6263
} = useForm({
6364
defaultValues,
6465
mode: 'all',
6566
});
6667

67-
const { passphrase } = watch();
68+
const { passphrase, confirmationPassphrase } = watch();
6869
const { validations, valid } = useValidatePassphrase(passphrase);
6970
useEffect(() => {
7071
if (!valid) {
7172
resetField('confirmationPassphrase');
73+
return;
7274
}
73-
}, [valid, resetField]);
75+
if (confirmationPassphrase) {
76+
const validateConfirmation = async () => {
77+
await trigger('confirmationPassphrase');
78+
};
79+
void validateConfirmation();
80+
}
81+
}, [valid, confirmationPassphrase, resetField, trigger]);
7482
const keysExist = useKeysExist();
7583

7684
const updatePassword = useChangeE2EPasswordMutation();

packages/password-policies/src/PasswordPolicy.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ type PasswordPolicyType = {
2525
policy: PasswordPolicyParametersEntry[];
2626
};
2727

28-
export type PasswordPolicyOptions = Partial<PasswordPolicyMap & {
29-
enabled: boolean;
30-
throwError: boolean;
31-
}>;
28+
export type PasswordPolicyOptions = Partial<
29+
PasswordPolicyMap & {
30+
enabled: boolean;
31+
throwError: boolean;
32+
}
33+
>;
3234

3335
export type PasswordPolicyValidation = {
3436
[K in PasswordPolicyKey]: PasswordPolicyMap[K] extends number

packages/ui-client/src/components/PasswordVerifier/PasswordVerifier.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { useVerifyPassword } from '@rocket.chat/ui-contexts';
22

3-
import { PasswordVerifierList } from './PasswordVerifierList';
3+
import { PasswordVerifierList, type PasswordVerifierListProps } from './PasswordVerifierList';
44

5-
type PasswordVerifierProps = {
6-
password: string | undefined;
7-
id?: string;
8-
vertical?: boolean;
5+
export type PasswordVerifierProps = Pick<PasswordVerifierListProps, 'id' | 'vertical'> & {
6+
password: string;
97
};
108

119
export const PasswordVerifier = ({ password, id, vertical }: PasswordVerifierProps) => {
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,45 @@
11
import { Box, Icon, type IconProps } from '@rocket.chat/fuselage';
22
import type { PasswordPolicyValidation } from '@rocket.chat/ui-contexts';
3+
import { useId } from 'react';
34
import { useTranslation } from 'react-i18next';
45

56
const variants = {
67
success: {
78
name: 'success-circle',
9+
label: 'Success',
810
color: 'status-font-on-success',
911
},
1012
error: {
1113
name: 'error-circle',
14+
label: 'Error',
1215
color: 'status-font-on-danger',
1316
},
14-
} satisfies Record<string, IconProps>;
17+
} as const satisfies Record<string, IconProps>;
1518

1619
type PasswordVerifierItemProps = PasswordPolicyValidation & {
1720
vertical: boolean;
1821
};
1922

2023
export const PasswordVerifierItem = (props: PasswordVerifierItemProps) => {
2124
const { t } = useTranslation();
22-
const { name, color } = variants[props.isValid ? 'success' : 'error'];
23-
const label = t(`${props.name}-label`, 'limit' in props ? { limit: props.limit } : undefined);
25+
const id = useId();
26+
const icon = variants[props.isValid ? 'success' : 'error'];
27+
const name = `${props.name}-label` as const;
28+
const requirementText = t(name, 'limit' in props ? { limit: props.limit } : undefined);
2429
return (
2530
<Box
2631
display='flex'
2732
flexBasis={props.vertical ? '100%' : '50%'}
2833
alignItems='center'
2934
mbe={8}
3035
fontScale='c1'
31-
color={color}
36+
color={icon.color}
3237
role='listitem'
33-
aria-label={label}
38+
aria-hidden='false'
39+
aria-labelledby={`${id}-icon ${id}-text`}
3440
>
35-
<Icon name={name} color={color} size='x16' mie={4} />
36-
<span aria-hidden>{label}</span>
41+
<Icon id={`${id}-icon`} aria-label={t(icon.label)} aria-hidden='false' name={icon.name} color={icon.color} size='x16' mie={4} />
42+
<span id={`${id}-text`}>{requirementText}</span>
3743
</Box>
3844
);
3945
};

packages/ui-client/src/components/PasswordVerifier/PasswordVerifierList.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next';
55

66
import { PasswordVerifierItem } from './PasswordVerifierItem';
77

8-
type PasswordVerifierListProps = {
8+
export type PasswordVerifierListProps = {
99
id?: string;
1010
validations: PasswordPolicyValidation[];
1111
vertical?: boolean;
1212
};
1313

14-
export const PasswordVerifierList = ({ id, validations, vertical }: PasswordVerifierListProps) => {
14+
export const PasswordVerifierList = ({ id, validations, vertical = true }: PasswordVerifierListProps) => {
1515
const { t } = useTranslation();
1616
const uniqueId = useId();
1717

@@ -28,9 +28,9 @@ export const PasswordVerifierList = ({ id, validations, vertical }: PasswordVeri
2828
<Box mbe={8} fontScale='c2' id={uniqueId} aria-hidden>
2929
{t('Password_must_have')}
3030
</Box>
31-
<Box display='flex' flexWrap='wrap' role='list' aria-labelledby={uniqueId}>
31+
<Box display='flex' flexWrap='wrap' role='list' aria-labelledby={uniqueId} aria-live='polite'>
3232
{validations.map((validation) => (
33-
<PasswordVerifierItem key={validation.name} vertical={!!vertical} {...validation} />
33+
<PasswordVerifierItem key={validation.name} vertical={vertical} {...validation} />
3434
))}
3535
</Box>
3636
</Box>

packages/ui-client/src/components/PasswordVerifier/PasswordVerifiers.spec.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ it('should render policy list if its enabled and not empty', async () => {
4040
});
4141

4242
expect(screen.queryByRole('list')).toBeVisible();
43-
expect(screen.queryByRole('listitem', { name: 'get-password-policy-minLength-label' })).toBeVisible();
43+
expect(screen.queryByRole('listitem', { name: 'Success get-password-policy-minLength-label' })).toBeVisible();
4444
});
4545

4646
it('should render all the policies when all policies are enabled', async () => {
@@ -77,7 +77,10 @@ it("should render policy as invalid if password doesn't match the requirements",
7777
expect(screen.queryByTestId('password-verifier-skeleton')).toBeNull();
7878
});
7979

80-
expect(screen.getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'true');
80+
const item = screen.getByRole('listitem', { name: 'Error get-password-policy-minLength-label' });
81+
82+
expect(item.children[0]).toHaveAccessibleName('Error');
83+
expect(item.children[1]).toHaveTextContent('get-password-policy-minLength-label');
8184
});
8285

8386
it('should render policy as valid if password matches the requirements', async () => {
@@ -91,5 +94,9 @@ it('should render policy as valid if password matches the requirements', async (
9194
await waitFor(() => {
9295
expect(screen.queryByTestId('password-verifier-skeleton')).toBeNull();
9396
});
94-
expect(screen.getByRole('listitem', { name: 'get-password-policy-minLength-label' })).toHaveAttribute('aria-invalid', 'false');
97+
98+
const item = screen.getByRole('listitem', { name: 'Success get-password-policy-minLength-label' });
99+
100+
expect(item.children[0]).toHaveAccessibleName('Success');
101+
expect(item.children[1]).toHaveTextContent('get-password-policy-minLength-label');
95102
});

0 commit comments

Comments
 (0)