Merge pull request #11369 from edx/clrux/ac-223
AC-223 updating transcript skip links
This commit is contained in:
@@ -646,7 +646,8 @@ hr.divider {
|
||||
}
|
||||
|
||||
// ui - skipnav
|
||||
.nav-skip {
|
||||
.nav-skip,
|
||||
.transcript-skip {
|
||||
@extend %t-action3;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
@@ -23,7 +23,8 @@ nav {
|
||||
// ====================
|
||||
|
||||
// skip navigation
|
||||
.nav-skip {
|
||||
.nav-skip,
|
||||
.transcript-skip {
|
||||
@include font-size(13);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
@@ -196,7 +196,7 @@ html:not('.afontgarde') .icon-fallback-img {
|
||||
background-color: black;
|
||||
position: relative;
|
||||
|
||||
.video-player-pre,
|
||||
.video-player-pre,
|
||||
.video-player-post {
|
||||
height: 50px;
|
||||
background-color: rgb(17, 16, 16) // UXPL grayscale black;
|
||||
@@ -275,6 +275,12 @@ html:not('.afontgarde') .icon-fallback-img {
|
||||
}
|
||||
}
|
||||
|
||||
.video-error {
|
||||
padding: ($baseline / 5);
|
||||
background: black;
|
||||
color: white !important; // the pattern library headings shim is more scoped
|
||||
}
|
||||
|
||||
object,
|
||||
iframe,
|
||||
video {
|
||||
@@ -723,6 +729,10 @@ html:not('.afontgarde') .icon-fallback-img {
|
||||
font-size: 14px;
|
||||
visibility: visible;
|
||||
|
||||
a {
|
||||
color: #0074b5;
|
||||
}
|
||||
|
||||
.subtitles-menu {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -417,7 +417,7 @@
|
||||
});
|
||||
|
||||
it('show explanation message', function () {
|
||||
expect($('.subtitles-menu li')).toHaveHtml(
|
||||
expect($('.subtitles-menu li')).toHaveText(
|
||||
'Transcript will be displayed when you start playing the video.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -132,7 +132,6 @@ function (VideoPlayer, i18n, moment) {
|
||||
|
||||
onYTApiReady = function () {
|
||||
console.log('[Video info]: YouTube API is available and is loaded.');
|
||||
|
||||
if (state.htmlPlayerLoaded) { return; }
|
||||
|
||||
console.log('[Video info]: Starting YouTube player.');
|
||||
@@ -140,7 +139,6 @@ function (VideoPlayer, i18n, moment) {
|
||||
|
||||
state.modules.push(video);
|
||||
state.__dfd__.resolve();
|
||||
|
||||
state.youtubeApiAvailable = true;
|
||||
};
|
||||
|
||||
@@ -211,7 +209,6 @@ function (VideoPlayer, i18n, moment) {
|
||||
|
||||
function _waitForYoutubeApi(state) {
|
||||
console.log('[Video info]: Starting to wait for YouTube API to load.');
|
||||
|
||||
window.setTimeout(function () {
|
||||
// If YouTube API will load OK, it will run `onYouTubeIframeAPIReady`
|
||||
// callback, which will set `state.youtubeApiAvailable` to `true`.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
(function (define) {
|
||||
(function(define) {
|
||||
// VideoCaption module.
|
||||
'use strict';
|
||||
|
||||
define(
|
||||
'video/09_video_caption.js',
|
||||
['video/00_sjson.js', 'video/00_async_process.js'],
|
||||
define('video/09_video_caption.js', [
|
||||
'video/00_sjson.js',
|
||||
'video/00_async_process.js'
|
||||
],
|
||||
function (Sjson, AsyncProcess) {
|
||||
|
||||
/**
|
||||
@@ -43,49 +44,6 @@
|
||||
};
|
||||
|
||||
VideoCaption.prototype = {
|
||||
langTemplate: [
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text">',
|
||||
gettext('Turn on closed captioning'),
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text">',
|
||||
gettext('Turn off transcript'),
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<button class="control language-menu" aria-label="',
|
||||
/* jshint maxlen:250 */
|
||||
gettext('Language: Press the UP arrow key to enter the language menu, then use UP and DOWN arrow keys to navigate language options. Press ENTER to change to the selected language.'),
|
||||
'" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text">',
|
||||
gettext('Open language menu'),
|
||||
'</span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
template: [
|
||||
'<div class="subtitles" role="region" aria-label="',
|
||||
/* jshint maxlen:200 */
|
||||
gettext('Activating an item in this group will spool the video to the corresponding time point. To skip transcript, go to previous item.'),
|
||||
'">',
|
||||
'<ol id="transcript-captions" class="subtitles-menu">',
|
||||
'</ol>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
|
||||
destroy: function () {
|
||||
this.state.el
|
||||
@@ -117,10 +75,46 @@
|
||||
renderElements: function () {
|
||||
var languages = this.state.config.transcriptLanguages;
|
||||
|
||||
var langTemplate = [
|
||||
'<div class="grouped-controls">',
|
||||
'<button class="control toggle-captions" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-cc" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<button class="control toggle-transcript" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-quote-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'<div class="lang menu-container" role="application">',
|
||||
'<button class="control language-menu" aria-label="" aria-disabled="false">',
|
||||
'<span class="icon-fallback-img">',
|
||||
'<span class="icon fa fa-caret-left" aria-hidden="true"></span>',
|
||||
'<span class="sr control-text"></span>',
|
||||
'</span>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('');
|
||||
|
||||
var template = [
|
||||
'<div class="subtitles" role="region" id="transcript-' + this.state.id + '">',
|
||||
'<a href="#transcript-end-' + this.state.id + '"',
|
||||
'id="transcript-start-' + this.state.id + '" class="transcript-start"></a>',
|
||||
'<h3 id="transcript-label-' + this.state.id + '" class="transcript-title sr"></h3>',
|
||||
'<ol id="transcript-captions" class="subtitles-menu"></ol>',
|
||||
'<a href="#transcript-start-' + this.state.id + '"',
|
||||
'id="transcript-end-' + this.state.id + '" class="transcript-end">\</a>',
|
||||
'</div>'
|
||||
].join('');
|
||||
|
||||
this.loaded = false;
|
||||
this.subtitlesEl = $(this.template);
|
||||
this.subtitlesEl = $(template);
|
||||
this.subtitlesMenuEl = this.subtitlesEl.find('.subtitles-menu');
|
||||
this.container = $(this.langTemplate);
|
||||
this.container = $(langTemplate);
|
||||
this.captionControlEl = this.container.find('.toggle-captions');
|
||||
this.captionDisplayEl = this.state.el.find('.closed-captions');
|
||||
this.transcriptControlEl = this.container.find('.toggle-transcript');
|
||||
@@ -535,12 +529,9 @@
|
||||
}
|
||||
} else {
|
||||
if (state.isTouch) {
|
||||
self.subtitlesEl.find('.subtitles-menu').html(
|
||||
gettext(
|
||||
'<li>Transcript will be displayed when ' +
|
||||
'you start playing the video.</li>'
|
||||
)
|
||||
);
|
||||
self.subtitlesEl.find('.subtitles-menu')
|
||||
.text(gettext('Transcript will be displayed when you start playing the video.')) // jshint ignore: line
|
||||
.wrapInner('<li></li>');
|
||||
} else {
|
||||
self.renderCaption(start, captions);
|
||||
}
|
||||
@@ -747,6 +738,24 @@
|
||||
self.isMouseFocus = false;
|
||||
self.rendered = true;
|
||||
self.state.el.addClass('is-captions-rendered');
|
||||
|
||||
self.subtitlesEl
|
||||
.attr('aria-label', gettext('Activating a link in this group will skip to the corresponding point in the video.')); // jshint ignore:line
|
||||
|
||||
self.subtitlesEl.find('.transcript-title')
|
||||
.text(gettext('Video transcript'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-start')
|
||||
.text(gettext('Start of transcript. Skip to the end.'));
|
||||
|
||||
self.subtitlesEl.find('.transcript-end')
|
||||
.text(gettext('End of transcript. Skip to the start.'));
|
||||
|
||||
self.container.find('.menu-container .control')
|
||||
.attr('aria-label', gettext('Language: Press the UP arrow key to enter the language menu then use UP and DOWN arrow keys to navigate language options. Press ENTER to change to the selected language.')); // jshint ignore:line
|
||||
|
||||
self.container.find('.menu-container .control .control-text')
|
||||
.text(gettext('Open language menu.'));
|
||||
};
|
||||
|
||||
this.rendered = false;
|
||||
|
||||
10
common/static/js/vendor/afontgarde/edx-icons.js
vendored
10
common/static/js/vendor/afontgarde/edx-icons.js
vendored
@@ -1,3 +1,7 @@
|
||||
AFontGarde('FontAwesome', {
|
||||
glyphs: ''
|
||||
});
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.AFontGarde('FontAwesome', {
|
||||
glyphs: ''
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,8 @@ VIDEO_MENUS = {
|
||||
'language': '.lang .menu',
|
||||
'speed': '.speed .menu',
|
||||
'download_transcript': '.video-tracks .a11y-menu-list',
|
||||
'transcript-format': '.video-tracks .a11y-menu-button'
|
||||
'transcript-format': '.video-tracks .a11y-menu-button',
|
||||
'transcript-skip': '.sr-is-focusable.transcript-start',
|
||||
}
|
||||
|
||||
|
||||
@@ -906,6 +907,17 @@ class VideoPage(PageObject):
|
||||
classes = self.q(css=selector).attrs('class')[0].split()
|
||||
return 'active' in classes
|
||||
|
||||
@property
|
||||
def is_transcript_skip_visible(self):
|
||||
"""
|
||||
Checks if the skip-to containers in transcripts are present and visible.
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
selector = self.get_element_selector(VIDEO_MENUS['transcript-skip'])
|
||||
return self.q(css=selector).visible
|
||||
|
||||
def wait_for_captions(self):
|
||||
"""
|
||||
Wait until captions rendered completely.
|
||||
|
||||
@@ -345,13 +345,18 @@ class CMSVideoA11yTest(CMSVideoBaseTest):
|
||||
super(CMSVideoA11yTest, self).setUp()
|
||||
|
||||
def test_video_player_a11y(self):
|
||||
# Limit the scope of the audit to the video player only.
|
||||
self.outline.a11y_audit.config.set_scope(include=["div.video"])
|
||||
self.outline.a11y_audit.config.set_rules({
|
||||
"ignore": [
|
||||
'link-href', # TODO: AC-223
|
||||
],
|
||||
})
|
||||
# we're loading a shorter transcript to ensure both skip links are available
|
||||
self._create_course_unit(subtitles=True)
|
||||
self.edit_component()
|
||||
self.video.upload_transcript('english_single_transcript.srt')
|
||||
|
||||
self._create_course_unit()
|
||||
self.save_unit_settings()
|
||||
self.video.wait_for_captions()
|
||||
self.assertTrue(self.video.is_captions_visible())
|
||||
|
||||
# limit the scope of the audit to the video player only.
|
||||
self.outline.a11y_audit.config.set_scope(
|
||||
include=["div.video"],
|
||||
exclude=["a.ui-slider-handle"]
|
||||
)
|
||||
self.outline.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
@@ -1210,13 +1210,18 @@ class LMSVideoModuleA11yTest(VideoBaseTest):
|
||||
super(LMSVideoModuleA11yTest, self).setUp()
|
||||
|
||||
def test_video_player_a11y(self):
|
||||
self.navigate_to_video()
|
||||
# load transcripts so we can test skipping to
|
||||
self.assets.extend(['english_single_transcript.srt', 'subs_3_yD_cEKoCk.srt.sjson'])
|
||||
data = {'transcripts': {"en": "english_single_transcript.srt"}, 'sub': '3_yD_cEKoCk'}
|
||||
self.metadata = self.metadata_for_mode('youtube', additional_data=data)
|
||||
|
||||
# Limit the scope of the audit to the video player only.
|
||||
self.video.a11y_audit.config.set_scope(include=["div.video"])
|
||||
self.video.a11y_audit.config.set_rules({
|
||||
"ignore": [
|
||||
'link-href', # TODO: AC-223
|
||||
],
|
||||
})
|
||||
# go to video
|
||||
self.navigate_to_video()
|
||||
self.video.show_captions()
|
||||
|
||||
# limit the scope of the audit to the video player only.
|
||||
self.video.a11y_audit.config.set_scope(
|
||||
include=["div.video"],
|
||||
exclude=["a.ui-slider-handle"]
|
||||
)
|
||||
self.video.a11y_audit.check_for_accessibility_errors()
|
||||
|
||||
3
common/test/data/uploads/english_single_transcript.srt
Normal file
3
common/test/data/uploads/english_single_transcript.srt
Normal file
@@ -0,0 +1,3 @@
|
||||
1
|
||||
00:00:01,000 --> 00:00:24,400
|
||||
Hellow, 'Orld!
|
||||
@@ -333,7 +333,8 @@ mark {
|
||||
}
|
||||
|
||||
// UI - skipnav
|
||||
.nav-skip {
|
||||
.nav-skip,
|
||||
.transcript-skip {
|
||||
@extend %ui-print-excluded;
|
||||
|
||||
display: inline-block;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
@extend %text-sr;
|
||||
}
|
||||
|
||||
.nav-contents, .nav-skip {
|
||||
.nav-contents, .nav-skip, .transcript-skip {
|
||||
@extend %nav-skip;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
<div class="focus_grabber first"></div>
|
||||
|
||||
<div class="tc-wrapper">
|
||||
<a href="#before-transcript_${id}" class="nav-skip sr">${_("Skip to a navigable version of this video's transcript.")}</a>
|
||||
|
||||
<article class="video-wrapper">
|
||||
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
|
||||
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
|
||||
@@ -37,12 +35,9 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
<div class="secondary-controls"></div>
|
||||
</div>
|
||||
</section>
|
||||
<a class="nav-skip sr" id="before-transcript_${id}" href="#after-transcript_${id}">${_('Skip to end of transcript.')}</a>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<a class="nav-skip sr" id="after-transcript_${id}" href="#before-transcript_${id}">${_('Go back to start of transcript.')}</a>
|
||||
|
||||
<div class="focus_grabber last"></div>
|
||||
<ul class="wrapper-downloads">
|
||||
% if download_video_link:
|
||||
|
||||
Reference in New Issue
Block a user