diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index c484b03963..2957542b4d 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -272,6 +272,7 @@ nav.sequence-bottom { // hover and active states .sequence-nav-button, .sequence-nav button { + &.focused, &:hover, &:active, &.active { diff --git a/common/lib/xmodule/xmodule/js/fixtures/sequence.html b/common/lib/xmodule/xmodule/js/fixtures/sequence.html index f8660ea286..43acc02f7d 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/sequence.html +++ b/common/lib/xmodule/xmodule/js/fixtures/sequence.html @@ -1,20 +1,47 @@ -
- +
-
+
- + +
diff --git a/common/lib/xmodule/xmodule/js/spec/.gitignore b/common/lib/xmodule/xmodule/js/spec/.gitignore index 9d0c021751..ab5950468c 100644 --- a/common/lib/xmodule/xmodule/js/spec/.gitignore +++ b/common/lib/xmodule/xmodule/js/spec/.gitignore @@ -7,3 +7,4 @@ !time_spec.js !collapsible_spec.js !xmodule_spec.js +!sequence/display_spec.js diff --git a/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.js b/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.js new file mode 100644 index 0000000000..2ae0381e71 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/sequence/display_spec.js @@ -0,0 +1,68 @@ +/* globals Sequence */ +(function() { + 'use strict'; + + describe('Sequence', function() { + var local = {}, + keydownHandler, + keys = { + ENTER: 13, + LEFT: 37, + RIGHT: 39 + }; + + beforeEach(function() { + loadFixtures('sequence.html'); + local.XBlock = window.XBlock = jasmine.createSpyObj('XBlock', ['initializeBlocks']); + }); + + afterEach(function() { + delete local.XBlock; + }); + + keydownHandler = function(key) { + var event = document.createEvent('Event'); + event.keyCode = key; + event.initEvent('keydown', false, false); + document.dispatchEvent(event); + }; + + describe('Navbar', function() { + it('works with keyboard navigation LEFT and ENTER', function() { + this.sequence = new Sequence($('.xblock-student_view-sequential')); + this.sequence.$('.nav-item[data-index=0]').focus(); + keydownHandler(keys.LEFT); + keydownHandler(keys.ENTER); + + expect(this.sequence.$('.nav-item[data-index=1]')).toHaveAttr({ + 'aria-expanded': 'false', + 'aria-selected': 'false', + tabindex: '-1' + }); + expect(this.sequence.$('.nav-item[data-index=0]')).toHaveAttr({ + 'aria-expanded': 'true', + 'aria-selected': 'true', + tabindex: '0' + }); + }); + + it('works with keyboard navigation RIGHT and ENTER', function() { + this.sequence = new Sequence($('.xblock-student_view-sequential')); + this.sequence.$('.nav-item[data-index=0]').focus(); + keydownHandler(keys.RIGHT); + keydownHandler(keys.ENTER); + + expect(this.sequence.$('.nav-item[data-index=0]')).toHaveAttr({ + 'aria-expanded': 'false', + 'aria-selected': 'false', + tabindex: '-1' + }); + expect(this.sequence.$('.nav-item[data-index=1]')).toHaveAttr({ + 'aria-expanded': 'true', + 'aria-selected': 'true', + tabindex: '0' + }); + }); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.js b/common/lib/xmodule/xmodule/js/src/sequence/display.js index 802b96dcac..b240b487c5 100644 --- a/common/lib/xmodule/xmodule/js/src/sequence/display.js +++ b/common/lib/xmodule/xmodule/js/src/sequence/display.js @@ -38,6 +38,12 @@ this.displayTabTooltip = function(event) { return Sequence.prototype.displayTabTooltip.apply(self, [event]); }; + this.arrowKeys = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40 + }; this.updatedProblems = {}; this.requestToken = $(element).data('request-token'); @@ -52,6 +58,7 @@ this.nextUrl = this.el.data('next-url'); this.prevUrl = this.el.data('prev-url'); this.base_page_title = ' | ' + document.title; + this.keydownHandler($(element).find('#sequence-list .tab')); this.bind(); this.render(parseInt(this.el.data('position'), 10)); } @@ -62,12 +69,63 @@ Sequence.prototype.bind = function() { this.$('#sequence-list .nav-item').click(this.goto); + this.$('#sequence-list .nav-item').keypress(this.keyDownHandler); this.el.on('bookmark:add', this.addBookmarkIconToActiveNavItem); this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem); this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip); this.$('#sequence-list .nav-item').on('blur mouseleave', this.hideTabTooltip); }; + Sequence.prototype.previousNav = function(focused, index) { + var $navItemList, + $sequenceList = $(focused).parent().parent(); + if (index === 0) { + $navItemList = $sequenceList.find('li').last(); + } else { + $navItemList = $sequenceList.find('li:eq(' + index + ')').prev(); + } + $sequenceList.find('.tab').removeClass('visited').removeClass('focused'); + $navItemList.find('.tab').addClass('focused').focus(); + }; + + Sequence.prototype.nextNav = function(focused, index, total) { + var $navItemList, + $sequenceList = $(focused).parent().parent(); + if (index === total) { + $navItemList = $sequenceList.find('li').first(); + } else { + $navItemList = $sequenceList.find('li:eq(' + index + ')').next(); + } + $sequenceList.find('.tab').removeClass('visited').removeClass('focused'); + $navItemList.find('.tab').addClass('focused').focus(); + }; + + Sequence.prototype.keydownHandler = function(element) { + var self = this; + element.keydown(function(event) { + var key = event.keyCode, + $focused = $(event.currentTarget), + $sequenceList = $focused.parent().parent(), + index = $sequenceList.find('li') + .index($focused.parent()), + total = $sequenceList.find('li') + .size() - 1; + switch (key) { + case self.arrowKeys.LEFT: + event.preventDefault(); + self.previousNav($focused, index); + break; + + case self.arrowKeys.RIGHT: + event.preventDefault(); + self.nextNav($focused, index, total); + break; + + // no default + } + }); + }; + Sequence.prototype.displayTabTooltip = function(event) { $(event.currentTarget).find('.sequence-tooltip').removeClass('sr'); }; @@ -317,13 +375,22 @@ Sequence.prototype.mark_visited = function(position) { // Don't overwrite class attribute to avoid changing Progress class var element = this.link_for(position); - element.removeClass('inactive').removeClass('active').addClass('visited'); + element.attr({tabindex: '-1', 'aria-selected': 'false', 'aria-expanded': 'false'}) + .removeClass('inactive') + .removeClass('active') + .removeClass('focused') + .addClass('visited'); }; Sequence.prototype.mark_active = function(position) { // Don't overwrite class attribute to avoid changing Progress class var element = this.link_for(position); - element.removeClass('inactive').removeClass('visited').addClass('active'); + element.attr({tabindex: '0', 'aria-selected': 'true', 'aria-expanded': 'true'}) + .removeClass('inactive') + .removeClass('visited') + .removeClass('focused') + .addClass('active'); + this.$('.sequence-list-wrapper').focus(); }; Sequence.prototype.addBookmarkIconToActiveNavItem = function(event) { diff --git a/lms/djangoapps/courseware/tests/test_split_module.py b/lms/djangoapps/courseware/tests/test_split_module.py index 6161179c02..c7aa323170 100644 --- a/lms/djangoapps/courseware/tests/test_split_module.py +++ b/lms/djangoapps/courseware/tests/test_split_module.py @@ -125,7 +125,7 @@ class SplitTestBase(SharedModuleStoreTestCase): content = resp.content # Assert we see the proper icon in the top display - self.assertIn(' -