Merge pull request #14744 from edx/ari/video-transcript-focus
video a11y improvements (TNL-6361, AC-587)
This commit is contained in:
@@ -986,9 +986,11 @@
|
||||
videoWrapperHeight = $('.video-wrapper').height();
|
||||
progressSliderHeight = state.el.find('.slider').height();
|
||||
controlHeight = state.el.find('.video-controls').height();
|
||||
shouldBeHeight = videoWrapperHeight -
|
||||
shouldBeHeight = parseInt((
|
||||
videoWrapperHeight -
|
||||
0.5 * progressSliderHeight -
|
||||
controlHeight;
|
||||
controlHeight
|
||||
), 10);
|
||||
|
||||
expect(realHeight).toBe(shouldBeHeight);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
expect(timeControl).toHaveAttrs({
|
||||
'role': 'slider',
|
||||
'aria-label': 'Video position',
|
||||
'aria-label': 'Video position. Press space to toggle playback',
|
||||
'aria-disabled': 'false'
|
||||
});
|
||||
|
||||
|
||||
@@ -405,6 +405,8 @@ function(HTML5Video, Resizer) {
|
||||
this.videoPlayer.goToStartTime = false;
|
||||
|
||||
this.videoPlayer.seekTo(time);
|
||||
this.trigger('videoProgressSlider.focusSlider');
|
||||
|
||||
this.el.trigger('seek', [time, oldTime, type]);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ mind, or whether to act, and in acting, to live."
|
||||
[],
|
||||
function() {
|
||||
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('');
|
||||
|
||||
// VideoProgressSlider() function - what this module "exports".
|
||||
@@ -35,6 +37,8 @@ function() {
|
||||
//
|
||||
// Functions which will be accessible via 'state' object. When called,
|
||||
// these functions will get the 'state' object as a context.
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
function _makeFunctionsPublic(state) {
|
||||
var methodsDict = {
|
||||
destroy: destroy,
|
||||
@@ -45,7 +49,8 @@ function() {
|
||||
updatePlayTime: updatePlayTime,
|
||||
updateStartEndTimeRegion: updateStartEndTimeRegion,
|
||||
notifyThroughHandleEnd: notifyThroughHandleEnd,
|
||||
getTimeDescription: getTimeDescription
|
||||
getTimeDescription: getTimeDescription,
|
||||
focusSlider: focusSlider
|
||||
};
|
||||
|
||||
state.bindTo(methodsDict, state.videoProgressSlider, state);
|
||||
@@ -57,6 +62,12 @@ function() {
|
||||
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)
|
||||
//
|
||||
// Create any necessary DOM elements, attach them, and set their
|
||||
@@ -69,6 +80,7 @@ function() {
|
||||
state.el.find('.video-controls').prepend(state.videoProgressSlider.el);
|
||||
state.videoProgressSlider.buildSlider();
|
||||
_buildHandle(state);
|
||||
bindHandlers(state);
|
||||
}
|
||||
|
||||
function _buildHandle(state) {
|
||||
@@ -77,7 +89,10 @@ function() {
|
||||
|
||||
// ARIA
|
||||
// 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
|
||||
// handle, behaves as a slider named 'video position'.
|
||||
state.videoProgressSlider.handle.attr({
|
||||
@@ -89,10 +104,8 @@ function() {
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuenow': state.videoPlayer.currentTime,
|
||||
'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() {
|
||||
// ***************************************************************
|
||||
|
||||
function buildSlider() {
|
||||
this.videoProgressSlider.el
|
||||
.append('<div class="ui-slider-handle progress-handle"></div>');
|
||||
var sliderContents = edx.HtmlUtils.joinHtml(
|
||||
edx.HtmlUtils.HTML('<div class="ui-slider-handle progress-handle"></div>')
|
||||
);
|
||||
|
||||
this.videoProgressSlider.el.append(sliderContents.text);
|
||||
|
||||
this.videoProgressSlider.slider = this.videoProgressSlider.el
|
||||
.slider({
|
||||
@@ -328,5 +344,21 @@ function() {
|
||||
|
||||
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));
|
||||
|
||||
@@ -40,10 +40,10 @@ function(HtmlUtils) {
|
||||
videoVolumeControlHtml: HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML([
|
||||
'<div class="volume" role="application">',
|
||||
'<p class="sr instructions" id="volume-instructions">',
|
||||
'<p class="sr instructions">',
|
||||
'{volumeInstructions}',
|
||||
'</p>',
|
||||
'<button class="control" aria-disabled="false" aria-describedby="volume-instructions"',
|
||||
'<button class="control" aria-disabled="false"',
|
||||
'" aria-expanded="false" title="',
|
||||
'{adjustVideoVolume}',
|
||||
'">',
|
||||
@@ -129,7 +129,8 @@ function(HtmlUtils) {
|
||||
* initial configuration.
|
||||
*/
|
||||
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>'));
|
||||
|
||||
@@ -146,6 +147,10 @@ function(HtmlUtils) {
|
||||
// order.
|
||||
container.find('.volume-handle').attr('tabindex', -1);
|
||||
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. */
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
SpeedControl.prototype = {
|
||||
template: [
|
||||
'<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
|
||||
'</p>',
|
||||
'<button class="control speed-button" aria-disabled="false" aria-expanded="false"',
|
||||
'title="',
|
||||
gettext('Adjust video speed'),
|
||||
'" aria-describedby="speed-instructions">',
|
||||
'">',
|
||||
'<span>',
|
||||
'<span class="icon fa fa-caret-right" aria-hidden="true"></span>',
|
||||
'</span>',
|
||||
@@ -98,6 +98,7 @@
|
||||
render: function(speeds, currentSpeed) {
|
||||
var speedsContainer = this.speedsContainer,
|
||||
reversedSpeeds = speeds.concat().reverse(),
|
||||
instructionsId = 'speed-instructions-' + this.state.id,
|
||||
speedsList = $.map(reversedSpeeds, function(speed) {
|
||||
return HtmlUtils.interpolateHtml(
|
||||
HtmlUtils.HTML(
|
||||
@@ -125,6 +126,10 @@
|
||||
HtmlUtils.HTML(this.el)
|
||||
);
|
||||
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 @@
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'<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"',
|
||||
'aria-describedby="lang-instructions" ',
|
||||
'aria-describedby="lang-instructions-{courseId}" ',
|
||||
'title="{langTitle}">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
{
|
||||
langTitle: gettext('Open language menu')
|
||||
}
|
||||
)
|
||||
|
||||
].join('')),
|
||||
{
|
||||
langTitle: gettext('Open language menu'),
|
||||
courseId: this.state.id
|
||||
}
|
||||
);
|
||||
|
||||
var subtitlesHtml = HtmlUtils.interpolateHtml(
|
||||
@@ -109,7 +108,7 @@
|
||||
[
|
||||
'<div class="subtitles" role="region" id="transcript-{courseId}">',
|
||||
'<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>'
|
||||
].join('')),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user