diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss
index 69f190b121..fc631e177a 100644
--- a/cms/static/sass/_base.scss
+++ b/cms/static/sass/_base.scss
@@ -646,7 +646,8 @@ hr.divider {
}
// ui - skipnav
-.nav-skip {
+.nav-skip,
+.transcript-skip {
@extend %t-action3;
display: inline-block;
position: absolute;
diff --git a/cms/static/sass/elements/_navigation.scss b/cms/static/sass/elements/_navigation.scss
index 1e7e5f42da..efedbead78 100644
--- a/cms/static/sass/elements/_navigation.scss
+++ b/cms/static/sass/elements/_navigation.scss
@@ -23,7 +23,8 @@ nav {
// ====================
// skip navigation
-.nav-skip {
+.nav-skip,
+.transcript-skip {
@include font-size(13);
display: inline-block;
position: absolute;
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index a0ac08bf06..5de928ab08 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -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;
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html
index f7d04cce65..ce520fd522 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video.html
@@ -26,7 +26,6 @@
-
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index c190933177..95bb6a879c 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -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.'
);
});
diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
index 9a6300442f..ac59870d88 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -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`.
diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
index e25f968659..f41908156d 100644
--- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
+++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js
@@ -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: [
- '',
- '',
- '',
- '',
- '
'
- ].join(''),
-
- template: [
- '',
- '',
- '
'
- ].join(''),
destroy: function () {
this.state.el
@@ -117,10 +75,46 @@
renderElements: function () {
var languages = this.state.config.transcriptLanguages;
+ var langTemplate = [
+ '',
+ '',
+ '',
+ '',
+ '
'
+ ].join('');
+
+ var template = [
+ '',
+ '
',
+ '
',
+ '',
+ '
\',
+ '
'
+ ].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(
- 'Transcript will be displayed when ' +
- 'you start playing the video.'
- )
- );
+ self.subtitlesEl.find('.subtitles-menu')
+ .text(gettext('Transcript will be displayed when you start playing the video.')) // jshint ignore: line
+ .wrapInner('');
} 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;
diff --git a/common/static/js/vendor/afontgarde/edx-icons.js b/common/static/js/vendor/afontgarde/edx-icons.js
index a019005f00..dec6d2c935 100644
--- a/common/static/js/vendor/afontgarde/edx-icons.js
+++ b/common/static/js/vendor/afontgarde/edx-icons.js
@@ -1,3 +1,7 @@
-AFontGarde('FontAwesome', {
- glyphs: ''
-});
\ No newline at end of file
+(function() {
+ 'use strict';
+
+ window.AFontGarde('FontAwesome', {
+ glyphs: ''
+ });
+});
diff --git a/common/test/acceptance/pages/lms/video/video.py b/common/test/acceptance/pages/lms/video/video.py
index 1a33d8c6af..52ffd0d310 100644
--- a/common/test/acceptance/pages/lms/video/video.py
+++ b/common/test/acceptance/pages/lms/video/video.py
@@ -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.
diff --git a/common/test/acceptance/tests/video/test_studio_video_module.py b/common/test/acceptance/tests/video/test_studio_video_module.py
index 18a4d59f35..ef0fa267b6 100644
--- a/common/test/acceptance/tests/video/test_studio_video_module.py
+++ b/common/test/acceptance/tests/video/test_studio_video_module.py
@@ -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()
diff --git a/common/test/acceptance/tests/video/test_video_module.py b/common/test/acceptance/tests/video/test_video_module.py
index e3a7820b4c..bcff620033 100644
--- a/common/test/acceptance/tests/video/test_video_module.py
+++ b/common/test/acceptance/tests/video/test_video_module.py
@@ -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()
diff --git a/common/test/data/uploads/english_single_transcript.srt b/common/test/data/uploads/english_single_transcript.srt
new file mode 100644
index 0000000000..a2b4c2cdf8
--- /dev/null
+++ b/common/test/data/uploads/english_single_transcript.srt
@@ -0,0 +1,3 @@
+1
+00:00:01,000 --> 00:00:24,400
+Hellow, 'Orld!
diff --git a/lms/static/sass/base/_base.scss b/lms/static/sass/base/_base.scss
index a67a8198d5..2ff9baca12 100644
--- a/lms/static/sass/base/_base.scss
+++ b/lms/static/sass/base/_base.scss
@@ -333,7 +333,8 @@ mark {
}
// UI - skipnav
-.nav-skip {
+.nav-skip,
+.transcript-skip {
@extend %ui-print-excluded;
display: inline-block;
diff --git a/lms/static/sass/elements/_navigation.scss b/lms/static/sass/elements/_navigation.scss
index 40087c370b..040991d7a5 100644
--- a/lms/static/sass/elements/_navigation.scss
+++ b/lms/static/sass/elements/_navigation.scss
@@ -20,7 +20,7 @@
@extend %text-sr;
}
-.nav-contents, .nav-skip {
+.nav-contents, .nav-skip, .transcript-skip {
@extend %nav-skip;
}
diff --git a/lms/templates/video.html b/lms/templates/video.html
index c25a21bb60..79ddf44e09 100644
--- a/lms/templates/video.html
+++ b/lms/templates/video.html
@@ -19,8 +19,6 @@ from openedx.core.djangolib.js_utils import js_escaped_string
- ${_('Skip to end of transcript.')}
- ${_('Go back to start of transcript.')}
-
% if download_video_link: