Bug 1683053 - Keyboard and Screen Reader access for CC improvements r=ayeddi,kpatenio

Improved keyboard and screen reader accessibility for video closed captions menu by adding ARIA attributes, keyboard arrow navigation, and focus behavior to take the user into and out of the CC menu as it opens and closes.

Differential Revision: https://phabricator.services.mozilla.com/D136280
This commit is contained in:
Bernard Igiri 2022-02-11 17:50:27 +00:00
parent cccd748af9
commit e3dadbf6b0
7 changed files with 222 additions and 17 deletions

View File

@ -7,6 +7,8 @@ support-files =
video.ogg
head.js
tree_shared.js
test-webvtt-1.vtt
test-webvtt-2.vtt
videocontrols_direction-1-ref.html
videocontrols_direction-1a.html
videocontrols_direction-1b.html
@ -53,3 +55,4 @@ skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536347
skip-if = true # Bug 1483656
[test_bug1654500.html]
[test_videocontrols_clickToPlay_ariaLabel.html]
[test_videocontrols_closed_caption_menu.html]

View File

@ -0,0 +1,10 @@
WEBVTT
1
00:00:00.000 --> 00:00:02.000
This is a one line text track
2
00:00:05.000 --> 00:00:07.000
- <b>This is a new text track but bolded</b>
- <i>This line should be italicized<i>

View File

@ -0,0 +1,10 @@
WEBVTT
1
00:00:00.000 --> 00:00:02.000
Voici un text track en une ligne
2
00:00:05.000 --> 00:00:07.000
- <b>Voici un nouveau text track en gras</b>
- <i>Cette ligne devrait être en italiques<i>

View File

@ -0,0 +1,144 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Video controls test - KeyHandler</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<p id="display"></p>
<div id="content">
<video id="video" controls preload="auto">
<track
id="track1"
kind="subtitles"
label="[test] en"
srclang="en"
src="test-webvtt-1.vtt"
/>
<track
id="track2"
kind="subtitles"
label="[test] fr"
srclang="fr"
src="test-webvtt-2.vtt"
/>
</video>
</div>
<pre id="test">
<script class="testbody" type="application/javascript">
SimpleTest.waitForExplicitFinish();
const video = document.getElementById("video");
const closedCaptionButton = getElementWithinVideo(video, "closedCaptionButton");
const fullscreenButton = getElementWithinVideo(video, "fullscreenButton");
const textTrackList = getElementWithinVideo(video, "textTrackList");
const textTrackListContainer = getElementWithinVideo(video, "textTrackListContainer");
function isClosedCaptionVisible() {
return !textTrackListContainer.hidden;
}
// Setup video
tests.push(done => {
SpecialPowers.pushPrefEnv({"set": [
["media.cache_size", 40000],
["media.videocontrols.keyboard-tab-to-all-controls", true],
]}, done);
}, done => {
video.src = "seek_with_sound.ogg";
video.addEventListener("loadedmetadata", done);
}, cleanup);
tests.push(done => {
info("Opening the CC menu should focus the first item in the menu");
info("Focusing and clicking the closed caption button");
closedCaptionButton.focus();
synthesizeKey(" ");
ok(isClosedCaptionVisible(), "The CC menu is visible");
ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus");
done();
});
tests.push(done => {
info("aria-expanded should be reflected whether the CC menu is open or not");
ok(closedCaptionButton.getAttribute("aria-expanded") === "false", "Closed CC menu has aria-expanded set to false");
info("Focusing and clicking the closed caption button");
closedCaptionButton.focus();
synthesizeKey(" ");
ok(isClosedCaptionVisible(), "The CC menu is visible");
ok(closedCaptionButton.getAttribute("aria-expanded") === "true", "Open CC menu has aria-expanded set to true");
done();
});
tests.push(done => {
info("If CC menu is open, then arrow keys should navigate menu");
info("Opening the CC menu");
closedCaptionButton.focus();
synthesizeKey(" ");
ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus first");
info("Pressing down arrow");
synthesizeKey("KEY_ArrowDown");
ok(textTrackList.children[1].matches(":focus"), "The second item in CC menu should now be in focus");
info("Pressing up arrow");
synthesizeKey("KEY_ArrowUp");
ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be back in focus again");
done();
});
tests.push(done => {
info("Escape should close the CC menu");
info("Opening the CC menu");
closedCaptionButton.focus();
synthesizeKey(" ");
info("Pressing Escape key");
synthesizeKey("KEY_Escape");
ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus");
ok(!isClosedCaptionVisible(), "The CC menu should be closed");
done();
});
tests.push(done => {
info("Tabbing away should close the CC menu");
info("Opening the CC menu");
closedCaptionButton.focus();
synthesizeKey(" ");
info("Pressing Tab key 3x");
synthesizeKey("KEY_Tab");
synthesizeKey("KEY_Tab");
synthesizeKey("KEY_Tab");
ok(fullscreenButton.matches(":focus"), "The fullscreen button should be in focus");
ok(!isClosedCaptionVisible(), "The CC menu should be closed");
done();
});
tests.push(done => {
info("Shift + Tabbing away should close the CC menu");
info("Opening the CC menu");
closedCaptionButton.focus();
synthesizeKey(" ");
info("Pressing Shift + Tab key");
synthesizeKey("KEY_Tab", { shiftKey: true });
ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus");
ok(!isClosedCaptionVisible(), "The CC menu should be closed");
done();
});
function cleanup(done) {
if (isClosedCaptionVisible()) {
closedCaptionButton.click();
}
done();
}
// add cleanup after every test
tests = tests.reduce((a, v) => a.concat([v, cleanup]), []);
tests.push(SimpleTest.finish);
window.addEventListener("load", executeTests);
</script>
</pre>
</body>
</html>

View File

@ -75,18 +75,18 @@
await new Promise(SimpleTest.executeSoon);
ok(!ttListContainer.hidden, "Texttrack menu should show up");
ok(ttList.lastChild.hasAttribute("on"), "The last added item should be highlighted");
ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last added item should be highlighted");
});
add_task(async function check_select_texttrack() {
const tt = ttList.children[1];
ok(!tt.hasAttribute("on"), "Item should be off before click");
ok(tt.getAttribute("aria-checked") === "false", "Item should be off before click");
synthesizeMouseAtCenter(tt, {});
await once(video.textTracks, "change");
await new Promise(SimpleTest.executeSoon);
ok(tt.hasAttribute("on"), "Selected item should be enabled");
ok(tt.getAttribute("aria-checked") === "true", "Selected item should be enabled");
ok(ttListContainer.hidden, "Should hide texttrack menu once clicked on an item");
});
@ -103,7 +103,7 @@
await once(video.textTracks, "change");
await new Promise(SimpleTest.executeSoon);
ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled");
ok(ttList.lastChild.hasAttribute("on"), "The last item should be highlighted");
ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last item should be highlighted");
});
</script>

View File

@ -907,6 +907,7 @@ this.VideoControlsImplWidget = class {
case this.textTrackList:
const index = +aEvent.originalTarget.getAttribute("index");
this.changeTextTrack(index);
this.closedCaptionButton.focus();
break;
case this.videocontrols:
// Prevent any click event within media controls from dispatching through to video.
@ -1452,7 +1453,7 @@ this.VideoControlsImplWidget = class {
}
this.startFadeOut(this.controlBar, false);
this.textTrackListContainer.hidden = true;
this.hideClosedCaptionMenu();
this.window.clearTimeout(this._showControlsTimeout);
this._controlsHiddenByTimeout = false;
}
@ -1916,6 +1917,8 @@ this.VideoControlsImplWidget = class {
case "ArrowDown" /* Volume decrease */:
if (allTabbable && target == this.scrubber) {
this.keyboardSeekBack(/* tenPercent */ false);
} else if (target.classList.contains("textTrackItem")) {
target.nextSibling?.focus();
} else {
this.keyboardVolumeDecrease();
}
@ -1923,6 +1926,8 @@ this.VideoControlsImplWidget = class {
case "ArrowUp" /* Volume increase */:
if (allTabbable && target == this.scrubber) {
this.keyboardSeekForward(/* tenPercent */ false);
} else if (target.classList.contains("textTrackItem")) {
target.previousSibling?.focus();
} else {
this.keyboardVolumeIncrease();
}
@ -1962,6 +1967,15 @@ this.VideoControlsImplWidget = class {
this.video.duration || this.maxCurrentTimeSeen / 1000;
}
break;
case "Escape" /* Escape */:
if (
target.classList.contains("textTrackItem") &&
!this.textTrackListContainer.hidden
) {
this.toggleClosedCaption();
this.closedCaptionButton.focus();
}
break;
default:
return;
}
@ -2053,9 +2067,9 @@ this.VideoControlsImplWidget = class {
const idx = +tti.getAttribute("index");
if (idx == this.currentTextTrackIndex) {
tti.setAttribute("on", "true");
tti.setAttribute("aria-checked", "true");
} else {
tti.removeAttribute("on");
tti.setAttribute("aria-checked", "false");
}
}
@ -2087,6 +2101,7 @@ this.VideoControlsImplWidget = class {
ttBtn.classList.add("textTrackItem");
ttBtn.setAttribute("index", tt.index);
ttBtn.setAttribute("role", "menuitemradio");
if (tt.mode === "showing" && tt.index) {
this.changeTextTrack(tt.index);
@ -2108,7 +2123,7 @@ this.VideoControlsImplWidget = class {
},
onControlBarAnimationFinished() {
this.textTrackListContainer.hidden = true;
this.hideClosedCaptionMenu();
this.video.dispatchEvent(
new this.window.CustomEvent("controlbarchange")
);
@ -2121,17 +2136,28 @@ this.VideoControlsImplWidget = class {
);
},
hideClosedCaptionMenu() {
this.textTrackListContainer.hidden = true;
this.closedCaptionButton.setAttribute("aria-expanded", "false");
},
showClosedCaptionMenu() {
this.textTrackListContainer.hidden = false;
this.closedCaptionButton.setAttribute("aria-expanded", "true");
},
toggleClosedCaption() {
if (this.textTrackListContainer.hidden) {
this.textTrackListContainer.hidden = false;
this.showClosedCaptionMenu();
if (this.prefs["media.videocontrols.keyboard-tab-to-all-controls"]) {
// If we're about to hide the controls after focus, prevent that, as
// that will dismiss the CC menu before the user can use it.
this.textTrackList.firstChild.focus();
this.window.clearTimeout(this._hideControlsTimeout);
this._hideControlsTimeout = 0;
}
} else {
this.textTrackListContainer.hidden = true;
this.hideClosedCaptionMenu();
// If the CC menu was shown via the keyboard, we may have prevented
// the controls from hiding. We can now hide them.
if (
@ -2201,6 +2227,16 @@ this.VideoControlsImplWidget = class {
}
this.setClosedCaptionButtonState();
// Hide the Closed Caption menu when the user moves focus
this.hideClosedCaptionMenu = this.hideClosedCaptionMenu.bind(this);
this.closedCaptionButton.addEventListener(
"focus",
this.hideClosedCaptionMenu
);
this.fullscreenButton.addEventListener(
"focus",
this.hideClosedCaptionMenu
);
},
log(msg) {
@ -2787,7 +2823,7 @@ this.VideoControlsImplWidget = class {
<div id="controlsOverlay" class="controlsOverlay stackItem" role="none">
<div class="controlsSpacerStack">
<div id="controlsSpacer" class="controlsSpacer stackItem" role="none"></div>
<button id="clickToPlay" class="clickToPlay" hidden="true"></button>
<button id="clickToPlay" class="clickToPlay" aria-label="&playButton.playLabel;" hidden="true"></button>
</div>
<button id="pictureInPictureToggle" class="pip-wrapper" position="left" hidden="true">
@ -2840,16 +2876,17 @@ this.VideoControlsImplWidget = class {
</div>
<button id="castingButton" class="button castingButton"
aria-label="&castingButton.castingLabel;"/>
<button id="closedCaptionButton" class="button closedCaptionButton"
<button id="closedCaptionButton" class="button closedCaptionButton" aria-controls="textTrackList"
aria-haspopup="menu" aria-expanded="false" data-l10n-id="videocontrols-closed-caption-button"/>
<div id="textTrackListContainer" class="textTrackListContainer" hidden="true" role="presentation">
<div id="textTrackList" role="menu" class="textTrackList" offlabel="&closedCaption.off;"
data-l10n-id="videocontrols-closed-caption-button"/>
</div>
<button id="fullscreenButton"
class="button fullscreenButton"
enterfullscreenlabel="&fullscreenButton.enterfullscreenlabel;"
exitfullscreenlabel="&fullscreenButton.exitfullscreenlabel;"/>
</div>
<div id="textTrackListContainer" class="textTrackListContainer" hidden="true">
<div id="textTrackList" class="textTrackList" offlabel="&closedCaption.off;"></div>
</div>
</div>
</div>
</div>`,

View File

@ -139,7 +139,6 @@
}
.controlBar {
position: relative;
display: flex;
box-sizing: border-box;
justify-content: center;
@ -155,6 +154,8 @@
}
.controlBar > .button {
/* Prevent #textTrackListContainer from blocking clicks on controls */
z-index: 1;
height: 100%;
min-width: var(--button-size);
min-height: var(--button-size);
@ -407,7 +408,7 @@
background-color: #444;
}
.textTrackList > .textTrackItem[on] {
.textTrackList > .textTrackItem[aria-checked="true"] {
color: #48a0f7;
}