Merge pull request #3810 from element-hq/robin/button-accessibility
Improve accessibility of microphone, camera, and screen share buttons
This commit is contained in:
@@ -22,8 +22,8 @@ test("Start a new call then leave and show the feedback screen", async ({
|
|||||||
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
await expect(page.getByTestId("lobby_joinCall")).toBeVisible();
|
||||||
|
|
||||||
// Check the button toolbar
|
// Check the button toolbar
|
||||||
// await expect(page.getByRole('button', { name: 'Mute microphone' })).toBeVisible();
|
// await expect(page.getByRole('switch', { name: 'Mute microphone' })).toBeVisible();
|
||||||
// await expect(page.getByRole('button', { name: 'Stop video' })).toBeVisible();
|
// await expect(page.getByRole('switch', { name: 'Stop video' })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Settings" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "End call" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "End call" })).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ test("can only interact with header and footer while reconnecting", async ({
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Tab order should jump directly from header to footer, skipping media tiles
|
// Tab order should jump directly from header to footer, skipping media tiles
|
||||||
await page.getByRole("button", { name: "Mute microphone" }).focus();
|
await page.getByRole("switch", { name: "Mute microphone" }).focus();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("button", { name: "Mute microphone" }),
|
page.getByRole("switch", { name: "Mute microphone" }),
|
||||||
).toBeFocused();
|
).toBeFocused();
|
||||||
await page.keyboard.press("Tab");
|
await page.keyboard.press("Tab");
|
||||||
await expect(page.getByRole("button", { name: "Stop video" })).toBeFocused();
|
await expect(page.getByRole("switch", { name: "Stop video" })).toBeFocused();
|
||||||
// Most critically, we should be able to press the hangup button
|
// Most critically, we should be able to press the hangup button
|
||||||
await page.getByRole("button", { name: "End call" }).click();
|
await page.getByRole("button", { name: "End call" }).click();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,13 +55,10 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => {
|
|||||||
const frame = user.page
|
const frame = user.page
|
||||||
.locator('iframe[title="Element Call"]')
|
.locator('iframe[title="Element Call"]')
|
||||||
.contentFrame();
|
.contentFrame();
|
||||||
|
|
||||||
// No lobby, should start with video on
|
// No lobby, should start with video on
|
||||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
await expect(
|
||||||
const videoButton = frame.getByTestId("incall_videomute");
|
frame.getByRole("switch", { name: "Stop video", checked: true }),
|
||||||
await expect(videoButton).toBeVisible();
|
).toBeVisible();
|
||||||
// video should be on
|
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should see 5 video tiles everywhere now
|
// We should see 5 video tiles everywhere now
|
||||||
@@ -101,13 +98,15 @@ widgetTest("Create and join a group call", async ({ addUser, browserName }) => {
|
|||||||
const florianFrame = florian.page
|
const florianFrame = florian.page
|
||||||
.locator('iframe[title="Element Call"]')
|
.locator('iframe[title="Element Call"]')
|
||||||
.contentFrame();
|
.contentFrame();
|
||||||
const florianMuteButton = florianFrame.getByTestId("incall_videomute");
|
const florianVideoButton = florianFrame.getByRole("switch", {
|
||||||
await florianMuteButton.click();
|
name: /video/,
|
||||||
|
});
|
||||||
|
await expect(florianVideoButton).toHaveAccessibleName("Stop video");
|
||||||
|
await expect(florianVideoButton).toBeChecked();
|
||||||
|
await florianVideoButton.click();
|
||||||
// Now the button should indicate we can start video
|
// Now the button should indicate we can start video
|
||||||
await expect(florianMuteButton).toHaveAttribute(
|
await expect(florianVideoButton).toHaveAccessibleName("Start video");
|
||||||
"aria-label",
|
await expect(florianVideoButton).not.toBeChecked();
|
||||||
/^Start video$/,
|
|
||||||
);
|
|
||||||
|
|
||||||
// wait a bit for the state to propagate
|
// wait a bit for the state to propagate
|
||||||
await valere.page.waitForTimeout(3000);
|
await valere.page.waitForTimeout(3000);
|
||||||
|
|||||||
@@ -47,14 +47,17 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// Check for a bug where the video had the wrong fit in PIP
|
// Check for a bug where the video had the wrong fit in PIP
|
||||||
const hangupBtn = iFrame.getByRole("button", { name: "End call" });
|
const audioBtn = iFrame.getByRole("switch", { name: /microphone/ });
|
||||||
const audioBtn = iFrame.getByTestId("incall_mute");
|
const videoBtn = iFrame.getByRole("switch", { name: /video/ });
|
||||||
const videoBtn = iFrame.getByTestId("incall_videomute");
|
await expect(
|
||||||
await expect(hangupBtn).toBeVisible();
|
iFrame.getByRole("button", { name: "End call" }),
|
||||||
|
).toBeVisible();
|
||||||
await expect(audioBtn).toBeVisible();
|
await expect(audioBtn).toBeVisible();
|
||||||
await expect(videoBtn).toBeVisible();
|
await expect(videoBtn).toBeVisible();
|
||||||
await expect(audioBtn).toHaveAttribute("aria-label", /^Mute microphone$/);
|
await expect(audioBtn).toHaveAccessibleName("Mute microphone");
|
||||||
await expect(videoBtn).toHaveAttribute("aria-label", /^Stop video$/);
|
await expect(audioBtn).toBeChecked();
|
||||||
|
await expect(videoBtn).toHaveAccessibleName("Stop video");
|
||||||
|
await expect(videoBtn).toBeChecked();
|
||||||
|
|
||||||
await videoBtn.click();
|
await videoBtn.click();
|
||||||
await audioBtn.click();
|
await audioBtn.click();
|
||||||
@@ -62,7 +65,9 @@ widgetTest("Footer interaction in PiP", async ({ addUser, browserName }) => {
|
|||||||
// stop hovering on any of the buttons
|
// stop hovering on any of the buttons
|
||||||
await iFrame.getByTestId("videoTile").hover();
|
await iFrame.getByTestId("videoTile").hover();
|
||||||
|
|
||||||
await expect(audioBtn).toHaveAttribute("aria-label", /^Unmute microphone$/);
|
await expect(audioBtn).toHaveAccessibleName("Unmute microphone");
|
||||||
await expect(videoBtn).toHaveAttribute("aria-label", /^Start video$/);
|
await expect(audioBtn).toBeChecked();
|
||||||
|
await expect(videoBtn).toHaveAccessibleName("Start video");
|
||||||
|
await expect(videoBtn).not.toBeChecked();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,16 +40,14 @@ widgetTest("Put call in PIP", async ({ addUser, browserName }) => {
|
|||||||
|
|
||||||
await TestHelpers.joinCallInCurrentRoom(timo.page);
|
await TestHelpers.joinCallInCurrentRoom(timo.page);
|
||||||
|
|
||||||
{
|
|
||||||
const frame = timo.page
|
const frame = timo.page
|
||||||
.locator('iframe[title="Element Call"]')
|
.locator('iframe[title="Element Call"]')
|
||||||
.contentFrame();
|
.contentFrame();
|
||||||
|
|
||||||
const videoButton = frame.getByTestId("incall_videomute");
|
|
||||||
await expect(videoButton).toBeVisible();
|
|
||||||
// check that the video is on
|
// check that the video is on
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
|
await expect(
|
||||||
}
|
frame.getByRole("switch", { name: "Stop video", checked: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// Switch to the other room, the call should go to PIP
|
// Switch to the other room, the call should go to PIP
|
||||||
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
|
await TestHelpers.switchToRoomNamed(valere.page, "DoubleTask");
|
||||||
|
|||||||
@@ -54,34 +54,36 @@ widgetTest(
|
|||||||
.contentFrame();
|
.contentFrame();
|
||||||
|
|
||||||
// ASSERT the button states for whistler (the callee)
|
// ASSERT the button states for whistler (the callee)
|
||||||
{
|
|
||||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
|
||||||
const videoButton = whistlerFrame.getByTestId("incall_videomute");
|
|
||||||
// video should be off by default in a voice call
|
// video should be off by default in a voice call
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/);
|
await expect(
|
||||||
|
whistlerFrame.getByRole("switch", {
|
||||||
const audioButton = whistlerFrame.getByTestId("incall_mute");
|
name: "Start video",
|
||||||
|
checked: false,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
// audio should be on for the voice call
|
// audio should be on for the voice call
|
||||||
await expect(audioButton).toHaveAttribute(
|
await expect(
|
||||||
"aria-label",
|
whistlerFrame.getByRole("switch", {
|
||||||
/^Mute microphone$/,
|
name: "Mute microphone",
|
||||||
);
|
checked: true,
|
||||||
}
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// ASSERT the button states for brools (the caller)
|
// ASSERT the button states for brools (the caller)
|
||||||
{
|
|
||||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
|
||||||
const videoButton = brooksFrame.getByTestId("incall_videomute");
|
|
||||||
// video should be off by default in a voice call
|
// video should be off by default in a voice call
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/);
|
await expect(
|
||||||
|
whistlerFrame.getByRole("switch", {
|
||||||
const audioButton = brooksFrame.getByTestId("incall_mute");
|
name: "Start video",
|
||||||
|
checked: false,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
// audio should be on for the voice call
|
// audio should be on for the voice call
|
||||||
await expect(audioButton).toHaveAttribute(
|
await expect(
|
||||||
"aria-label",
|
whistlerFrame.getByRole("switch", {
|
||||||
/^Mute microphone$/,
|
name: "Mute microphone",
|
||||||
);
|
checked: true,
|
||||||
}
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
||||||
// So first we need to confirm that it is hidden when in the call.
|
// So first we need to confirm that it is hidden when in the call.
|
||||||
@@ -93,10 +95,7 @@ widgetTest(
|
|||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
// ASSERT hanging up on one side ends the call for both
|
// ASSERT hanging up on one side ends the call for both
|
||||||
{
|
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||||
const hangupButton = brooksFrame.getByTestId("incall_leave");
|
|
||||||
await hangupButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The widget should be closed on both sides and the timeline should be back on screen
|
// The widget should be closed on both sides and the timeline should be back on screen
|
||||||
await expect(
|
await expect(
|
||||||
@@ -148,34 +147,30 @@ widgetTest(
|
|||||||
.contentFrame();
|
.contentFrame();
|
||||||
|
|
||||||
// ASSERT the button states for whistler (the callee)
|
// ASSERT the button states for whistler (the callee)
|
||||||
{
|
// video should be off by default in a video call
|
||||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
await expect(
|
||||||
const videoButton = whistlerFrame.getByTestId("incall_videomute");
|
whistlerFrame.getByRole("switch", { name: "Stop video", checked: true }),
|
||||||
// video should be on by default in a voice call
|
).toBeVisible();
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
|
// audio should be on too
|
||||||
|
await expect(
|
||||||
const audioButton = whistlerFrame.getByTestId("incall_mute");
|
whistlerFrame.getByRole("switch", {
|
||||||
// audio should be on for the voice call
|
name: "Mute microphone",
|
||||||
await expect(audioButton).toHaveAttribute(
|
checked: true,
|
||||||
"aria-label",
|
}),
|
||||||
/^Mute microphone$/,
|
).toBeVisible();
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASSERT the button states for brools (the caller)
|
// ASSERT the button states for brools (the caller)
|
||||||
{
|
// video should be off by default in a video call
|
||||||
// The only way to know if it is muted or not is to look at the data-kind attribute..
|
await expect(
|
||||||
const videoButton = brooksFrame.getByTestId("incall_videomute");
|
whistlerFrame.getByRole("switch", { name: "Stop video", checked: true }),
|
||||||
// video should be on by default in a voice call
|
).toBeVisible();
|
||||||
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
|
// audio should be on too
|
||||||
|
await expect(
|
||||||
const audioButton = brooksFrame.getByTestId("incall_mute");
|
whistlerFrame.getByRole("switch", {
|
||||||
// audio should be on for the voice call
|
name: "Mute microphone",
|
||||||
await expect(audioButton).toHaveAttribute(
|
checked: true,
|
||||||
"aria-label",
|
}),
|
||||||
/^Mute microphone$/,
|
).toBeVisible();
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
|
||||||
// So first we need to confirm that it is hidden when in the call.
|
// So first we need to confirm that it is hidden when in the call.
|
||||||
@@ -187,10 +182,7 @@ widgetTest(
|
|||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
// ASSERT hanging up on one side ends the call for both
|
// ASSERT hanging up on one side ends the call for both
|
||||||
{
|
await brooksFrame.getByRole("button", { name: "End call" }).click();
|
||||||
const hangupButton = brooksFrame.getByTestId("incall_leave");
|
|
||||||
await hangupButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The widget should be closed on both sides and the timeline should be back on screen
|
// The widget should be closed on both sides and the timeline should be back on screen
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ export const MicButton: FC<MicButtonProps> = ({ enabled, ...props }) => {
|
|||||||
<Tooltip label={label}>
|
<Tooltip label={label}>
|
||||||
<CpdButton
|
<CpdButton
|
||||||
iconOnly
|
iconOnly
|
||||||
aria-label={label}
|
|
||||||
Icon={Icon}
|
Icon={Icon}
|
||||||
kind={enabled ? "primary" : "secondary"}
|
kind={enabled ? "primary" : "secondary"}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={enabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -62,9 +63,10 @@ export const VideoButton: FC<VideoButtonProps> = ({ enabled, ...props }) => {
|
|||||||
<Tooltip label={label}>
|
<Tooltip label={label}>
|
||||||
<CpdButton
|
<CpdButton
|
||||||
iconOnly
|
iconOnly
|
||||||
aria-label={label}
|
|
||||||
Icon={Icon}
|
Icon={Icon}
|
||||||
kind={enabled ? "primary" : "secondary"}
|
kind={enabled ? "primary" : "secondary"}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={enabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -91,6 +93,8 @@ export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
|
|||||||
iconOnly
|
iconOnly
|
||||||
Icon={ShareScreenSolidIcon}
|
Icon={ShareScreenSolidIcon}
|
||||||
kind={enabled ? "primary" : "secondary"}
|
kind={enabled ? "primary" : "secondary"}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={enabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -112,7 +116,6 @@ export const EndCallButton: FC<EndCallButtonProps> = ({
|
|||||||
<CpdButton
|
<CpdButton
|
||||||
className={classNames(className, styles.endCall)}
|
className={classNames(className, styles.endCall)}
|
||||||
iconOnly
|
iconOnly
|
||||||
aria-label={t("hangup_button_label")}
|
|
||||||
Icon={EndCallIcon}
|
Icon={EndCallIcon}
|
||||||
destructive
|
destructive
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -272,14 +272,14 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
class="buttons"
|
class="buttons"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
aria-checked="false"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Unmute microphone"
|
|
||||||
aria-labelledby="_r_8_"
|
aria-labelledby="_r_8_"
|
||||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="incall_mute"
|
data-testid="incall_mute"
|
||||||
role="button"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -296,14 +296,14 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
aria-checked="false"
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="Start video"
|
|
||||||
aria-labelledby="_r_d_"
|
aria-labelledby="_r_d_"
|
||||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||||
data-kind="secondary"
|
data-kind="secondary"
|
||||||
data-size="lg"
|
data-size="lg"
|
||||||
data-testid="incall_videomute"
|
data-testid="incall_videomute"
|
||||||
role="button"
|
role="switch"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -341,7 +341,6 @@ exports[`InCallView > rendering > renders 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="End call"
|
|
||||||
aria-labelledby="_r_n_"
|
aria-labelledby="_r_n_"
|
||||||
class="_button_13vu4_8 endCall _has-icon_13vu4_60 _icon-only_13vu4_53 _destructive_13vu4_110"
|
class="_button_13vu4_8 endCall _has-icon_13vu4_60 _icon-only_13vu4_53 _destructive_13vu4_110"
|
||||||
data-kind="primary"
|
data-kind="primary"
|
||||||
|
|||||||
Reference in New Issue
Block a user