Skip to content

Commit 2a1cdba

Browse files
joebuonoJoe Buonohbuchel
authored
ui-react(fix): Update ShowPasswordButton (#2323)
* Update ShowPasswordButton to use role=“switch” and add additional SR context * update types and PasswordField * updated tests * Create poor-pans-chew.md * updated props table, marked hidePasswordButtonLabel as deprecated, updated variable names * adding passwordIsHiddenLabel and passwordIsShownLabel props * update tests * update docs * update type descriptions * update props table * Update .changeset/poor-pans-chew.md Co-authored-by: Heather Buchel <[email protected]> * update changeset Co-authored-by: Joe Buono <[email protected]> Co-authored-by: Heather Buchel <[email protected]>
1 parent 5ffdc40 commit 2a1cdba

File tree

11 files changed

+193
-97
lines changed

11 files changed

+193
-97
lines changed

.changeset/poor-pans-chew.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@aws-amplify/ui-react": patch
3+
---
4+
5+
ui-react(fix): Update ShowPasswordButton to use role=“switch” and add additional screen reader context
6+
7+
- Keep consistent aria-label “Show password”
8+
- Add a visually hidden aria-live region (polite) that updates based on the ShowPasswordButton checked state
9+
- Add `passwordIsHiddenLabel` and `passwordIsShownLabel` props for screen readers
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PasswordField } from '@aws-amplify/ui-react';
2+
3+
export const ShowPasswordButtonExample = () => {
4+
return (
5+
<PasswordField
6+
label="Password"
7+
showPasswordButtonLabel="Toggle password shown/hidden"
8+
passwordIsHiddenLabel="Your password is hidden"
9+
passwordIsShownLabel="Your password is shown"
10+
/>
11+
);
12+
};

docs/src/pages/[platform]/components/passwordfield/examples/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { MaxLengthExample } from './MaxLengthExample';
99
export { PasswordFieldStyledPropsExample } from './PasswordFieldStyledPropsExample';
1010
export { RefExample } from './refs';
1111
export { RequiredPasswordFieldExample } from './RequiredPasswordFieldExample';
12+
export { ShowPasswordButtonExample } from './ShowPasswordButtonExample';
1213
export { SignUpFormExample } from './SignUpFormExample';
1314
export { SizeExample } from './SizeExample';
1415
export { PasswordFieldThemeExample } from './PasswordFieldThemeExample';

docs/src/pages/[platform]/components/passwordfield/props-table.mdx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ boolean
116116
string
117117
```
118118
</TableCell>
119-
<TableCell className="props-table__tr-description">Set the &#96;aria-label&#96; for hide password button Default: "Hide password"</TableCell>
119+
<TableCell className="props-table__tr-description">The hidePasswordButtonLabel prop is no longer in use, since the aria-label is now consistent between state changes. Set the &#96;aria-label&#96; for hide password button Default: "Hide password"</TableCell>
120120
</TableRow>
121121

122122
<TableRow>
@@ -249,6 +249,26 @@ React.ReactNode
249249
<TableCell className="props-table__tr-description">Component(s) to show before input</TableCell>
250250
</TableRow>
251251

252+
<TableRow>
253+
<TableCell className="props-table__tr-name">passwordIsHiddenLabel</TableCell>
254+
<TableCell>
255+
```jsx
256+
string
257+
```
258+
</TableCell>
259+
<TableCell className="props-table__tr-description">Sets the text read by screen readers when the password is hidden Default: "Password is hidden"</TableCell>
260+
</TableRow>
261+
262+
<TableRow>
263+
<TableCell className="props-table__tr-name">passwordIsShownLabel</TableCell>
264+
<TableCell>
265+
```jsx
266+
string
267+
```
268+
</TableCell>
269+
<TableCell className="props-table__tr-description">Sets the text read by screen readers when the password is shown Default: "Password is shown"</TableCell>
270+
</TableRow>
271+
252272
<TableRow>
253273
<TableCell className="props-table__tr-name">placeholder</TableCell>
254274
<TableCell>

docs/src/pages/[platform]/components/passwordfield/react.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import {
2424
LoginFormExample,
2525
MaxLengthExample,
2626
PasswordFieldStyledPropsExample,
27+
PasswordFieldThemeExample,
2728
RefExample,
2829
RequiredPasswordFieldExample,
30+
ShowPasswordButtonExample,
2931
SignUpFormExample,
3032
SizeExample,
31-
PasswordFieldThemeExample,
3233
ValidationErrorExample,
3334
VariationExample,
3435
} from './examples';
@@ -128,6 +129,27 @@ _Change password example: `current-password` and `new-password`_
128129
</ExampleCode>
129130
</Example>
130131

132+
#### ShowPasswordButton
133+
134+
The ShowPasswordButton renders a `<button>` element with `role="switch"`. Its `aria-checked` attribute is set to `false` when the password is hidden, and `true` when the password is shown.
135+
136+
There are several optional props for customizing the accessibility of the ShowPasswordButton:
137+
138+
- `showPasswordButtonLabel`: Sets the `aria-label` for the ShowPasswordButton (defaults to `"Show password"`)
139+
- `passwordIsHiddenLabel`: Sets the text read by screen readers when the password is hidden (defaults to `"Password is hidden"`)
140+
- `passwordIsShownLabel`: Sets the text read by screen readers when the password is shown (defaults to `"Password is shown"`)
141+
142+
<Example>
143+
<ShowPasswordButtonExample />
144+
<ExampleCode>
145+
146+
```jsx file=./examples/ShowPasswordButtonExample.tsx
147+
148+
```
149+
150+
</ExampleCode>
151+
</Example>
152+
131153
### Sizes
132154

133155
Use the `size` prop to change the visual size of the PasswordField. Three sizes are available: `small`, (default), and `large`.

packages/react/src/primitives/PasswordField/PasswordField.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const PasswordFieldPrimitive: Primitive<PasswordFieldProps, 'input'> = (
1212
label,
1313
className,
1414
hideShowPassword = false,
15-
hidePasswordButtonLabel,
15+
passwordIsHiddenLabel,
16+
passwordIsShownLabel,
1617
showPasswordButtonLabel,
1718
showPasswordButtonRef,
1819
size,
@@ -39,9 +40,10 @@ const PasswordFieldPrimitive: Primitive<PasswordFieldProps, 'input'> = (
3940
<ShowPasswordButton
4041
fieldType={type}
4142
onClick={showPasswordOnClick}
43+
passwordIsHiddenLabel={passwordIsHiddenLabel}
44+
passwordIsShownLabel={passwordIsShownLabel}
4245
ref={showPasswordButtonRef}
4346
size={size}
44-
hidePasswordButtonLabel={hidePasswordButtonLabel}
4547
showPasswordButtonLabel={showPasswordButtonLabel}
4648
/>
4749
)

packages/react/src/primitives/PasswordField/ShowPasswordButton.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,43 @@
11
import * as React from 'react';
22

33
import { Button } from '../Button';
4+
import { VisuallyHidden } from '../VisuallyHidden';
45
import { ComponentClassNames, ComponentText } from '../shared/constants';
56
import { IconVisibility, IconVisibilityOff } from '../Icon/internal';
67
import { Primitive, ShowPasswordButtonProps } from '../types';
78

8-
const ariaLabelText = ComponentText.PasswordField;
9+
const { passwordIsHidden, passwordIsShown, showPassword } =
10+
ComponentText.PasswordField;
911

1012
const ShowPasswordButtonPrimitive: Primitive<
1113
ShowPasswordButtonProps,
1214
typeof Button
1315
> = (
1416
{
1517
fieldType,
18+
passwordIsHiddenLabel = passwordIsHidden,
19+
passwordIsShownLabel = passwordIsShown,
20+
showPasswordButtonLabel = showPassword,
1621
size,
17-
hidePasswordButtonLabel,
18-
showPasswordButtonLabel,
1922
...rest
2023
},
2124
ref
2225
) => {
2326
return (
2427
<Button
25-
ariaLabel={
26-
fieldType === 'password'
27-
? showPasswordButtonLabel || ariaLabelText.showPasswordButtonLabel
28-
: hidePasswordButtonLabel || ariaLabelText.hidePasswordButtonLabel
29-
}
28+
aria-checked={fieldType !== 'password'}
29+
ariaLabel={showPasswordButtonLabel}
3030
className={ComponentClassNames.FieldShowPassword}
3131
ref={ref}
32+
role="switch"
3233
size={size}
3334
{...rest}
3435
>
36+
<VisuallyHidden aria-live="polite">
37+
{fieldType === 'password'
38+
? passwordIsHiddenLabel
39+
: passwordIsShownLabel}
40+
</VisuallyHidden>
3541
{fieldType === 'password' ? (
3642
<IconVisibility size={size} />
3743
) : (

packages/react/src/primitives/PasswordField/__tests__/PasswordField.test.tsx

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import * as React from 'react';
22
import { render, screen } from '@testing-library/react';
3-
import userEvent from '@testing-library/user-event';
43

54
import { PasswordField } from '../PasswordField';
65
import { ComponentClassNames, ComponentText } from '../../shared/constants';
76

8-
const ariaLabelText = ComponentText.PasswordField;
9-
107
describe('PasswordField component', () => {
118
const testId = 'PasswordFieldTestId';
129
it('should render classname for PasswordField', async () => {
@@ -39,8 +36,8 @@ describe('PasswordField component', () => {
3936
);
4037

4138
await screen.findByTestId(testId);
42-
expect(ref.current.nodeName).toBe('INPUT');
43-
expect(showPasswordButtonRef.current.nodeName).toBe('BUTTON');
39+
expect(ref.current?.nodeName).toBe('INPUT');
40+
expect(showPasswordButtonRef.current?.nodeName).toBe('BUTTON');
4441
});
4542

4643
it('should be password input type', async () => {
@@ -82,10 +79,10 @@ describe('PasswordField component', () => {
8279
/>
8380
);
8481

85-
const button = await screen.findByRole('button');
82+
const button = await screen.findByRole('switch');
8683
expect(button).toBeDefined();
8784
expect(button.getAttribute('aria-label')).toBe(
88-
ariaLabelText.showPasswordButtonLabel
85+
ComponentText.PasswordField.showPassword
8986
);
9087
});
9188

@@ -102,60 +99,4 @@ describe('PasswordField component', () => {
10299
const button = screen.queryByRole('button');
103100
expect(button).toBeNull();
104101
});
105-
106-
describe(' - ShowPasswordButton', () => {
107-
it('should toggle button type and label when clicked', async () => {
108-
render(
109-
<PasswordField
110-
label="Password"
111-
descriptiveText="Required"
112-
name="password"
113-
placeholder="Password"
114-
/>
115-
);
116-
117-
const button = await screen.findByRole('button');
118-
const passwordField = await screen.findByPlaceholderText('Password');
119-
120-
expect(passwordField.getAttribute('type')).toBe('password');
121-
expect(button.getAttribute('aria-label')).toBe(
122-
ariaLabelText.showPasswordButtonLabel
123-
);
124-
125-
userEvent.click(button);
126-
127-
expect(passwordField.getAttribute('type')).toBe('text');
128-
expect(button.getAttribute('aria-label')).toBe(
129-
ariaLabelText.hidePasswordButtonLabel
130-
);
131-
132-
userEvent.click(button);
133-
134-
expect(passwordField.getAttribute('type')).toBe('password');
135-
expect(button.getAttribute('aria-label')).toBe(
136-
ariaLabelText.showPasswordButtonLabel
137-
);
138-
});
139-
});
140-
141-
it('should be able to customize show/hide password button label', async () => {
142-
const showPasswordButtonLabel = 'Show my password';
143-
const hidePasswordButtonLabel = 'Hide my password';
144-
render(
145-
<PasswordField
146-
label="Password"
147-
name="password"
148-
placeholder="Password"
149-
showPasswordButtonLabel={showPasswordButtonLabel}
150-
hidePasswordButtonLabel={hidePasswordButtonLabel}
151-
/>
152-
);
153-
154-
const button = await screen.findByRole('button');
155-
expect(button).toHaveAttribute('aria-label', showPasswordButtonLabel);
156-
157-
userEvent.click(button);
158-
159-
expect(button).toHaveAttribute('aria-label', hidePasswordButtonLabel);
160-
});
161102
});
Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import * as React from 'react';
22
import { render, screen } from '@testing-library/react';
33

4+
import { PasswordField } from '../PasswordField';
45
import { ShowPasswordButton } from '../ShowPasswordButton';
56
import { ComponentClassNames, ComponentText } from '../../shared/constants';
7+
import userEvent from '@testing-library/user-event';
68

7-
const ariaLabelText = ComponentText.PasswordField;
9+
const { passwordIsHidden, passwordIsShown } = ComponentText.PasswordField;
810

911
describe('ShowPasswordButton component', () => {
10-
const testId = 'testId';
11-
1212
it('should render default classname for ShowPasswordButton', async () => {
1313
render(<ShowPasswordButton fieldType="password" />);
1414

15-
const button = await screen.findByRole('button');
15+
const button = await screen.findByRole('switch');
1616

1717
expect(button).toHaveClass(ComponentClassNames.FieldShowPassword);
1818
});
@@ -21,33 +21,79 @@ describe('ShowPasswordButton component', () => {
2121
const ref = React.createRef<HTMLButtonElement>();
2222
render(<ShowPasswordButton fieldType="password" ref={ref} />);
2323

24-
await screen.findByRole('button');
24+
await screen.findByRole('switch');
2525

26-
expect(ref.current.nodeName).toBe('BUTTON');
26+
expect(ref.current?.nodeName).toBe('BUTTON');
2727
});
2828

29-
it('should set correct ariaLabel for fieldType password', async () => {
30-
const ref = React.createRef<HTMLButtonElement>();
31-
render(<ShowPasswordButton fieldType="password" ref={ref} />);
29+
it('should toggle field type, screen reader context, and aria-checked when clicked', async () => {
30+
render(
31+
<PasswordField
32+
label="Password"
33+
descriptiveText="Required"
34+
name="password"
35+
placeholder="Password"
36+
/>
37+
);
3238

33-
await screen.findByLabelText(ariaLabelText.showPasswordButtonLabel);
39+
const passwordField = await screen.findByPlaceholderText('Password');
40+
const button = await screen.findByRole('switch');
41+
const visuallyHidden = await screen.findByText(passwordIsHidden);
3442

35-
expect(ref.current.nodeName).toBe('BUTTON');
43+
expect(passwordField.getAttribute('type')).toBe('password');
44+
expect(button.getAttribute('aria-checked')).toBe('false');
45+
expect(visuallyHidden.textContent).toBe(passwordIsHidden);
46+
47+
userEvent.click(button);
48+
49+
expect(passwordField.getAttribute('type')).toBe('text');
50+
expect(button.getAttribute('aria-checked')).toBe('true');
51+
expect(visuallyHidden.textContent).toBe(passwordIsShown);
52+
53+
userEvent.click(button);
54+
55+
expect(passwordField.getAttribute('type')).toBe('password');
56+
expect(button.getAttribute('aria-checked')).toBe('false');
57+
expect(visuallyHidden.textContent).toBe(passwordIsHidden);
3658
});
3759

38-
it('should set correct ariaLabel for fieldType text', async () => {
39-
const ref = React.createRef<HTMLButtonElement>();
60+
it('should be able to customize show password button label', async () => {
61+
const showPasswordButtonLabel = 'Show my password';
62+
63+
render(
64+
<PasswordField
65+
label="Password"
66+
name="password"
67+
placeholder="Password"
68+
showPasswordButtonLabel={showPasswordButtonLabel}
69+
/>
70+
);
71+
72+
const button = await screen.findByRole('switch');
73+
expect(button).toHaveAttribute('aria-label', showPasswordButtonLabel);
74+
});
75+
76+
it('should be able to customize passwordIsHiddenLabel and passwordIsShownLabel', async () => {
77+
const passwordIsHiddenLabel = 'Your password is hidden';
78+
const passwordIsShownLabel = 'Your password is visible';
79+
4080
render(
41-
<ShowPasswordButton
42-
fieldType="text"
43-
className="custom-class"
44-
ref={ref}
45-
testId={testId}
81+
<PasswordField
82+
label="Password"
83+
name="password"
84+
placeholder="Password"
85+
passwordIsHiddenLabel={passwordIsHiddenLabel}
86+
passwordIsShownLabel={passwordIsShownLabel}
4687
/>
4788
);
4889

49-
await screen.findByLabelText(ariaLabelText.hidePasswordButtonLabel);
90+
const button = await screen.findByRole('switch');
91+
const visuallyHidden = await screen.findByText(passwordIsHiddenLabel);
92+
93+
expect(visuallyHidden.textContent).toBe(passwordIsHiddenLabel);
94+
95+
userEvent.click(button);
5096

51-
expect(ref.current.nodeName).toBe('BUTTON');
97+
expect(visuallyHidden.textContent).toBe(passwordIsShownLabel);
5298
});
5399
});

0 commit comments

Comments
 (0)