Commit 53c98f10 by Chris Committed by GitHub

Merge pull request #12750 from edx/clrux/ac-486

AC-486 updating tabbed_view to use proper accessibility rules
parents 7147a694 298f168e
...@@ -4,9 +4,12 @@ ...@@ -4,9 +4,12 @@
define(['jquery', define(['jquery',
'underscore', 'underscore',
'backbone', 'backbone',
'common/js/components/views/tabbed_view' 'common/js/components/views/tabbed_view',
'jquery.simulate'
], ],
function($, _, Backbone, TabbedView) { function($, _, Backbone, TabbedView) {
var keys = $.simulate.keyCode;
var view, var view,
TestSubview = Backbone.View.extend({ TestSubview = Backbone.View.extend({
initialize: function (options) { initialize: function (options) {
...@@ -22,7 +25,7 @@ ...@@ -22,7 +25,7 @@
return view.$('.page-content-nav'); return view.$('.page-content-nav');
}, },
activeTabPanel = function () { activeTabPanel = function () {
return view.$('.tabpanel[aria-expanded="true"]'); return view.$('.tabpanel[aria-hidden="false"]');
}; };
describe('TabbedView component', function () { describe('TabbedView component', function () {
...@@ -39,21 +42,10 @@ ...@@ -39,21 +42,10 @@
}], }],
viewLabel: 'Tabs', viewLabel: 'Tabs',
}).render(); }).render();
// _.defer() is used to make calls to
// jQuery.focus() work in Chrome. _.defer()
// delays the execution of a function until the
// current call stack is clear. That behavior
// will cause tests to fail, so we'll instead
// make _.defer() immediately invoke its
// argument.
spyOn(_, 'defer').and.callFake(function (func) {
func();
});
}); });
it('can render itself', function () { it('can render itself', function () {
expect(view.$el.html()).toContain('<nav class="page-content-nav"'); expect(view.$el.html()).toContain('<div class="page-content-nav"');
}); });
it('shows its first tab by default', function () { it('shows its first tab by default', function () {
...@@ -73,66 +65,100 @@ ...@@ -73,66 +65,100 @@
}); });
it('marks the active tab as selected using aria attributes', function () { it('marks the active tab as selected using aria attributes', function () {
expect(view.$('.nav-item[data-index=0]')).toHaveAttr('aria-expanded', 'true'); expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
expect(view.$('.nav-item[data-index=1]')).toHaveAttr('aria-expanded', 'false'); 'aria-expanded': 'true',
'aria-selected': 'true',
'tabindex': '0'
});
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
'aria-expanded': 'false',
'aria-selected': 'false',
'tabindex': '-1'
});
view.$('.nav-item[data-index=1]').click(); view.$('.nav-item[data-index=1]').click();
expect(view.$('.nav-item[data-index=0]')).toHaveAttr('aria-expanded', 'false'); expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
expect(view.$('.nav-item[data-index=1]')).toHaveAttr('aria-expanded', 'true'); 'aria-expanded': 'false',
'aria-selected': 'false',
'tabindex': '-1'
});
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
'aria-expanded': 'true',
'aria-selected': 'true',
'tabindex': '0'
});
}); });
it('sets focus for screen readers', function () { it('works with keyboard navigation RIGHT and ENTER', function() {
spyOn($.fn, 'focus'); view.$('.nav-item[data-index=0]').focus();
view.$('.nav-item[data-url="test-2"]').click(); view.$('.nav-item[data-index=0]')
expect(view.$('.sr-is-focusable.test-2').focus).toHaveBeenCalled(); .simulate("keydown", { keyCode: keys.RIGHT })
.simulate("keydown", { keyCode: keys.ENTER });
expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
'aria-expanded': 'false',
'aria-selected': 'false',
'tabindex': '-1'
});
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
'aria-expanded': 'true',
'aria-selected': 'true',
'tabindex': '0'
});
}); });
describe('history', function() { it('works with keyboard navigation DOWN and ENTER', function() {
beforeEach(function () { view.$('.nav-item[data-index=0]').focus();
spyOn(Backbone.history, 'navigate').and.callThrough(); view.$('.nav-item[data-index=0]')
view = new TabbedView({ .simulate("keydown", { keyCode: keys.DOWN })
tabs: [{ .simulate("keydown", { keyCode: keys.ENTER });
url: 'test 1',
title: 'Test 1', expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
view: new TestSubview({text: 'this is test text'}) 'aria-expanded': 'false',
}, { 'aria-selected': 'false',
url: 'test 2', 'tabindex': '-1'
title: 'Test 2',
view: new TestSubview({text: 'other text'})
}],
router: new Backbone.Router({
routes: {
'test 1': function () {
view.setActiveTab(0);
},
'test 2': function () {
view.setActiveTab(1);
}
}
})
}).render();
Backbone.history.start();
}); });
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
afterEach(function () { 'aria-expanded': 'true',
view.router.navigate(''); 'aria-selected': 'true',
Backbone.history.stop(); 'tabindex': '0'
}); });
});
it('updates the page URL on tab switches without adding to browser history', function () {
view.$('.nav-item[data-index=1]').click(); it('works with keyboard navigation LEFT and ENTER', function() {
expect(Backbone.history.navigate).toHaveBeenCalledWith( view.$('.nav-item[data-index=1]').focus();
'test 2', view.$('.nav-item[data-index=1]')
{replace: true} .simulate("keydown", { keyCode: keys.LEFT })
); .simulate("keydown", { keyCode: keys.ENTER });
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
'aria-expanded': 'false',
'aria-selected': 'false',
'tabindex': '-1'
}); });
expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
it('changes tabs on URL navigation', function () { 'aria-expanded': 'true',
expect(view.$('.nav-item.is-active').data('index')).toEqual(0); 'aria-selected': 'true',
Backbone.history.navigate('test 2', {trigger: true}); 'tabindex': '0'
expect(view.$('.nav-item.is-active').data('index')).toEqual(1); });
});
it('works with keyboard navigation UP and ENTER', function() {
view.$('.nav-item[data-index=1]').focus();
view.$('.nav-item[data-index=1]')
.simulate("keydown", { keyCode: keys.UP })
.simulate("keydown", { keyCode: keys.ENTER });
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
'aria-expanded': 'false',
'aria-selected': 'false',
'tabindex': '-1'
});
expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
'aria-expanded': 'true',
'aria-selected': 'true',
'tabindex': '0'
}); });
}); });
}); });
}); });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
<button class="nav-item tab" data-url="<%= url %>" data-index="<%= index %>" is-active="false" aria-expanded="false" aria-controls="<%= tabPanelId %>"><%= title %></button> <button role="tab" class="nav-item tab" data-url="<%= url %>" data-index="<%= index %>" aria-selected="false" aria-expanded="false" aria-controls="<%= tabPanelId %>" tabindex="-1" id="tab-<%= index %>"><%= title %></button>
<nav class="page-content-nav" aria-label="<%- viewLabel %>"></nav> <div class="page-content-nav" role="tablist"></div>
<div class="page-content-main"> <div class="page-content-main">
<div class="tabs"></div> <div class="tabs"></div>
</div> </div>
<div class="tabpanel is-hidden" id="<%= tabId %>" aria-expanded="false"> <div role="tabpanel" class="tabpanel is-hidden" id="<%= tabId %>" aria-labelledby="tab-<%= index %>" aria-hidden="true" tabindex="0"></div>
<div class="sr-is-focusable <%= tabId %>" tabindex="-1"></div>
</div>
...@@ -823,6 +823,7 @@ class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest): ...@@ -823,6 +823,7 @@ class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest):
profile_page.a11y_audit.config.set_rules({ profile_page.a11y_audit.config.set_rules({
"ignore": [ "ignore": [
'link-href', # TODO: AC-231 'link-href', # TODO: AC-231
'color-contrast', # TODO: AC-231
], ],
}) })
profile_page.display_accomplishments() profile_page.display_accomplishments()
......
...@@ -71,9 +71,9 @@ define([ ...@@ -71,9 +71,9 @@ define([
expect(teamsTabView.$('.breadcrumbs').length).toBe(0); expect(teamsTabView.$('.breadcrumbs').length).toBe(0);
}); });
it('does not interfere with anchor links to #content', function() { it('does not interfere with anchor links to #main', function () {
var teamsTabView = createTeamsTabView(this); var teamsTabView = createTeamsTabView(this);
teamsTabView.router.navigate('#content', {trigger: true}); teamsTabView.router.navigate('#main', {trigger: true});
expect(teamsTabView.$('.wrapper-msg')).toHaveClass('is-hidden'); expect(teamsTabView.$('.wrapper-msg')).toHaveClass('is-hidden');
}); });
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
router = this.router = new Backbone.Router(); router = this.router = new Backbone.Router();
_.each([ _.each([
[':default', _.bind(this.routeNotFound, this)], [':default', _.bind(this.routeNotFound, this)],
['content', _.bind(function () { ['main', _.bind(function () {
// The backbone router unfortunately usurps the // The backbone router unfortunately usurps the
// default behavior of in-page-links. This hack // default behavior of in-page-links. This hack
// prevents the screen reader in-page-link from // prevents the screen reader in-page-link from
...@@ -580,7 +580,7 @@ ...@@ -580,7 +580,7 @@
/** /**
* Set up the tabbed view and switch tabs. * Set up the tabbed view and switch tabs.
*/ */
goToTab: function (tab) { goToTab: function(tab) {
this.mainView = this.tabbedView; this.mainView = this.tabbedView;
// Note that `render` should be called first so // Note that `render` should be called first so
// that the tabbed view's element is set // that the tabbed view's element is set
......
<div class="sr-is-focusable sr-teams-view" tabindex="-1"></div> <div class="sr-is-focusable sr-teams-view" tabindex="-1" aria-label="Tab content"></div>
<div class="teams-paging-header"></div> <div class="teams-paging-header"></div>
<div class="teams-list"></div> <div class="teams-list"></div>
<div class="teams-paging-footer"></div> <div class="teams-paging-footer"></div>
<div class="sr-is-focusable sr-topics-view" tabindex="-1"></div> <div class="sr-is-focusable sr-topics-view" tabindex="-1" aria-label="Tab content"></div>
<div class="topics-paging-header"></div> <div class="topics-paging-header"></div>
<div class="topics-list"></div> <div class="topics-list"></div>
<div class="topics-paging-footer"></div> <div class="topics-paging-footer"></div>
...@@ -22,8 +22,10 @@ from openedx.core.djangolib.js_utils import ( ...@@ -22,8 +22,10 @@ from openedx.core.djangolib.js_utils import (
<div class="container"> <div class="container">
<div class="teams-wrapper"> <div class="teams-wrapper">
<section class="teams-content"> <main id="main" aria-label="Content" tabindex="-1">
</section> <section class="teams-content">
</section>
</main>
</div> </div>
</div> </div>
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
// +utility navigation // +utility navigation
// +toggling utilities // +toggling utilities
// +case - calculator spacing // +case - calculator spacing
// +tabs
// +notes: // +notes:
// -------------------- // --------------------
...@@ -128,26 +129,43 @@ ...@@ -128,26 +129,43 @@
%page-content-nav { %page-content-nav {
margin-bottom: $baseline; margin-bottom: $baseline;
border-bottom: 3px solid $gray-l5; border-bottom: 1px solid $gray-l5;
.nav-item { .nav-item {
@extend %button-reset; @extend %button-reset;
display: inline-block; display: inline-block;
margin-bottom: -3px; // to match the border
border-bottom: 3px solid $gray-l5;
padding: ($baseline*.75); padding: ($baseline*.75);
color: $gray-d2; color: $gray-d2;
&.is-active { &.is-active {
border-bottom: 3px solid $gray-d2; border-bottom: 4px solid $link-color;
color: $gray-d2; color: $gray-d2;
} }
// STATE: hover and focus // STATE: hover and focus
&:hover, &:hover,
&:focus { &:focus {
border-bottom: 3px solid $link-color; border-bottom: 4px solid $link-color;
color: $link-color; color: $link-color;
} }
} }
} }
// +tabs - styles for tabs and tabpanels (teams and learner profile, currently)
// --------------------
.page-content-nav {
.tab {
}
}
.page-content-main {
.tabs {
.tabpanel {
outline: none;
}
}
}
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