Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5807c64

Browse files
Improve avatar settings accessibility (#9985)
Co-authored-by: Germain <germain@souquet.com>
1 parent 406edfc commit 5807c64

File tree

2 files changed

+73
-9
lines changed

2 files changed

+73
-9
lines changed

src/components/views/settings/AvatarSetting.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useState } from "react";
17+
import React, { useRef, useState } from "react";
1818
import classNames from "classnames";
1919

2020
import { _t } from "../../../languageHandler";
21-
import AccessibleButton from "../elements/AccessibleButton";
21+
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
2222

2323
interface IProps {
2424
avatarUrl?: string;
2525
avatarName: string; // name of user/room the avatar belongs to
26-
uploadAvatar?: (e: React.MouseEvent) => void;
27-
removeAvatar?: (e: React.MouseEvent) => void;
26+
uploadAvatar?: (e: ButtonEvent) => void;
27+
removeAvatar?: (e: ButtonEvent) => void;
2828
avatarAltText: string;
2929
}
3030

@@ -34,12 +34,16 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
3434
onMouseEnter: () => setIsHovering(true),
3535
onMouseLeave: () => setIsHovering(false),
3636
};
37+
// TODO: Use useId() as soon as we're using React 18.
38+
// Prevents ID collisions when this component is used more than once on the same page.
39+
const a11yId = useRef(`hover-text-${Math.random()}`);
3740

3841
let avatarElement = (
3942
<AccessibleButton
4043
element="div"
41-
onClick={uploadAvatar}
44+
onClick={uploadAvatar ?? null}
4245
className="mx_AvatarSetting_avatarPlaceholder"
46+
aria-labelledby={a11yId.current}
4347
{...hoveringProps}
4448
/>
4549
);
@@ -50,7 +54,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
5054
src={avatarUrl}
5155
alt={avatarAltText}
5256
aria-label={avatarAltText}
53-
onClick={uploadAvatar}
57+
onClick={uploadAvatar ?? null}
5458
{...hoveringProps}
5559
/>
5660
);
@@ -60,7 +64,12 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
6064
if (uploadAvatar) {
6165
// insert an empty div to be the host for a css mask containing the upload.svg
6266
uploadAvatarBtn = (
63-
<AccessibleButton onClick={uploadAvatar} className="mx_AvatarSetting_uploadButton" {...hoveringProps} />
67+
<AccessibleButton
68+
onClick={uploadAvatar}
69+
className="mx_AvatarSetting_uploadButton"
70+
aria-labelledby={a11yId.current}
71+
{...hoveringProps}
72+
/>
6473
);
6574
}
6675

@@ -80,9 +89,9 @@ const AvatarSetting: React.FC<IProps> = ({ avatarUrl, avatarAltText, avatarName,
8089
return (
8190
<div className={avatarClasses}>
8291
{avatarElement}
83-
<div className="mx_AvatarSetting_hover">
92+
<div className="mx_AvatarSetting_hover" aria-hidden="true">
8493
<div className="mx_AvatarSetting_hoverBg" />
85-
<span>{_t("Upload")}</span>
94+
<span id={a11yId.current}>{_t("Upload")}</span>
8695
</div>
8796
{uploadAvatarBtn}
8897
{removeAvatarBtn}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
import React from "react";
17+
import { render } from "@testing-library/react";
18+
19+
import AvatarSetting from "../../../../src/components/views/settings/AvatarSetting";
20+
21+
describe("<AvatarSetting />", () => {
22+
it("renders avatar with specified alt text", async () => {
23+
const { queryByAltText } = render(
24+
<AvatarSetting
25+
avatarName="Peter Fox"
26+
avatarAltText="Avatar of Peter Fox"
27+
avatarUrl="https://avatar.fictional/my-avatar"
28+
/>,
29+
);
30+
31+
const imgElement = queryByAltText("Avatar of Peter Fox");
32+
expect(imgElement).toBeInTheDocument();
33+
});
34+
35+
it("renders avatar with remove button", async () => {
36+
const { queryByText } = render(
37+
<AvatarSetting
38+
avatarName="Peter Fox"
39+
avatarAltText="Avatar of Peter Fox"
40+
avatarUrl="https://avatar.fictional/my-avatar"
41+
removeAvatar={jest.fn()}
42+
/>,
43+
);
44+
45+
const removeButton = queryByText("Remove");
46+
expect(removeButton).toBeInTheDocument();
47+
});
48+
49+
it("renders avatar without remove button", async () => {
50+
const { queryByText } = render(<AvatarSetting avatarName="Peter Fox" avatarAltText="Avatar of Peter Fox" />);
51+
52+
const removeButton = queryByText("Remove");
53+
expect(removeButton).toBeNull();
54+
});
55+
});

0 commit comments

Comments
 (0)