Merge pull request #12750 from edx/clrux/ac-486
AC-486 updating tabbed_view to use proper accessibility rules
This commit is contained in:
@@ -1,138 +1,238 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['backbone',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'text!common/templates/components/tabbed_view.underscore',
|
||||
'text!common/templates/components/tab.underscore',
|
||||
'text!common/templates/components/tabpanel.underscore',
|
||||
], function (
|
||||
Backbone,
|
||||
_,
|
||||
$,
|
||||
tabbedViewTemplate,
|
||||
tabTemplate,
|
||||
tabPanelTemplate
|
||||
) {
|
||||
var getTabPanelId = function (id) {
|
||||
return 'tabpanel-' + id;
|
||||
};
|
||||
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'edx-ui-toolkit/js/utils/constants',
|
||||
'text!common/templates/components/tabbed_view.underscore',
|
||||
'text!common/templates/components/tab.underscore',
|
||||
'text!common/templates/components/tabpanel.underscore',
|
||||
], function (
|
||||
Backbone,
|
||||
_,
|
||||
$,
|
||||
Constants,
|
||||
tabbedViewTemplate,
|
||||
tabTemplate,
|
||||
tabPanelTemplate
|
||||
) {
|
||||
|
||||
var getTabPanelId = function (id) {
|
||||
return 'tabpanel-' + id;
|
||||
};
|
||||
|
||||
var TabPanelView = Backbone.View.extend({
|
||||
template: _.template(tabPanelTemplate),
|
||||
initialize: function (options) {
|
||||
this.url = options.url;
|
||||
this.view = options.view;
|
||||
this.index = options.index;
|
||||
},
|
||||
render: function() {
|
||||
var tabPanelHtml = this.template({
|
||||
tabId: getTabPanelId(this.url),
|
||||
index: this.index
|
||||
});
|
||||
this.setElement($(tabPanelHtml));
|
||||
this.$el.append(this.view.render().el);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var TabbedView = Backbone.View.extend({
|
||||
events: {
|
||||
'click .tab': 'switchTab',
|
||||
'keydown .tab': 'keydownHandler'
|
||||
},
|
||||
|
||||
/**
|
||||
* View for a tabbed interface. Expects a list of tabs
|
||||
* in its options object, each of which should contain the
|
||||
* following properties:
|
||||
* view (Backbone.View): the view to render for this tab.
|
||||
* title (string): The title to display for this tab.
|
||||
* url (string): The URL fragment which will
|
||||
* navigate to this tab when a router is
|
||||
* provided.
|
||||
* If a router is passed in (via options.router),
|
||||
* use that router to keep track of history between
|
||||
* tabs. Backbone.history.start() must be called
|
||||
* by the router's instantiator after this view is
|
||||
* initialized.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
this.router = options.router || null;
|
||||
this.tabs = options.tabs;
|
||||
this.template = _.template(tabbedViewTemplate)({
|
||||
viewLabel: this.viewLabel
|
||||
});
|
||||
// Convert each view into a TabPanelView
|
||||
_.each(this.tabs, function(tabInfo, index) {
|
||||
tabInfo.view = new TabPanelView({
|
||||
url: tabInfo.url,
|
||||
view: tabInfo.view,
|
||||
index: index
|
||||
});
|
||||
}, this);
|
||||
this.urlMap = _.reduce(this.tabs, function (map, value) {
|
||||
map[value.url] = value;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
render: function () {
|
||||
var self = this;
|
||||
this.$el.html(this.template);
|
||||
_.each(this.tabs, function(tabInfo, index) {
|
||||
var tabEl = $(_.template(tabTemplate)({
|
||||
index: index,
|
||||
title: tabInfo.title,
|
||||
url: tabInfo.url,
|
||||
tabPanelId: getTabPanelId(tabInfo.url)
|
||||
})),
|
||||
tabContainerEl = this.$('.tabs');
|
||||
self.$('.page-content-nav').append(tabEl);
|
||||
|
||||
// Render and append the current tab panel
|
||||
tabContainerEl.append(tabInfo.view.render().$el);
|
||||
}, this);
|
||||
// Re-display the default (first) tab if the
|
||||
// current route does not belong to one of the
|
||||
// tabs. Otherwise continue displaying the tab
|
||||
// corresponding to the current URL.
|
||||
if (!(Backbone.history.getHash() in this.urlMap)) {
|
||||
this.setActiveTab(0);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
setActiveTab: function(index) {
|
||||
var tabMeta = this.getTabMeta(index),
|
||||
tab = tabMeta.tab,
|
||||
view = tab.view,
|
||||
$tabEl = tabMeta.element;
|
||||
|
||||
// Hide old tab/tabpanel
|
||||
this.$('button.is-active')
|
||||
.removeClass('is-active')
|
||||
.attr({
|
||||
'aria-expanded': 'false',
|
||||
'aria-selected': 'false',
|
||||
'tabindex': '-1'
|
||||
});
|
||||
|
||||
this.$('.tabpanel[aria-hidden="false"]')
|
||||
.addClass('is-hidden')
|
||||
.attr({
|
||||
'aria-hidden': 'true'
|
||||
});
|
||||
|
||||
// Show new tab/tabpanel
|
||||
if (this.router) {
|
||||
this.router.navigate(tab.url, { replace: true });
|
||||
}
|
||||
|
||||
$tabEl
|
||||
.addClass('is-active')
|
||||
.attr({
|
||||
'aria-expanded': 'true',
|
||||
'aria-selected': 'true',
|
||||
'tabindex': '0'
|
||||
});
|
||||
|
||||
view.$el
|
||||
.removeClass('is-hidden')
|
||||
.attr({
|
||||
'aria-hidden': 'false',
|
||||
});
|
||||
},
|
||||
|
||||
switchTab: function(event) {
|
||||
event.preventDefault();
|
||||
this.setActiveTab($(event.currentTarget).data('index'));
|
||||
},
|
||||
|
||||
previousTab: function(focused, index) {
|
||||
var $tab, $panel;
|
||||
|
||||
if (index === 0) {
|
||||
$tab = $(focused).parent().find('.tab').last();
|
||||
} else {
|
||||
$tab = $(focused).parent().find('.tab:eq(' + index + ')').prev();
|
||||
}
|
||||
|
||||
$panel = $($tab).data('index');
|
||||
|
||||
$tab.focus();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
nextTab: function(focused, index, total) {
|
||||
var $tab, $panel;
|
||||
|
||||
if (index === total) {
|
||||
$tab = $(focused).parent().find('.tab').first();
|
||||
} else {
|
||||
$tab = $(focused).parent().find('.tab:eq(' + index + ')').next();
|
||||
}
|
||||
|
||||
$panel = $($tab).data('index');
|
||||
|
||||
$tab.focus();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
keydownHandler: function(event) {
|
||||
var key = event.which,
|
||||
focused = $(event.currentTarget),
|
||||
index = $(focused).parent().find('.tab').index(focused),
|
||||
total = $(focused).parent().find('.tab').size() - 1,
|
||||
$tab = $(focused).data('index');
|
||||
|
||||
switch (key) {
|
||||
case Constants.keyCodes.left:
|
||||
case Constants.keyCodes.up:
|
||||
event.preventDefault();
|
||||
this.previousTab(focused, index);
|
||||
break;
|
||||
|
||||
case Constants.keyCodes.right:
|
||||
case Constants.keyCodes.down:
|
||||
event.preventDefault();
|
||||
this.nextTab(focused, index, total);
|
||||
break;
|
||||
|
||||
case Constants.keyCodes.enter:
|
||||
case Constants.keyCodes.space:
|
||||
this.setActiveTab($tab);
|
||||
break;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the tab by name or index. Returns an object
|
||||
* encapsulating the tab object and its element.
|
||||
*/
|
||||
getTabMeta: function (tabNameOrIndex) {
|
||||
var tab, $element;
|
||||
|
||||
var TabPanelView = Backbone.View.extend({
|
||||
template: _.template(tabPanelTemplate),
|
||||
initialize: function (options) {
|
||||
this.url = options.url;
|
||||
this.view = options.view;
|
||||
},
|
||||
render: function () {
|
||||
var tabPanelHtml = this.template({tabId: getTabPanelId(this.url)});
|
||||
this.setElement($(tabPanelHtml));
|
||||
this.$el.append(this.view.render().el);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var TabbedView = Backbone.View.extend({
|
||||
events: {
|
||||
'click .nav-item.tab': 'switchTab'
|
||||
},
|
||||
|
||||
/**
|
||||
* View for a tabbed interface. Expects a list of tabs
|
||||
* in its options object, each of which should contain the
|
||||
* following properties:
|
||||
* view (Backbone.View): the view to render for this tab.
|
||||
* title (string): The title to display for this tab.
|
||||
* url (string): The URL fragment which will
|
||||
* navigate to this tab when a router is
|
||||
* provided.
|
||||
* If a router is passed in (via options.router),
|
||||
* use that router to keep track of history between
|
||||
* tabs. Backbone.history.start() must be called
|
||||
* by the router's instantiator after this view is
|
||||
* initialized.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
this.router = options.router || null;
|
||||
this.tabs = options.tabs;
|
||||
this.template = _.template(tabbedViewTemplate)({viewLabel: options.viewLabel});
|
||||
// Convert each view into a TabPanelView
|
||||
_.each(this.tabs, function (tabInfo) {
|
||||
tabInfo.view = new TabPanelView({url: tabInfo.url, view: tabInfo.view});
|
||||
}, this);
|
||||
this.urlMap = _.reduce(this.tabs, function (map, value) {
|
||||
map[value.url] = value;
|
||||
return map;
|
||||
}, {});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var self = this;
|
||||
this.$el.html(this.template);
|
||||
_.each(this.tabs, function(tabInfo, index) {
|
||||
var tabEl = $(_.template(tabTemplate)({
|
||||
index: index,
|
||||
title: tabInfo.title,
|
||||
url: tabInfo.url,
|
||||
tabPanelId: getTabPanelId(tabInfo.url)
|
||||
})),
|
||||
tabContainerEl = this.$('.tabs');
|
||||
self.$('.page-content-nav').append(tabEl);
|
||||
|
||||
// Render and append the current tab panel
|
||||
tabContainerEl.append(tabInfo.view.render().$el);
|
||||
}, this);
|
||||
// Re-display the default (first) tab if the
|
||||
// current route does not belong to one of the
|
||||
// tabs. Otherwise continue displaying the tab
|
||||
// corresponding to the current URL.
|
||||
if (!(Backbone.history.getHash() in this.urlMap)) {
|
||||
this.setActiveTab(0);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
setActiveTab: function (index) {
|
||||
var tabMeta = this.getTabMeta(index),
|
||||
tab = tabMeta.tab,
|
||||
tabEl = tabMeta.element,
|
||||
view = tab.view;
|
||||
// Hide old tab/tabpanel
|
||||
this.$('button.is-active').removeClass('is-active').attr('aria-expanded', 'false');
|
||||
this.$('.tabpanel[aria-expanded="true"]').attr('aria-expanded', 'false').addClass('is-hidden');
|
||||
// Show new tab/tabpanel
|
||||
tabEl.addClass('is-active').attr('aria-expanded', 'true');
|
||||
view.$el.attr('aria-expanded', 'true').removeClass('is-hidden');
|
||||
// This bizarre workaround makes focus work in Chrome.
|
||||
_.defer(function () {
|
||||
view.$('.sr-is-focusable.' + getTabPanelId(tab.url)).focus();
|
||||
});
|
||||
if (this.router) {
|
||||
this.router.navigate(tab.url, {replace: true});
|
||||
}
|
||||
},
|
||||
|
||||
switchTab: function (event) {
|
||||
event.preventDefault();
|
||||
this.setActiveTab($(event.currentTarget).data('index'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the tab by name or index. Returns an object
|
||||
* encapsulating the tab object and its element.
|
||||
*/
|
||||
getTabMeta: function (tabNameOrIndex) {
|
||||
var tab, element;
|
||||
if (typeof tabNameOrIndex === 'string') {
|
||||
tab = this.urlMap[tabNameOrIndex];
|
||||
element = this.$('button[data-url='+tabNameOrIndex+']');
|
||||
} else {
|
||||
tab = this.tabs[tabNameOrIndex];
|
||||
element = this.$('button[data-index='+tabNameOrIndex+']');
|
||||
}
|
||||
return {'tab': tab, 'element': element};
|
||||
}
|
||||
});
|
||||
return TabbedView;
|
||||
});
|
||||
if (typeof tabNameOrIndex === 'string') {
|
||||
tab = this.urlMap[tabNameOrIndex];
|
||||
$element = this.$('button[data-url='+tabNameOrIndex+']');
|
||||
} else {
|
||||
tab = this.tabs[tabNameOrIndex];
|
||||
$element = this.$('button[data-index='+tabNameOrIndex+']');
|
||||
}
|
||||
return {'tab': tab, 'element': $element};
|
||||
}
|
||||
});
|
||||
return TabbedView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
define(['jquery',
|
||||
'underscore',
|
||||
'backbone',
|
||||
'common/js/components/views/tabbed_view'
|
||||
'common/js/components/views/tabbed_view',
|
||||
'jquery.simulate'
|
||||
],
|
||||
function($, _, Backbone, TabbedView) {
|
||||
var keys = $.simulate.keyCode;
|
||||
|
||||
var view,
|
||||
TestSubview = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
@@ -22,7 +25,7 @@
|
||||
return view.$('.page-content-nav');
|
||||
},
|
||||
activeTabPanel = function () {
|
||||
return view.$('.tabpanel[aria-expanded="true"]');
|
||||
return view.$('.tabpanel[aria-hidden="false"]');
|
||||
};
|
||||
|
||||
describe('TabbedView component', function () {
|
||||
@@ -39,21 +42,10 @@
|
||||
}],
|
||||
viewLabel: 'Tabs',
|
||||
}).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 () {
|
||||
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 () {
|
||||
@@ -73,66 +65,100 @@
|
||||
});
|
||||
|
||||
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=1]')).toHaveAttr('aria-expanded', 'false');
|
||||
expect(view.$('.nav-item[data-index=0]')).toHaveAttr({
|
||||
'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();
|
||||
expect(view.$('.nav-item[data-index=0]')).toHaveAttr('aria-expanded', 'false');
|
||||
expect(view.$('.nav-item[data-index=1]')).toHaveAttr('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('sets focus for screen readers', function () {
|
||||
spyOn($.fn, 'focus');
|
||||
view.$('.nav-item[data-url="test-2"]').click();
|
||||
expect(view.$('.sr-is-focusable.test-2').focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('history', function() {
|
||||
beforeEach(function () {
|
||||
spyOn(Backbone.history, 'navigate').and.callThrough();
|
||||
view = new TabbedView({
|
||||
tabs: [{
|
||||
url: 'test 1',
|
||||
title: 'Test 1',
|
||||
view: new TestSubview({text: 'this is test text'})
|
||||
}, {
|
||||
url: 'test 2',
|
||||
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=0]')).toHaveAttr({
|
||||
'aria-expanded': 'false',
|
||||
'aria-selected': 'false',
|
||||
'tabindex': '-1'
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
view.router.navigate('');
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
it('updates the page URL on tab switches without adding to browser history', function () {
|
||||
view.$('.nav-item[data-index=1]').click();
|
||||
expect(Backbone.history.navigate).toHaveBeenCalledWith(
|
||||
'test 2',
|
||||
{replace: true}
|
||||
);
|
||||
});
|
||||
|
||||
it('changes tabs on URL navigation', function () {
|
||||
expect(view.$('.nav-item.is-active').data('index')).toEqual(0);
|
||||
Backbone.history.navigate('test 2', {trigger: true});
|
||||
expect(view.$('.nav-item.is-active').data('index')).toEqual(1);
|
||||
expect(view.$('.nav-item[data-index=1]')).toHaveAttr({
|
||||
'aria-expanded': 'true',
|
||||
'aria-selected': 'true',
|
||||
'tabindex': '0'
|
||||
});
|
||||
});
|
||||
|
||||
it('works with keyboard navigation RIGHT and ENTER', function() {
|
||||
view.$('.nav-item[data-index=0]').focus();
|
||||
view.$('.nav-item[data-index=0]')
|
||||
.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'
|
||||
});
|
||||
});
|
||||
|
||||
it('works with keyboard navigation DOWN and ENTER', function() {
|
||||
view.$('.nav-item[data-index=0]').focus();
|
||||
view.$('.nav-item[data-index=0]')
|
||||
.simulate("keydown", { keyCode: keys.DOWN })
|
||||
.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'
|
||||
});
|
||||
});
|
||||
|
||||
it('works with keyboard navigation LEFT and ENTER', function() {
|
||||
view.$('.nav-item[data-index=1]').focus();
|
||||
view.$('.nav-item[data-index=1]')
|
||||
.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({
|
||||
'aria-expanded': 'true',
|
||||
'aria-selected': 'true',
|
||||
'tabindex': '0'
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -1 +1 @@
|
||||
<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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav class="page-content-nav" aria-label="<%- viewLabel %>"></nav>
|
||||
<div class="page-content-nav" role="tablist"></div>
|
||||
<div class="page-content-main">
|
||||
<div class="tabs"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
<div class="tabpanel is-hidden" id="<%= tabId %>" aria-expanded="false">
|
||||
<div class="sr-is-focusable <%= tabId %>" tabindex="-1"></div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tabpanel is-hidden" id="<%= tabId %>" aria-labelledby="tab-<%= index %>" aria-hidden="true" tabindex="0"></div>
|
||||
|
||||
@@ -823,6 +823,7 @@ class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest):
|
||||
profile_page.a11y_audit.config.set_rules({
|
||||
"ignore": [
|
||||
'link-href', # TODO: AC-231
|
||||
'color-contrast', # TODO: AC-231
|
||||
],
|
||||
})
|
||||
profile_page.display_accomplishments()
|
||||
|
||||
@@ -71,9 +71,9 @@ define([
|
||||
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);
|
||||
teamsTabView.router.navigate('#content', {trigger: true});
|
||||
teamsTabView.router.navigate('#main', {trigger: true});
|
||||
expect(teamsTabView.$('.wrapper-msg')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
router = this.router = new Backbone.Router();
|
||||
_.each([
|
||||
[':default', _.bind(this.routeNotFound, this)],
|
||||
['content', _.bind(function () {
|
||||
['main', _.bind(function () {
|
||||
// The backbone router unfortunately usurps the
|
||||
// default behavior of in-page-links. This hack
|
||||
// prevents the screen reader in-page-link from
|
||||
@@ -580,7 +580,7 @@
|
||||
/**
|
||||
* Set up the tabbed view and switch tabs.
|
||||
*/
|
||||
goToTab: function (tab) {
|
||||
goToTab: function(tab) {
|
||||
this.mainView = this.tabbedView;
|
||||
// Note that `render` should be called first so
|
||||
// that the tabbed view's element is set
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<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-list"></div>
|
||||
<div class="teams-paging-footer"></div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<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-list"></div>
|
||||
<div class="topics-paging-footer"></div>
|
||||
|
||||
@@ -22,8 +22,10 @@ from openedx.core.djangolib.js_utils import (
|
||||
|
||||
<div class="container">
|
||||
<div class="teams-wrapper">
|
||||
<section class="teams-content">
|
||||
</section>
|
||||
<main id="main" aria-label="Content" tabindex="-1">
|
||||
<section class="teams-content">
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
// +utility navigation
|
||||
// +toggling utilities
|
||||
// +case - calculator spacing
|
||||
// +tabs
|
||||
|
||||
// +notes:
|
||||
// --------------------
|
||||
@@ -128,26 +129,43 @@
|
||||
|
||||
%page-content-nav {
|
||||
margin-bottom: $baseline;
|
||||
border-bottom: 3px solid $gray-l5;
|
||||
border-bottom: 1px solid $gray-l5;
|
||||
|
||||
.nav-item {
|
||||
@extend %button-reset;
|
||||
display: inline-block;
|
||||
margin-bottom: -3px; // to match the border
|
||||
border-bottom: 3px solid $gray-l5;
|
||||
padding: ($baseline*.75);
|
||||
color: $gray-d2;
|
||||
|
||||
&.is-active {
|
||||
border-bottom: 3px solid $gray-d2;
|
||||
border-bottom: 4px solid $link-color;
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
// STATE: hover and focus
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom: 3px solid $link-color;
|
||||
border-bottom: 4px solid $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user