Commit d497e194 by alisan617 Committed by GitHub

Merge pull request #14218 from edx/alisan/kb-support-seq-nav-TNL-5917

Sequence navs arrow keyboard support
parents 4503ab42 88e10e0e
...@@ -272,6 +272,7 @@ nav.sequence-bottom { ...@@ -272,6 +272,7 @@ nav.sequence-bottom {
// hover and active states // hover and active states
.sequence-nav-button, .sequence-nav-button,
.sequence-nav button { .sequence-nav button {
&.focused,
&:hover, &:hover,
&:active, &:active,
&.active { &.active {
......
<div id="sequence_1" class="sequence"> <div class="xblock-student_view-sequential">
<nav class="sequence-nav"> <div id="sequence_workflow" class="sequence">
<ol id="sequence-list"> <div class="sequence-nav">
</ol> <button class="sequence-nav-button button-previous">
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
<span>Previous</span>
</button>
<nav class="sequence-list-wrapper" aria-label="Unit">
<ol id="sequence-list" role="tablist">
<li>
<button role="tab" tabindex="0" aria-selected="true" aria-expanded="true" aria-controls="seq_content" class="seq_problem nav-item tab active" data-index="0" data-id="block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050" data-element="1" data-page-title="Unit 101" data-path="Example Week 2: Get Interactive > Homework - Part 1 > Unit 101" id="tab_0">
<span class="icon fa seq_problem" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon is-hidden" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">problem</span>Unit 101<span class="sr bookmark-icon-sr"></span></div>
</button>
</li>
<li>
<button role="tab" tabindex="-1" aria-selected="true" aria-expanded="true" aria-controls="seq_content" class="seq_problem inactive nav-item tab" data-index="1" data-id="block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9051" data-element="2" data-page-title="Unit 102" data-path="Example Week 2: Get Interactive > Homework - Part 1 > Unit 102" id="tab_1">
<span class="icon fa seq_problem" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon is-hidden" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">problem</span>Unit 102<span class="sr bookmark-icon-sr"></span></div>
</button>
</li>
</ol>
</nav>
<button class="sequence-nav-button button-next">
<span>Next</span>
<span class="icon fa fa-chevron-next" aria-hidden="true"></span>
</button>
</div>
<ul class="sequence-nav-buttons"> <div class="sr-is-focusable" tabindex="-1"></div>
<li class="prev"><button>Previous</button></li>
<li class="next"><button>Next</button></li>
</ul>
</nav>
<div id="seq_content"></div> <div id="seq_content" role="tabpanel"></div>
<nav class="sequence-bottom"> <nav class="sequence-bottom" aria-label="Section">
<ul class="sequence-nav-buttons"> <button class="sequence-nav-button button-previous">
<li class="prev"><button>Previous</button></li> <span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
<li class="next"><button>Next</button></li> <span>Previous</span>
</ul> </button>
</nav> <button class="sequence-nav-button button-next">
<span>Next</span>
<span class="icon fa fa-chevron-next" aria-hidden="true"></span>
</button>
</nav>
</div>
</div> </div>
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
!time_spec.js !time_spec.js
!collapsible_spec.js !collapsible_spec.js
!xmodule_spec.js !xmodule_spec.js
!sequence/display_spec.js
/* 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);
...@@ -38,6 +38,12 @@ ...@@ -38,6 +38,12 @@
this.displayTabTooltip = function(event) { this.displayTabTooltip = function(event) {
return Sequence.prototype.displayTabTooltip.apply(self, [event]); return Sequence.prototype.displayTabTooltip.apply(self, [event]);
}; };
this.arrowKeys = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
};
this.updatedProblems = {}; this.updatedProblems = {};
this.requestToken = $(element).data('request-token'); this.requestToken = $(element).data('request-token');
...@@ -52,6 +58,7 @@ ...@@ -52,6 +58,7 @@
this.nextUrl = this.el.data('next-url'); this.nextUrl = this.el.data('next-url');
this.prevUrl = this.el.data('prev-url'); this.prevUrl = this.el.data('prev-url');
this.base_page_title = ' | ' + document.title; this.base_page_title = ' | ' + document.title;
this.keydownHandler($(element).find('#sequence-list .tab'));
this.bind(); this.bind();
this.render(parseInt(this.el.data('position'), 10)); this.render(parseInt(this.el.data('position'), 10));
} }
...@@ -62,12 +69,63 @@ ...@@ -62,12 +69,63 @@
Sequence.prototype.bind = function() { Sequence.prototype.bind = function() {
this.$('#sequence-list .nav-item').click(this.goto); 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:add', this.addBookmarkIconToActiveNavItem);
this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem); this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem);
this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip); this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip);
this.$('#sequence-list .nav-item').on('blur mouseleave', this.hideTabTooltip); 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) { Sequence.prototype.displayTabTooltip = function(event) {
$(event.currentTarget).find('.sequence-tooltip').removeClass('sr'); $(event.currentTarget).find('.sequence-tooltip').removeClass('sr');
}; };
...@@ -317,13 +375,22 @@ ...@@ -317,13 +375,22 @@
Sequence.prototype.mark_visited = function(position) { Sequence.prototype.mark_visited = function(position) {
// Don't overwrite class attribute to avoid changing Progress class // Don't overwrite class attribute to avoid changing Progress class
var element = this.link_for(position); 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) { Sequence.prototype.mark_active = function(position) {
// Don't overwrite class attribute to avoid changing Progress class // Don't overwrite class attribute to avoid changing Progress class
var element = this.link_for(position); 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) { Sequence.prototype.addBookmarkIconToActiveNavItem = function(event) {
......
...@@ -125,7 +125,7 @@ class SplitTestBase(SharedModuleStoreTestCase): ...@@ -125,7 +125,7 @@ class SplitTestBase(SharedModuleStoreTestCase):
content = resp.content content = resp.content
# Assert we see the proper icon in the top display # Assert we see the proper icon in the top display
self.assertIn('<button class="{} inactive nav-item"'.format(self.ICON_CLASSES[user_tag]), content) self.assertIn('<button class="{} inactive nav-item tab"'.format(self.ICON_CLASSES[user_tag]), content)
# And proper tooltips # And proper tooltips
for tooltip in self.TOOLTIPS[user_tag]: for tooltip in self.TOOLTIPS[user_tag]:
self.assertIn(tooltip, content) self.assertIn(tooltip, content)
......
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
'xblock/lms.runtime.v1': 'lms/js/xblock/lms.runtime.v1', 'xblock/lms.runtime.v1': 'lms/js/xblock/lms.runtime.v1',
'xblock': 'common/js/xblock', 'xblock': 'common/js/xblock',
'capa/display': 'xmodule_js/src/capa/display', 'capa/display': 'xmodule_js/src/capa/display',
'sequence/display': 'xmodule_js/src/sequence/display',
'string_utils': 'xmodule_js/common_static/js/src/string_utils', 'string_utils': 'xmodule_js/common_static/js/src/string_utils',
'logger': 'xmodule_js/common_static/js/src/logger', 'logger': 'xmodule_js/common_static/js/src/logger',
'Markdown.Converter': 'js/Markdown.Converter', 'Markdown.Converter': 'js/Markdown.Converter',
......
...@@ -17,16 +17,22 @@ ...@@ -17,16 +17,22 @@
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span> <span class="icon fa fa-chevron-prev" aria-hidden="true"></span>
<span>${_('Previous')}</span> <span>${_('Previous')}</span>
</button> </button>
<nav class="sequence-list-wrapper" aria-label="${_('Unit')}"> <nav class="sequence-list-wrapper" aria-label="${_('Sequence')}">
<ol id="sequence-list"> <ol id="sequence-list" role="tablist">
% for idx, item in enumerate(items): % for idx, item in enumerate(items):
<li> <li>
<button class="seq_${item['type']} inactive nav-item" <button class="seq_${item['type']} inactive nav-item tab"
data-id="${item['id']}" role="tab"
data-element="${idx+1}" tabindex="-1"
data-page-title="${item['page_title']}" aria-selected="false"
data-path="${item['path']}" aria-expanded="false"
id="tab_${idx}"> aria-controls="seq_content"
data-index="${idx}"
data-id="${item['id']}"
data-element="${idx+1}"
data-page-title="${item['page_title']}"
data-path="${item['path']}"
id="tab_${idx}">
<span class="icon fa seq_${item['type']}" aria-hidden="true"></span> <span class="icon fa seq_${item['type']}" aria-hidden="true"></span>
<span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span> <span class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></span>
<div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div> <div class="sequence-tooltip sr"><span class="sr">${item['type']}&nbsp;</span>${item['page_title']}<span class="sr bookmark-icon-sr">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></div>
...@@ -51,7 +57,7 @@ ...@@ -51,7 +57,7 @@
${item['content']} ${item['content']}
</div> </div>
% endfor % endfor
<div id="seq_content"></div> <div id="seq_content" role="tabpanel"></div>
<nav class="sequence-bottom" aria-label="${_('Section')}"> <nav class="sequence-bottom" aria-label="${_('Section')}">
<button class="sequence-nav-button button-previous"> <button class="sequence-nav-button button-previous">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment