Commit 8249f33c by Ari Rizzitano Committed by GitHub

Merge pull request #14744 from edx/ari/video-transcript-focus

video a11y improvements (TNL-6361, AC-587)
parents a8f5d4cb 3cb0fd41
...@@ -986,9 +986,11 @@ ...@@ -986,9 +986,11 @@
videoWrapperHeight = $('.video-wrapper').height(); videoWrapperHeight = $('.video-wrapper').height();
progressSliderHeight = state.el.find('.slider').height(); progressSliderHeight = state.el.find('.slider').height();
controlHeight = state.el.find('.video-controls').height(); controlHeight = state.el.find('.video-controls').height();
shouldBeHeight = videoWrapperHeight - shouldBeHeight = parseInt((
videoWrapperHeight -
0.5 * progressSliderHeight - 0.5 * progressSliderHeight -
controlHeight; controlHeight
), 10);
expect(realHeight).toBe(shouldBeHeight); expect(realHeight).toBe(shouldBeHeight);
}); });
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
expect(timeControl).toHaveAttrs({ expect(timeControl).toHaveAttrs({
'role': 'slider', 'role': 'slider',
'aria-label': 'Video position', 'aria-label': 'Video position. Press space to toggle playback',
'aria-disabled': 'false' 'aria-disabled': 'false'
}); });
......
...@@ -405,6 +405,8 @@ function(HTML5Video, Resizer) { ...@@ -405,6 +405,8 @@ function(HTML5Video, Resizer) {
this.videoPlayer.goToStartTime = false; this.videoPlayer.goToStartTime = false;
this.videoPlayer.seekTo(time); this.videoPlayer.seekTo(time);
this.trigger('videoProgressSlider.focusSlider');
this.el.trigger('seek', [time, oldTime, type]); this.el.trigger('seek', [time, oldTime, type]);
} }
......
...@@ -12,7 +12,9 @@ mind, or whether to act, and in acting, to live." ...@@ -12,7 +12,9 @@ mind, or whether to act, and in acting, to live."
[], [],
function() { function() {
var template = [ var template = [
'<div class="slider" title="', gettext('Video position'), '"></div>' '<div class="slider" role="application" title="',
gettext('Video position. Press space to toggle playback'),
'"></div>'
].join(''); ].join('');
// VideoProgressSlider() function - what this module "exports". // VideoProgressSlider() function - what this module "exports".
...@@ -35,6 +37,8 @@ function() { ...@@ -35,6 +37,8 @@ function() {
// //
// Functions which will be accessible via 'state' object. When called, // Functions which will be accessible via 'state' object. When called,
// these functions will get the 'state' object as a context. // these functions will get the 'state' object as a context.
/* eslint-disable no-use-before-define */
function _makeFunctionsPublic(state) { function _makeFunctionsPublic(state) {
var methodsDict = { var methodsDict = {
destroy: destroy, destroy: destroy,
...@@ -45,7 +49,8 @@ function() { ...@@ -45,7 +49,8 @@ function() {
updatePlayTime: updatePlayTime, updatePlayTime: updatePlayTime,
updateStartEndTimeRegion: updateStartEndTimeRegion, updateStartEndTimeRegion: updateStartEndTimeRegion,
notifyThroughHandleEnd: notifyThroughHandleEnd, notifyThroughHandleEnd: notifyThroughHandleEnd,
getTimeDescription: getTimeDescription getTimeDescription: getTimeDescription,
focusSlider: focusSlider
}; };
state.bindTo(methodsDict, state.videoProgressSlider, state); state.bindTo(methodsDict, state.videoProgressSlider, state);
...@@ -57,6 +62,12 @@ function() { ...@@ -57,6 +62,12 @@ function() {
delete this.videoProgressSlider; delete this.videoProgressSlider;
} }
function bindHandlers(state) {
state.videoProgressSlider.el.on('keypress', sliderToggle.bind(state));
state.el.on('destroy', state.videoProgressSlider.destroy);
}
/* eslint-enable no-use-before-define */
// function _renderElements(state) // function _renderElements(state)
// //
// Create any necessary DOM elements, attach them, and set their // Create any necessary DOM elements, attach them, and set their
...@@ -69,6 +80,7 @@ function() { ...@@ -69,6 +80,7 @@ function() {
state.el.find('.video-controls').prepend(state.videoProgressSlider.el); state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
state.videoProgressSlider.buildSlider(); state.videoProgressSlider.buildSlider();
_buildHandle(state); _buildHandle(state);
bindHandlers(state);
} }
function _buildHandle(state) { function _buildHandle(state) {
...@@ -77,7 +89,10 @@ function() { ...@@ -77,7 +89,10 @@ function() {
// ARIA // ARIA
// We just want the knob to be selectable with keyboard // We just want the knob to be selectable with keyboard
state.videoProgressSlider.el.attr('tabindex', -1); state.videoProgressSlider.el.attr({
tabindex: -1
});
// Let screen readers know that this div, representing the slider // Let screen readers know that this div, representing the slider
// handle, behaves as a slider named 'video position'. // handle, behaves as a slider named 'video position'.
state.videoProgressSlider.handle.attr({ state.videoProgressSlider.handle.attr({
...@@ -89,10 +104,8 @@ function() { ...@@ -89,10 +104,8 @@ function() {
'aria-valuemin': '0', 'aria-valuemin': '0',
'aria-valuenow': state.videoPlayer.currentTime, 'aria-valuenow': state.videoPlayer.currentTime,
'tabindex': '0', 'tabindex': '0',
'aria-label': gettext('Video position') 'aria-label': gettext('Video position. Press space to toggle playback')
}); });
state.el.on('destroy', state.videoProgressSlider.destroy);
} }
// *************************************************************** // ***************************************************************
...@@ -103,8 +116,11 @@ function() { ...@@ -103,8 +116,11 @@ function() {
// *************************************************************** // ***************************************************************
function buildSlider() { function buildSlider() {
this.videoProgressSlider.el var sliderContents = edx.HtmlUtils.joinHtml(
.append('<div class="ui-slider-handle progress-handle"></div>'); edx.HtmlUtils.HTML('<div class="ui-slider-handle progress-handle"></div>')
);
this.videoProgressSlider.el.append(sliderContents.text);
this.videoProgressSlider.slider = this.videoProgressSlider.el this.videoProgressSlider.slider = this.videoProgressSlider.el
.slider({ .slider({
...@@ -328,5 +344,21 @@ function() { ...@@ -328,5 +344,21 @@ function() {
return i18n(seconds, 'second'); return i18n(seconds, 'second');
} }
// Shift focus to the progress slider container element.
function focusSlider() {
this.videoProgressSlider.handle.attr(
'aria-valuetext', getTimeDescription(this.videoPlayer.currentTime)
);
this.videoProgressSlider.el.trigger('focus');
}
// Toggle video playback when the spacebar is pushed.
function sliderToggle(e) {
if (e.which === 32) {
e.preventDefault();
this.videoCommands.execute('togglePlayback');
}
}
}); });
}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); }(RequireJS.requirejs, RequireJS.require, RequireJS.define));
...@@ -40,10 +40,10 @@ function(HtmlUtils) { ...@@ -40,10 +40,10 @@ function(HtmlUtils) {
videoVolumeControlHtml: HtmlUtils.interpolateHtml( videoVolumeControlHtml: HtmlUtils.interpolateHtml(
HtmlUtils.HTML([ HtmlUtils.HTML([
'<div class="volume" role="application">', '<div class="volume" role="application">',
'<p class="sr instructions" id="volume-instructions">', '<p class="sr instructions">',
'{volumeInstructions}', '{volumeInstructions}',
'</p>', '</p>',
'<button class="control" aria-disabled="false" aria-describedby="volume-instructions"', '<button class="control" aria-disabled="false"',
'" aria-expanded="false" title="', '" aria-expanded="false" title="',
'{adjustVideoVolume}', '{adjustVideoVolume}',
'">', '">',
...@@ -129,7 +129,8 @@ function(HtmlUtils) { ...@@ -129,7 +129,8 @@ function(HtmlUtils) {
* initial configuration. * initial configuration.
*/ */
render: function() { render: function() {
var container = this.el.find('.volume-slider'); var container = this.el.find('.volume-slider'),
instructionsId = 'volume-instructions-' + this.state.id;
HtmlUtils.append(container, HtmlUtils.HTML('<div class="ui-slider-handle volume-handle"></div>')); HtmlUtils.append(container, HtmlUtils.HTML('<div class="ui-slider-handle volume-handle"></div>'));
...@@ -146,6 +147,10 @@ function(HtmlUtils) { ...@@ -146,6 +147,10 @@ function(HtmlUtils) {
// order. // order.
container.find('.volume-handle').attr('tabindex', -1); container.find('.volume-handle').attr('tabindex', -1);
this.state.el.find('.secondary-controls').append(this.el); this.state.el.find('.secondary-controls').append(this.el);
// set dynamic id for instruction element to avoid collisions
this.el.find('.instructions').attr('id', instructionsId);
this.button.attr('aria-describedby', instructionsId);
}, },
/** Bind any necessary function callbacks to DOM events. */ /** Bind any necessary function callbacks to DOM events. */
......
...@@ -31,13 +31,13 @@ ...@@ -31,13 +31,13 @@
SpeedControl.prototype = { SpeedControl.prototype = {
template: [ template: [
'<div class="speeds menu-container" role="application">', '<div class="speeds menu-container" role="application">',
'<p class="sr instructions" id="speed-instructions">', '<p class="sr instructions">',
gettext('Press UP to enter the speed menu then use the UP and DOWN arrow keys to navigate the different speeds, then press ENTER to change to the selected speed.'), // eslint-disable-line max-len, indent gettext('Press UP to enter the speed menu then use the UP and DOWN arrow keys to navigate the different speeds, then press ENTER to change to the selected speed.'), // eslint-disable-line max-len, indent
'</p>', '</p>',
'<button class="control speed-button" aria-disabled="false" aria-expanded="false"', '<button class="control speed-button" aria-disabled="false" aria-expanded="false"',
'title="', 'title="',
gettext('Adjust video speed'), gettext('Adjust video speed'),
'" aria-describedby="speed-instructions">', '">',
'<span>', '<span>',
'<span class="icon fa fa-caret-right" aria-hidden="true"></span>', '<span class="icon fa fa-caret-right" aria-hidden="true"></span>',
'</span>', '</span>',
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
render: function(speeds, currentSpeed) { render: function(speeds, currentSpeed) {
var speedsContainer = this.speedsContainer, var speedsContainer = this.speedsContainer,
reversedSpeeds = speeds.concat().reverse(), reversedSpeeds = speeds.concat().reverse(),
instructionsId = 'speed-instructions-' + this.state.id,
speedsList = $.map(reversedSpeeds, function(speed) { speedsList = $.map(reversedSpeeds, function(speed) {
return HtmlUtils.interpolateHtml( return HtmlUtils.interpolateHtml(
HtmlUtils.HTML( HtmlUtils.HTML(
...@@ -125,6 +126,10 @@ ...@@ -125,6 +126,10 @@
HtmlUtils.HTML(this.el) HtmlUtils.HTML(this.el)
); );
this.setActiveSpeed(currentSpeed); this.setActiveSpeed(currentSpeed);
// set dynamic id for instruction element to avoid collisions
this.el.find('.instructions').attr('id', instructionsId);
this.speedButton.attr('aria-describedby', instructionsId);
}, },
/** /**
......
...@@ -88,20 +88,19 @@ ...@@ -88,20 +88,19 @@
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>', '<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
'</button>', '</button>',
'<div class="lang menu-container" role="application">', '<div class="lang menu-container" role="application">',
'<p class="sr instructions" id="lang-instructions"></p>', '<p class="sr instructions" id="lang-instructions-{courseId}"></p>',
'<button class="control language-menu" aria-disabled="false"', '<button class="control language-menu" aria-disabled="false"',
'aria-describedby="lang-instructions" ', 'aria-describedby="lang-instructions-{courseId}" ',
'title="{langTitle}">', 'title="{langTitle}">',
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>', '<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
'</button>', '</button>',
'</div>', '</div>',
'</div>' '</div>'
].join(''), ].join('')),
{ {
langTitle: gettext('Open language menu') langTitle: gettext('Open language menu'),
} courseId: this.state.id
) }
); );
var subtitlesHtml = HtmlUtils.interpolateHtml( var subtitlesHtml = HtmlUtils.interpolateHtml(
...@@ -109,7 +108,7 @@ ...@@ -109,7 +108,7 @@
[ [
'<div class="subtitles" role="region" id="transcript-{courseId}">', '<div class="subtitles" role="region" id="transcript-{courseId}">',
'<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>', '<h3 id="transcript-label-{courseId}" class="transcript-title sr"></h3>',
'<ol id="transcript-captions" class="subtitles-menu" lang="{courseLang}"></ol>', '<ol id="transcript-captions-{courseId}" class="subtitles-menu" lang="{courseLang}"></ol>',
'</div>' '</div>'
].join('')), ].join('')),
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment