зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
6abbe23090
Коммит
8295e41cd9
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче