Bug 1820141 - improve accessibility for PiP subtitles panel r=pip-reviewers,bnasar,fluent-reviewers,flod,mhowell

This patch makes the subtitles settings panel easier to understand when using a screen reader, as well as
easier to navigate when using keyboard. Accessibility improvements include:

- announcing the panel when tabbing to it
- setting focus on the first interactable element when opening the panel
- announcing label for subtitles toggle and group label for font size radio buttons
- setting `aria-expanded` attribute to notify users that the subtitles video control has a collapsable panel

Keyboard improvements include:
- ESC key closes panel when open
- setting focus on the subtitles video control after closing the panel with ESC key

Differential Revision: https://phabricator.services.mozilla.com/D172314
This commit is contained in:
Katherine Patenio 2023-04-13 22:20:46 +00:00
Родитель 6abbe23090
Коммит 8295e41cd9
6 изменённых файлов: 372 добавлений и 33 удалений

Просмотреть файл

@ -273,8 +273,7 @@ let Player = {
}
if (Services.prefs.getBoolPref(CAPTIONS_ENABLED_PREF, false)) {
const closedCaptionButton = document.getElementById("closed-caption");
closedCaptionButton.hidden = false;
this.closedCaptionButton.hidden = false;
}
if (Services.prefs.getBoolPref(IMPROVED_CONTROLS_ENABLED_PREF, false)) {
@ -359,8 +358,19 @@ let Player = {
this.controls.setAttribute("keying", true);
this.showVideoControls();
} else if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
let isSettingsPanelInFocus = this.settingsPanel.contains(
document.activeElement
);
event.preventDefault();
if (this.isFullscreen) {
if (!this.settingsPanel.classList.contains("hide")) {
// If the subtitles settings panel is open, let the ESC key close it
this.toggleSubtitlesSettingsPanel({ forceHide: true });
if (isSettingsPanelInFocus) {
document.getElementById("closed-caption").focus();
}
} else if (this.isFullscreen) {
// We handle the ESC key, in fullscreen modus as intent to leave only the fullscreen mode
document.exitFullscreen();
} else {
@ -633,7 +643,11 @@ let Player = {
}
case "closed-caption": {
this.toggleSubtitlesSettingsPanel();
let options = {};
if (event.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
options.isKeyboard = true;
}
this.toggleSubtitlesSettingsPanel(options);
// Early return to prevent hiding the panel below
return;
}
@ -645,6 +659,21 @@ let Player = {
});
break;
}
case "font-size-selection-radio-small": {
document.getElementById("small").click();
break;
}
case "font-size-selection-radio-medium": {
document.getElementById("medium").click();
break;
}
case "font-size-selection-radio-large": {
document.getElementById("large").click();
break;
}
}
// If the click came from a element that is not inside the subtitles settings panel
// then we want to hide the panel
@ -657,11 +686,14 @@ let Player = {
* Function to toggle the visibility of the subtitles settings panel
* @param {Object} options [optional] Object containing options for the function
* - forceHide: true to force hide the subtitles settings panel
* - isKeyboard: true if the subtitles button was activated using the keyboard
* to show or hide the subtitles settings panel
*/
toggleSubtitlesSettingsPanel(options) {
let settingsPanelVisible = !this.settingsPanel.classList.contains("hide");
if (options?.forceHide || settingsPanelVisible) {
this.settingsPanel.classList.add("hide");
this.closedCaptionButton.setAttribute("aria-expanded", false);
this.controls.removeAttribute("donthide");
if (
@ -679,8 +711,13 @@ let Player = {
});
} else {
this.settingsPanel.classList.remove("hide");
this.closedCaptionButton.setAttribute("aria-expanded", true);
this.controls.setAttribute("donthide", true);
this.showVideoControls();
if (options?.isKeyboard) {
document.querySelector("#subtitles-toggle").focus();
}
}
},
@ -999,8 +1036,7 @@ let Player = {
},
enableSubtitlesButton() {
let closedCaptionButton = document.getElementById("closed-caption");
closedCaptionButton.disabled = false;
this.closedCaptionButton.disabled = false;
this.alignEndControlsButtonTooltips();
this.captionsToggleEnabled = true;
@ -1013,8 +1049,7 @@ let Player = {
},
disableSubtitlesButton() {
let closedCaptionButton = document.getElementById("closed-caption");
closedCaptionButton.disabled = true;
this.closedCaptionButton.disabled = true;
this.alignEndControlsButtonTooltips();
},
@ -1085,6 +1120,13 @@ let Player = {
return (this.seekForward = document.getElementById("seekForward"));
},
get closedCaptionButton() {
delete this.closedCaptionButton;
return (this.closedCaptionButton = document.getElementById(
"closed-caption"
));
},
get settingsPanel() {
delete this.settingsPanel;
return (this.settingsPanel = document.getElementById("settings"));

Просмотреть файл

@ -79,31 +79,32 @@
<button id="audio" class="control-item control-button tooltip-over-controls center-tooltip" hidden="true" data-l10n-attrs="tooltip" tabindex="3"/>
<button id="closed-caption" class="control-item control-button tooltip-over-controls center-tooltip closed-caption" hidden="true" disabled="true" data-l10n-id="pictureinpicture-subtitles-btn" data-l10n-attrs="tooltip" tabindex="4"></button>
<div id="settings" class="hide panel">
<div class="box">
<fieldset class="box panel-fieldset">
<legend class="a11y-only panel-legend" data-l10n-id="pictureinpicture-subtitles-panel-accessible"></legend>
<div class="subtitle-grid">
<label data-l10n-id="pictureinpicture-subtitles-label" class="bold"></label>
<label id="subtitles-toggle-label" data-l10n-id="pictureinpicture-subtitles-label" class="bold" for="subtitles-toggle"></label>
<label class="switch">
<input id="subtitles-toggle" type="checkbox" tabindex="5" checked=""/>
<span class="slider" role="presentation"></span>
</label>
</div>
<div class="grey-line"></div>
<div class="font-size-selection">
<label data-l10n-id="pictureinpicture-font-size-label" class="bold"></label>
<label>
<fieldset class="font-size-selection panel-fieldset">
<legend data-l10n-id="pictureinpicture-font-size-label" class="bold panel-legend"></legend>
<div id="font-size-selection-radio-small" class="font-size-selection-radio">
<input id="small" type="radio" name="cc-size" tabindex="6"/>
<span data-l10n-id="pictureinpicture-font-size-small"></span>
</label>
<label>
<label data-l10n-id="pictureinpicture-font-size-small" for="small"></label>
</div>
<div id="font-size-selection-radio-medium" class="font-size-selection-radio">
<input id="medium" type="radio" name="cc-size" tabindex="6"/>
<span data-l10n-id="pictureinpicture-font-size-medium"></span>
</label>
<label>
<label data-l10n-id="pictureinpicture-font-size-medium" for="medium"></label>
</div>
<div id="font-size-selection-radio-large" class="font-size-selection-radio">
<input id="large" type="radio" name="cc-size" tabindex="6"/>
<span data-l10n-id="pictureinpicture-font-size-large"></span>
</label>
</div>
</div>
<label data-l10n-id="pictureinpicture-font-size-large" for="large"></label>
</div>
</fieldset>
</fieldset>
<div class="arrow"></div>
</div>
<button id="fullscreen" class="control-item control-button tooltip-over-controls inline-end-tooltip" hidden="true" data-l10n-id="pictureinpicture-fullscreen-btn" data-l10n-attrs="tooltip" tabindex="7"></button>

Просмотреть файл

@ -101,6 +101,7 @@ skip-if = os == "win" && bits == 64 && debug # Bug 1683002
[browser_smallVideoLayout.js]
skip-if = os == "win" && bits == 64 && debug # Bug 1683002
[browser_stripVideoStyles.js]
[browser_subtitles_settings_panel.js]
[browser_tabIconOverlayPiP.js]
[browser_telemetry_enhancements.js]
[browser_text_tracks_webvtt_1.js]

Просмотреть файл

@ -0,0 +1,263 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that pressing the Escape key will close the subtitles settings panel and
* not remove focus if activated via the mouse.
*/
add_task(async function test_closePanelESCMouseFocus() {
clearSavedPosition();
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
[
"media.videocontrols.picture-in-picture.display-text-tracks.size",
"medium",
],
],
});
let videoID = "with-controls";
await ensureVideosReady(browser);
await prepareVideosAndWebVTTTracks(browser, videoID);
await SpecialPowers.spawn(browser, [videoID], async videoID => {
await content.document.getElementById(videoID).play();
});
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
// Resize PiP window so that subtitles button is visible
let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
pipWin.resizeTo(640, 360);
await resizePromise;
let subtitlesButton = pipWin.document.getElementById("closed-caption");
Assert.ok(subtitlesButton, "Subtitles button found");
let subtitlesPanel = pipWin.document.getElementById("settings");
EventUtils.synthesizeMouseAtCenter(subtitlesButton, {}, pipWin);
Assert.ok(
BrowserTestUtils.is_visible(subtitlesPanel),
"Subtitles panel is visible"
);
let audioButton = pipWin.document.getElementById("audio");
audioButton.focus();
EventUtils.synthesizeKey("KEY_Escape", {}, pipWin);
info("Make sure subtitles settings panel closes after pressing ESC");
Assert.ok(
BrowserTestUtils.is_hidden(subtitlesPanel),
"Subtitles panel is hidden"
);
Assert.notEqual(
pipWin.document.activeElement,
subtitlesButton,
"Subtitles button does not have focus after closing panel"
);
Assert.ok(pipWin, "PiP window is still open");
clearSavedPosition();
}
);
});
/**
* Tests that pressing the Escape key will close the subtitles settings panel and
* refocus on the subtitles button if activated via the keyboard.
*/
add_task(async function test_closePanelESCKeyboardFocus() {
clearSavedPosition();
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await ensureVideosReady(browser);
await prepareVideosAndWebVTTTracks(browser, videoID);
await SpecialPowers.spawn(browser, [videoID], async videoID => {
await content.document.getElementById(videoID).play();
});
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
// Resize PiP window so that subtitles button is visible
let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
pipWin.resizeTo(640, 360);
await resizePromise;
let subtitlesButton = pipWin.document.getElementById("closed-caption");
Assert.ok(subtitlesButton, "Subtitles button found");
let subtitlesPanel = pipWin.document.getElementById("settings");
let subtitlesToggle = pipWin.document.getElementById("subtitles-toggle");
subtitlesButton.focus();
EventUtils.synthesizeKey(" ", {}, pipWin);
Assert.ok(
BrowserTestUtils.is_visible(subtitlesPanel),
"Subtitles panel is visible"
);
Assert.equal(
pipWin.document.activeElement,
subtitlesToggle,
"Subtitles switch toggle should have focus after opening panel"
);
EventUtils.synthesizeKey("KEY_Escape", {}, pipWin);
info("Make sure subtitles settings panel closes after pressing ESC");
Assert.ok(
BrowserTestUtils.is_hidden(subtitlesPanel),
"Subtitles panel is hidden"
);
Assert.equal(
pipWin.document.activeElement,
subtitlesButton,
"Subtitles button has focus after closing panel"
);
Assert.ok(pipWin, "PiP window is still open");
clearSavedPosition();
}
);
});
/**
* Tests keyboard navigation for the subtitles settings panel and that it closes after selecting
* the subtitles button.
*/
add_task(async function test_panelKeyboardButtons() {
clearSavedPosition();
await BrowserTestUtils.withNewTab(
{
url: TEST_PAGE_WITH_WEBVTT,
gBrowser,
},
async browser => {
await SpecialPowers.pushPrefEnv({
set: [
[
"media.videocontrols.picture-in-picture.display-text-tracks.enabled",
true,
],
],
});
let videoID = "with-controls";
await ensureVideosReady(browser);
await prepareVideosAndWebVTTTracks(browser, videoID);
await SpecialPowers.spawn(browser, [videoID], async videoID => {
await content.document.getElementById(videoID).play();
// Mute video
content.document.getElementById(videoID).muted = true;
});
let pipWin = await triggerPictureInPicture(browser, videoID);
ok(pipWin, "Got Picture-in-Picture window.");
// Resize PiP window so that subtitles button is visible
let resizePromise = BrowserTestUtils.waitForEvent(pipWin, "resize");
pipWin.resizeTo(640, 360);
await resizePromise;
let subtitlesButton = pipWin.document.getElementById("closed-caption");
Assert.ok(subtitlesButton, "Subtitles button found");
let subtitlesPanel = pipWin.document.getElementById("settings");
let subtitlesToggle = pipWin.document.getElementById("subtitles-toggle");
subtitlesButton.focus();
EventUtils.synthesizeKey(" ", {}, pipWin);
Assert.ok(
BrowserTestUtils.is_visible(subtitlesPanel),
"Subtitles panel is visible"
);
Assert.equal(
pipWin.document.activeElement,
subtitlesToggle,
"Subtitles switch toggle should have focus after opening panel"
);
let fontMediumRadio = pipWin.document.getElementById("medium");
EventUtils.synthesizeKey("KEY_Tab", {}, pipWin);
Assert.equal(
pipWin.document.activeElement,
fontMediumRadio,
"Medium font size radio button should have focus"
);
let fontSmallRadio = pipWin.document.getElementById("small");
EventUtils.synthesizeKey("KEY_ArrowUp", {}, pipWin);
Assert.equal(
pipWin.document.activeElement,
fontSmallRadio,
"Small font size radio button should have focus"
);
Assert.ok(isVideoMuted(browser, videoID), "Video should still be muted");
Assert.equal(
SpecialPowers.getCharPref(
"media.videocontrols.picture-in-picture.display-text-tracks.size"
),
"small",
"Font size changed to small"
);
subtitlesButton.focus();
EventUtils.synthesizeKey(" ", {}, pipWin);
info(
"Make sure subtitles settings panel closes after pressing the subtitles button"
);
Assert.ok(
BrowserTestUtils.is_hidden(subtitlesPanel),
"Subtitles panel is hidden"
);
Assert.ok(pipWin, "PiP window is still open");
clearSavedPosition();
}
);
});

Просмотреть файл

@ -51,6 +51,11 @@ pictureinpicture-seekforward-btn =
.aria-label = Forward
.tooltip = Forward (→)
# This string is never displayed on the window. Is intended to be announced by
# a screen reader whenever a user opens the subtitles settings panel
# after selecting the subtitles button.
pictureinpicture-subtitles-panel-accessible = Subtitles settings
pictureinpicture-subtitles-label = Subtitles
pictureinpicture-font-size-label = Font size

Просмотреть файл

@ -364,6 +364,11 @@ body:not(:fullscreen) #fullscreen {
direction: rtl;
}
.a11y-only {
visibility: hidden;
position: absolute;
}
.hide {
display: none;
}
@ -378,13 +383,15 @@ body:not(:fullscreen) #fullscreen {
}
.box label:not(.switch) {
display: flex;
align-items: center;
color: white;
padding: 8px;
font-family: sans-serif;
}
#subtitles-toggle-label {
width: fit-content;
padding: 8px;
}
.panel {
position: absolute;
bottom: 40px;
@ -392,6 +399,19 @@ body:not(:fullscreen) #fullscreen {
right: 24px;
}
.panel-fieldset {
border: none;
margin-top: 8px;
padding-inline-start: 0;
}
.panel-legend {
font-family: sans-serif;
color: white;
margin-top: 8px;
padding-inline-start: 0;
}
.arrow {
border: 10px solid transparent;
border-top-color: #2B2A33;
@ -401,22 +421,29 @@ body:not(:fullscreen) #fullscreen {
position: relative;
}
.font-size-selection {
padding: 8px;
}
.grey-line {
width: 100%;
height: 1px;
background: #8F8F9D;
}
.font-size-selection > label {
.font-size-selection {
margin-inline-start: 8px;
padding-inline-start: 8px;
}
.font-size-selection-radio {
display: flex;
width: fit-content;
cursor: pointer;
padding-block: 8px;
}
.font-size-selection-radio label {
cursor: pointer;
}
.font-size-selection > label > input[type="radio"] {
.font-size-selection-radio > input[type="radio"] {
appearance: none;
width: 16px;
height: 16px;
@ -426,7 +453,7 @@ body:not(:fullscreen) #fullscreen {
margin-inline-end: 6px;
}
.font-size-selection > label > input[type="radio"]:checked {
.font-size-selection-radio > input[type="radio"]:checked {
border: 4px solid #00ddff;
}