mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
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:
parent
cccd748af9
commit
e3dadbf6b0
@ -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]
|
||||
|
10
toolkit/content/tests/widgets/test-webvtt-1.vtt
Normal file
10
toolkit/content/tests/widgets/test-webvtt-1.vtt
Normal 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>
|
10
toolkit/content/tests/widgets/test-webvtt-2.vtt
Normal file
10
toolkit/content/tests/widgets/test-webvtt-2.vtt
Normal 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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>`,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user