Skip to content

Commit 2f8e2be

Browse files
Improve accessibility of the `<AvatarSetting> component (#30907)
* Always use an accessible button with base avatar rendered inside it * Rename avatarAltText to accessibleName * Improve accessibility * Fix tests
1 parent b6046d2 commit 2f8e2be

File tree

5 files changed

+40
-42
lines changed

5 files changed

+40
-42
lines changed

src/components/views/room_settings/RoomProfileSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
262262
? undefined
263263
: (this.state.avatarFile ?? this.state.originalAvatarUrl ?? undefined)
264264
}
265-
avatarAltText={_t("room_settings|general|avatar_field_label")}
265+
avatarAccessibleName={_t("room_settings|general|avatar_field_label")}
266266
disabled={!this.state.canSetAvatar}
267267
onChange={this.onAvatarChanged}
268268
removeAvatar={canRemove ? this.removeAvatar : undefined}

src/components/views/settings/AvatarSetting.tsx

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
66
Please see LICENSE files in the repository root for full details.
77
*/
88

9-
import React, { type JSX, type ReactNode, createRef, useCallback, useEffect, useState, useId } from "react";
9+
import React, { type JSX, type ReactNode, createRef, useCallback, useEffect, useState } from "react";
1010
import EditIcon from "@vector-im/compound-design-tokens/assets/web/icons/edit";
1111
import UploadIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
1212
import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete";
@@ -89,9 +89,9 @@ interface IProps {
8989
removeAvatar?: () => void;
9090

9191
/**
92-
* The alt text for the avatar
92+
* The accessible name for the avatar, eg: "Foo's Profile Picture"
9393
*/
94-
avatarAltText: string;
94+
avatarAccessibleName: string;
9595

9696
/**
9797
* String to use for computing the colour of the placeholder avatar if no avatar is set
@@ -121,7 +121,7 @@ export function getFileChanged(e: React.ChangeEvent<HTMLInputElement>): File | n
121121
*/
122122
const AvatarSetting: React.FC<IProps> = ({
123123
avatar,
124-
avatarAltText,
124+
avatarAccessibleName,
125125
onChange,
126126
removeAvatar,
127127
disabled,
@@ -147,9 +147,6 @@ const AvatarSetting: React.FC<IProps> = ({
147147
}
148148
}, [avatar]);
149149

150-
// Prevents ID collisions when this component is used more than once on the same page.
151-
const a11yId = useId();
152-
153150
const onFileChanged = useCallback(
154151
(e: React.ChangeEvent<HTMLInputElement>) => {
155152
const file = getFileChanged(e);
@@ -170,48 +167,47 @@ const AvatarSetting: React.FC<IProps> = ({
170167
setMenuOpen(newOpen);
171168
}, []);
172169

173-
let avatarElement = (
170+
const avatarElement = (
174171
<AccessibleButton
175172
element="div"
176-
onClick={uploadAvatar}
173+
/**
174+
* This button will open a menu. That is done by passing this element as trigger
175+
* to the menu component, hence the empty onClick.
176+
*/
177+
onClick={() => {}}
177178
className="mx_AvatarSetting_avatarPlaceholder mx_AvatarSetting_avatarDisplay"
178-
aria-labelledby={disabled ? undefined : a11yId}
179-
// Inhibit tab stop as we have explicit upload/remove buttons
180-
tabIndex={-1}
181179
disabled={disabled}
182180
>
183-
<BaseAvatar idName={placeholderId} name={placeholderName} size="90px" />
181+
<BaseAvatar
182+
idName={placeholderId}
183+
name={placeholderName}
184+
size="90px"
185+
url={avatarURL}
186+
altText={avatarAccessibleName}
187+
/>
184188
</AccessibleButton>
185189
);
186-
if (avatarURL) {
187-
avatarElement = (
188-
<AccessibleButton
189-
element="img"
190-
className="mx_AvatarSetting_avatarDisplay"
191-
src={avatarURL}
192-
alt={avatarAltText}
193-
onClick={uploadAvatar}
194-
// Inhibit tab stop as we have explicit upload/remove buttons
195-
tabIndex={-1}
196-
disabled={disabled}
197-
/>
198-
);
199-
}
200190

201191
let uploadAvatarBtn: JSX.Element | undefined;
202192
if (!disabled) {
203193
const uploadButtonClasses = classNames("mx_AvatarSetting_uploadButton", {
204194
mx_AvatarSetting_uploadButton_active: menuOpen,
205195
});
206196
uploadAvatarBtn = (
207-
<div className={uploadButtonClasses}>
208-
<EditIcon width="20px" height="20px" />
197+
<div
198+
className={uploadButtonClasses}
199+
role="button"
200+
aria-label={_t("settings|general|avatar_open_menu")}
201+
tabIndex={0}
202+
aria-haspopup="menu"
203+
>
204+
<EditIcon aria-hidden={true} width="20px" height="20px" />
209205
</div>
210206
);
211207
}
212208

213209
const content = (
214-
<div className="mx_AvatarSetting_avatar" role="group" aria-label={avatarAltText}>
210+
<div className="mx_AvatarSetting_avatar" role="group" aria-label={avatarAccessibleName}>
215211
{avatarElement}
216212
{uploadAvatarBtn}
217213
</div>

src/components/views/settings/UserProfileSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
203203
<div className="mx_UserProfileSettings_profile">
204204
<AvatarSetting
205205
avatar={avatarURL ?? undefined}
206-
avatarAltText={_t("common|user_avatar")}
206+
avatarAccessibleName={_t("common|user_avatar")}
207207
onChange={onAvatarChange}
208208
removeAvatar={avatarURL ? onAvatarRemove : undefined}
209209
placeholderName={displayName}

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2664,6 +2664,7 @@
26642664
"allow_spellcheck": "Allow spell check",
26652665
"application_language": "Application language",
26662666
"application_language_reload_hint": "The app will reload after selecting another language",
2667+
"avatar_open_menu": "Open avatar menu",
26672668
"avatar_remove_progress": "Removing image...",
26682669
"avatar_save_progress": "Uploading image...",
26692670
"avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.",

test/unit-tests/components/views/settings/AvatarSetting-test.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,32 @@ describe("<AvatarSetting />", () => {
2525
stubClient();
2626
});
2727

28-
it("renders avatar with specified alt text", async () => {
29-
const { queryByAltText } = render(
28+
it("renders avatar with specified accessible name", async () => {
29+
const { getByRole } = render(
3030
<AvatarSetting
3131
placeholderId="blee"
3232
placeholderName="boo"
33-
avatarAltText="Avatar of Peter Fox"
33+
avatarAccessibleName="Avatar of Peter Fox"
3434
avatar="mxc://example.org/my-avatar"
3535
/>,
3636
);
3737

38-
const imgElement = queryByAltText("Avatar of Peter Fox");
39-
expect(imgElement).toBeInTheDocument();
38+
const avatarButton = getByRole("button", { name: "Avatar of Peter Fox" });
39+
expect(avatarButton).toBeInTheDocument();
4040
});
4141

4242
it("renders a file as the avatar when supplied", async () => {
4343
render(
4444
<AvatarSetting
4545
placeholderId="blee"
4646
placeholderName="boo"
47-
avatarAltText="Avatar of Peter Fox"
47+
avatarAccessibleName="Avatar of Peter Fox"
4848
avatar={AVATAR_FILE}
4949
/>,
5050
);
5151

52-
const imgElement = await screen.findByRole("button", { name: "Avatar of Peter Fox" });
52+
const avatarButton = await screen.findByRole("button", { name: "Avatar of Peter Fox" });
53+
const imgElement = avatarButton.querySelector("img");
5354
expect(imgElement).toBeInTheDocument();
5455
expect(imgElement).toHaveAttribute("src", "data:image/gif;base64," + BASE64_GIF);
5556
});
@@ -63,7 +64,7 @@ describe("<AvatarSetting />", () => {
6364
placeholderId="blee"
6465
placeholderName="boo"
6566
avatar="mxc://example.org/my-avatar"
66-
avatarAltText="Avatar of Peter Fox"
67+
avatarAccessibleName="Avatar of Peter Fox"
6768
onChange={onChange}
6869
/>,
6970
);
@@ -82,7 +83,7 @@ describe("<AvatarSetting />", () => {
8283
placeholderId="blee"
8384
placeholderName="boo"
8485
avatar="mxc://example.org/my-avatar"
85-
avatarAltText="Avatar of Peter Fox"
86+
avatarAccessibleName="Avatar of Peter Fox"
8687
onChange={onChange}
8788
/>,
8889
);
@@ -102,7 +103,7 @@ describe("<AvatarSetting />", () => {
102103
placeholderId="blee"
103104
placeholderName="boo"
104105
avatar="mxc://example.org/my-avatar"
105-
avatarAltText="Avatar of Peter Fox"
106+
avatarAccessibleName="Avatar of Peter Fox"
106107
onChange={onChange}
107108
/>,
108109
);

0 commit comments

Comments
 (0)