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( - '
  1. Transcript will be displayed when ' + - 'you start playing the video.
  2. ' - ) - ); + self.subtitlesEl.find('.subtitles-menu') + .text(gettext('Transcript will be displayed when you start playing the video.')) // jshint ignore: line + .wrapInner('
  3. '); } 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 a navigable version of this video's transcript.")} -
    @@ -37,12 +35,9 @@ from openedx.core.djangolib.js_utils import js_escaped_string
    - ${_('Skip to end of transcript.')} - ${_('Go back to start of transcript.')} -