Skip to content

Commit

Permalink
feat: fix accessibility of the captions setting dialog (#4050)
Browse files Browse the repository at this point in the history
This fixes a lot of the issues from #2746 by making the dialog inherit from our actual ModalDialog which now has tab focus trapping.

The Captions Settings dialog has some accessibility issues:
- Field labels and fields are not explicitly associated
- Keyboard focus does not move into the dialog when it is opened
- Keyboard focus is not trapped inside the dialog while it is open
- Keyboard focus does not return to the control which opened the dialog when it is closed
- The extent (top and bottom) of the dialog is not indicated to screen readers
- The dialog cannot be closed with the Esc key
- The meaning of '---' in the select fields is not clear
- The control to close the dialog is labeled "Done" rather than "Close"
- The purpose of the "Defaults" button may not be obvious, and its effect may not be apparent to screen reader users
- Focus highlighting (outline) on the Default and Done buttons is *very* hard to see
- Pressing the Done button doesn’t seem to have the same effect as pressing the Close (x) button; does it trigger the same focus movement
- This requirement to move it back to the triggering element is tricky, since clicking on that item in the CC menu dismisses the CC menu. I need to think about this a little more - either the menu should open again, or the focus should go to the main CC Menu Button
- The focus outline on the whole dialog goes too far to the left (all the way to the edge of the video window, not just to the edge of the dialog)

Fixes #2746.
  • Loading branch information
gkatsev committed Feb 21, 2017
1 parent 181a19f commit 0d0dea4
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 142 deletions.
239 changes: 152 additions & 87 deletions docs/translations-needed.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
"Casual": "Zwanglos",
"Script": "Schreibeschrift",
"Small Caps": "Small-Caps",
"Defaults": "Standardeinstellungen",
"Done": "Fertig",
"Caption Settings Dialog": "Einstellungsdialog für Untertitel",
"Beginning of dialog window. Escape will cancel and close the window.": "Anfang des Dialogfensters. Esc bricht ab und schließt das Fenster."
Expand Down
6 changes: 4 additions & 2 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@
"Casual": "Casual",
"Script": "Script",
"Small Caps": "Small Caps",
"Defaults": "Defaults",
"Reset": "Reset",
"all settings to the default values": "all settings to the default values",
"Done": "Done",
"Caption Settings Dialog": "Caption Settings Dialog",
"Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window."
"Beginning of dialog window. Escape will cancel and close the window.": "Beginning of dialog window. Escape will cancel and close the window.",
"End of dialog window.": "End of dialog window."
}
1 change: 0 additions & 1 deletion lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
"Casual": "Manuscrite",
"Script": "Scripte",
"Small Caps": "Petites capitales",
"Defaults": "Valeurs par défaut",
"Done": "Terminé",
"Caption Settings Dialog": "Boîte de dialogue des paramètres des sous-titres transcrits",
"Beginning of dialog window. Escape will cancel and close the window.": "Début de la fenêtre de dialogue. La touche d'échappement annulera et fermera la fenêtre."
Expand Down
1 change: 0 additions & 1 deletion lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
"Casual": "Gündelik",
"Script": "El Yazısı",
"Small Caps": "Küçük Boyutlu Büyük Harfli",
"Defaults": "Varsayılan Ayarlar",
"Done": "Tamam",
"Caption Settings Dialog": "Altyazı Ayarları Menüsü",
"Beginning of dialog window. Escape will cancel and close the window.": "Diyalog penceresinin başlangıcı. ESC tuşu işlemi iptal edip pencereyi kapatacaktır."
Expand Down
32 changes: 27 additions & 5 deletions src/css/components/_captions-settings.scss
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
.vjs-caption-settings {
.vjs-modal-dialog.vjs-text-track-settings {
position: relative;
top: 1em;
background-color: $primary-background-color;
background-color: rgba($primary-background-color, 0.75);
color: $primary-foreground-color;
margin: 0 auto;
padding: 0.5em;
height: 16em;
font-size: 12px;
width: 40em;
height: 22em;
width: 48em;
}

.vjs-caption-settings .vjs-tracksettings {
top: 0;
bottom: 1em;
bottom: 0;
left: 0;
right: 0;
position: absolute;
Expand All @@ -36,6 +35,29 @@
right: 1em;
}

.vjs-tracksettings-controls button:focus,
.vjs-tracksettings-controls button:active {
outline-style: solid;
outline-width: medium;
background-image: linear-gradient(0deg, $primary-foreground-color 88%, $secondary-background-color 100%);
}

.vjs-tracksettings-controls button:hover {
color: rgba(#2B333F, 0.75);
}

.vjs-tracksettings-controls button {
background-color: $primary-foreground-color;
background-image: linear-gradient(-180deg, $primary-foreground-color 88%, $secondary-background-color 100%);
color: #2B333F;
cursor: pointer;
border-radius: 2px;
}

.vjs-tracksettings-controls .vjs-default-button {
margin-right: 1em;
}

.vjs-caption-settings .vjs-tracksetting {
margin: 5px;
padding: 3px;
Expand Down
2 changes: 1 addition & 1 deletion src/css/video-js.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@import "components/big-play";
@import "components/button";
@import "components/close-button";
@import "components/modal-dialog";

@import "components/menu/menu";
@import "components/menu/menu-popup";
Expand Down Expand Up @@ -38,6 +39,5 @@
@import "components/audio";
@import "components/adaptive";
@import "components/captions-settings";
@import "components/modal-dialog";

@import "print";
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ class CaptionSettingsMenuItem extends TextTrackMenuItem {
* @listens click
*/
handleClick(event) {
this.player().getChild('textTrackSettings').show();
this.player().getChild('textTrackSettings').el_.focus();
this.player().getChild('textTrackSettings').open();
}

}
Expand Down
2 changes: 1 addition & 1 deletion src/js/modal-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class ModalDialog extends Component {
});

this.descEl_ = Dom.createEl('p', {
className: `${MODAL_CLASS_NAME}-description vjs-offscreen`,
className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
id: this.el().getAttribute('aria-describedby')
});

Expand Down
115 changes: 75 additions & 40 deletions src/js/tracks/text-track-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* @file text-track-settings.js
*/
import window from 'global/window';
import document from 'global/document';
import Component from '../component';
import ModalDialog from '../modal-dialog';
import {createEl} from '../utils/dom';
import * as Fn from '../utils/fn';
import * as Obj from '../utils/obj';
Expand Down Expand Up @@ -236,9 +238,9 @@ function setSelectedOption(el, value, parser) {
/**
* Manipulate Text Tracks settings.
*
* @extends Component
* @extends ModalDialog
*/
class TextTrackSettings extends Component {
class TextTrackSettings extends ModalDialog {

/**
* Creates an instance of this class.
Expand All @@ -250,20 +252,34 @@ class TextTrackSettings extends Component {
* The key/value store of player options.
*/
constructor(player, options) {
options.temporary = false;

super(player, options);
this.setDefaults();
this.hide();

this.contentEl().className += ' vjs-caption-settings';

this.updateDisplay = Fn.bind(this, this.updateDisplay);

// fill the modal and pretend we have opened it
this.fill();
this.hasBeenOpened_ = this.hasBeenFilled_ = true;

this.endDialog = createEl('p', {
className: 'vjs-control-text',
textContent: this.localize('End of dialog window.')
});
this.el().appendChild(this.endDialog);

this.setDefaults();

// Grab `persistTextTrackSettings` from the player options if not passed in child options
if (options.persistTextTrackSettings === undefined) {
this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
}

this.on(this.$('.vjs-done-button'), 'click', () => {
this.saveSettings();
this.hide();
this.close();
});

this.on(this.$('.vjs-default-button'), 'click', () => {
Expand All @@ -290,21 +306,28 @@ class TextTrackSettings extends Component {
* The DOM element that gets created.
* @private
*/
createElSelect_(key) {
createElSelect_(key, legendId = '') {
const config = selectConfigs[key];
const id = config.id.replace('%s', this.id_);

return [
createEl('label', {
id,
className: 'vjs-label',
textContent: this.localize(config.label)
}, {
for: id
}),
createEl('select', {id}, undefined, config.options.map(o => {
createEl('select', {}, {
'aria-labelledby': `${legendId} ${id}`
}, config.options.map(o => {
const optionId = id + '-' + o[1];

return createEl('option', {
id: optionId,
textContent: this.localize(o[1]),
value: o[0]
}, {
'aria-labelledby': `${legendId} ${id} ${optionId}`
});
}))
];
Expand All @@ -320,14 +343,15 @@ class TextTrackSettings extends Component {
*/
createElFgColor_() {
const legend = createEl('legend', {
id: `captions-text-legend-${this.id_}`,
textContent: this.localize('Text')
});

const select = this.createElSelect_('color');
const select = this.createElSelect_('color', legend.id);

const opacity = createEl('span', {
className: 'vjs-text-opacity vjs-opacity'
}, undefined, this.createElSelect_('textOpacity'));
}, undefined, this.createElSelect_('textOpacity', legend.id));

return createEl('fieldset', {
className: 'vjs-fg-color vjs-tracksetting'
Expand All @@ -344,14 +368,15 @@ class TextTrackSettings extends Component {
*/
createElBgColor_() {
const legend = createEl('legend', {
id: `captions-background-${this.id_}`,
textContent: this.localize('Background')
});

const select = this.createElSelect_('backgroundColor');
const select = this.createElSelect_('backgroundColor', legend.id);

const opacity = createEl('span', {
className: 'vjs-bg-opacity vjs-opacity'
}, undefined, this.createElSelect_('backgroundOpacity'));
}, undefined, this.createElSelect_('backgroundOpacity', legend.id));

return createEl('fieldset', {
className: 'vjs-bg-color vjs-tracksetting'
Expand All @@ -368,14 +393,15 @@ class TextTrackSettings extends Component {
*/
createElWinColor_() {
const legend = createEl('legend', {
id: `captions-window-${this.id_}`,
textContent: this.localize('Window')
});

const select = this.createElSelect_('windowColor');
const select = this.createElSelect_('windowColor', legend.id);

const opacity = createEl('span', {
className: 'vjs-window-opacity vjs-opacity'
}, undefined, this.createElSelect_('windowOpacity'));
}, undefined, this.createElSelect_('windowOpacity', legend.id));

return createEl('fieldset', {
className: 'vjs-window-color vjs-tracksetting'
Expand Down Expand Up @@ -435,9 +461,11 @@ class TextTrackSettings extends Component {
* @private
*/
createElControls_() {
const defaultsDescription = this.localize('restore all settings to the default values');
const defaultsButton = createEl('button', {
className: 'vjs-default-button',
textContent: this.localize('Defaults')
title: defaultsDescription,
innerHTML: `${this.localize('Reset')}<span class='vjs-control-text'> ${defaultsDescription}</span>`
});

const doneButton = createEl('button', {
Expand All @@ -457,6 +485,10 @@ class TextTrackSettings extends Component {
* The element that was created.
*/
createEl() {
return super.createEl();
}

content() {
const settings = createEl('div', {
className: 'vjs-tracksettings'
}, undefined, [
Expand All @@ -465,33 +497,19 @@ class TextTrackSettings extends Component {
this.createElControls_()
]);

const heading = createEl('div', {
className: 'vjs-control-text',
id: `TTsettingsDialogLabel-${this.id_}`,
textContent: this.localize('Caption Settings Dialog')
}, {
'aria-level': '1',
'role': 'heading'
});
return settings;
}

const description = createEl('div', {
className: 'vjs-control-text',
id: `TTsettingsDialogDescription-${this.id_}`,
textContent: this.localize('Beginning of dialog window. Escape will cancel and close the window.')
});
label() {
return this.localize('Caption Settings Dialog');
}

const doc = createEl('div', undefined, {
role: 'document'
}, [heading, description, settings]);
description() {
return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
}

return createEl('div', {
className: 'vjs-caption-settings vjs-modal-overlay',
tabIndex: -1
}, {
'role': 'dialog',
'aria-labelledby': heading.id,
'aria-describedby': description.id
}, doc);
buildCSSClass() {
return super.buildCSSClass() + ' vjs-text-track-settings';
}

/**
Expand Down Expand Up @@ -525,7 +543,7 @@ class TextTrackSettings extends Component {
}

/**
* Sets all <select> elements to their default values.
* Sets all `<select>` elements to their default values.
*/
setDefaults() {
Obj.each(selectConfigs, (config) => {
Expand Down Expand Up @@ -584,6 +602,23 @@ class TextTrackSettings extends Component {
}
}

/**
* conditionally blur the element and refocus the captions button
*
* @private
*/
conditionalBlur_() {
this.previouslyActiveEl_ = null;
this.off(document, 'keydown', this.handleKeyDown);

const cb = this.player_.controlBar;
const ccBtn = cb && cb.captionsButton;

if (ccBtn) {
ccBtn.focus();
}
}

}

Component.registerComponent('TextTrackSettings', TextTrackSettings);
Expand Down
2 changes: 1 addition & 1 deletion test/unit/modal-dialog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ QUnit.test('should create the expected description element', function(assert) {
innerHTML: this.modal.description(),
classes: [
'vjs-modal-dialog-description',
'vjs-offscreen'
'vjs-control-text'
],
attrs: {
id: this.el.getAttribute('aria-describedby')
Expand Down

0 comments on commit 0d0dea4

Please sign in to comment.