Commit 26bbbb96 by Peter Fogg Committed by Daniel Friedman

Fix various team accessibility issues.

Authors:
  - Peter Fogg
  - Daniel Friedman

TNL-1953
parent 00eb18a1
/**
* A base class for a view which renders and paginates a collection,
* along with a header and footer displaying controls for
* pagination.
*
* Subclasses should define a `type` property which will be used to
* create class names for the different subcomponents, as well as an
* `itemViewClass` which will be used to display each individual
* element of the collection.
*
* If provided, the `srInfo` property will be used to provide
* information for screen readers on each item. The `srInfo.text`
* property will be shown in the header, and the `srInfo.id` property
* will be used to connect each card's title with the header text via
* the ARIA describedby attribute.
*/
;(function(define) {
'use strict';
define([
......@@ -24,7 +40,7 @@
},
createHeaderView: function() {
return new PagingHeader({collection: this.options.collection});
return new PagingHeader({collection: this.options.collection, srInfo: this.srInfo});
},
createFooterView: function() {
......
......@@ -8,6 +8,7 @@
], function (Backbone, _, gettext, headerTemplate) {
var PagingHeader = Backbone.View.extend({
initialize: function (options) {
this.srInfo = options.srInfo;
this.collections = options.collection;
this.collection.bind('add', _.bind(this.render, this));
this.collection.bind('remove', _.bind(this.render, this));
......@@ -28,7 +29,10 @@
context, true
);
}
this.$el.html(_.template(headerTemplate, {message: message}));
this.$el.html(_.template(headerTemplate, {
message: message,
srInfo: this.srInfo
}));
return this;
}
});
......
<div class="sr-is-focusable sr-<%= type %>-view" tabindex="-1"></div>
<div class="<%= type %>-paging-header"></div>
<div class="<%= type %>-list"></div>
<ul class="<%= type %>-list"></ul>
<div class="<%= type %>-paging-footer"></div>
<% if (!_.isUndefined(srInfo)) { %>
<h2 class="sr" id="<%= srInfo.id %>"><%- srInfo.text %></h2>
<% } %>
<div class="search-tools">
<span class="search-count">
<%= message %>
......
......@@ -145,7 +145,7 @@ class FieldsMixin(object):
return self.value_for_text_field(field_id)
def value_for_text_field(self, field_id, value=None):
def value_for_text_field(self, field_id, value=None, press_enter=True):
"""
Get or set the value of a text field.
"""
......@@ -159,7 +159,8 @@ class FieldsMixin(object):
current_value = query.attrs('value')[0]
query.results[0].send_keys(u'\ue003' * len(current_value)) # Delete existing value.
query.results[0].send_keys(value) # Input new value
query.results[0].send_keys(u'\ue007') # Press Enter
if press_enter:
query.results[0].send_keys(u'\ue007') # Press Enter
return query.attrs('value')[0]
def value_for_textarea_field(self, field_id, value=None):
......
......@@ -180,12 +180,12 @@ class TeamsTabTest(TeamsTabBase):
self.verify_teams_present(True)
@ddt.data(
('browse', 'div.topics-list'),
('browse', '.topics-list'),
# TODO: find a reliable way to match the "My Teams" tab
# ('my-teams', 'div.teams-list'),
('teams/{topic_id}/{team_id}', 'div.discussion-module'),
('topics/{topic_id}/create-team', 'div.create-team-instructions'),
('topics/{topic_id}', 'div.teams-list'),
('topics/{topic_id}', '.teams-list'),
('not-a-real-route', 'div.warning')
)
@ddt.unpack
......@@ -612,7 +612,7 @@ class CreateTeamTest(TeamsTabBase):
def fill_create_form(self):
"""Fill the create team form fields with appropriate values."""
self.create_team_page.value_for_text_field(field_id='name', value=self.team_name)
self.create_team_page.value_for_text_field(field_id='name', value=self.team_name, press_enter=False)
self.create_team_page.value_for_textarea_field(
field_id='description',
value='The Avengers are a fictional team of superheroes.'
......@@ -691,7 +691,8 @@ class CreateTeamTest(TeamsTabBase):
'transform themselves through cutting-edge technologies, innovative pedagogy, and '
'rigorous courses. More than 70 schools, nonprofits, corporations, and international'
'organizations offer or plan to offer courses on the edX website. As of 22 October 2014,'
'edX has more than 4 million users taking more than 500 courses online.'
'edX has more than 4 million users taking more than 500 courses online.',
press_enter=False
)
self.create_team_page.submit_form()
......
......@@ -72,6 +72,12 @@ define([
expectFocus(teamsTabView.$('.warning'));
});
it('does not interfere with anchor links to #content', function () {
var teamsTabView = createTeamsTabView();
teamsTabView.router.navigate('#content', {trigger: true});
expect(teamsTabView.$('.warning')).toHaveClass('is-hidden');
});
it('displays and focuses an error message when trying to navigate to a nonexistent topic', function () {
var requests = AjaxHelpers.requests(this),
teamsTabView = createTeamsTabView();
......
......@@ -15,6 +15,7 @@
events: {
'click .action-primary': 'createTeam',
'submit form': 'createTeam',
'click .action-cancel': 'goBackToTopic'
},
......@@ -48,13 +49,6 @@
helpMessage: gettext('A short description of the team to help other learners understand the goals or direction of the team (maximum 300 characters).')
});
this.optionalDescriptionField = new FieldViews.ReadonlyFieldView({
model: this.teamModel,
title: gettext('Optional Characteristics'),
valueAttribute: 'optional_description',
helpMessage: gettext('Help other learners decide whether to join your team by specifying some characteristics for your team. Choose carefully, because fewer people might be interested in joining your team if it seems too restrictive.')
});
this.teamLanguageField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext('Language'),
......@@ -82,7 +76,6 @@
this.$el.html(_.template(editTeamTemplate)({primaryButtonTitle: this.primaryButtonTitle}));
this.set(this.teamNameField, '.team-required-fields');
this.set(this.teamDescriptionField, '.team-required-fields');
this.set(this.optionalDescriptionField, '.team-optional-fields');
this.set(this.teamLanguageField, '.team-optional-fields');
this.set(this.teamCountryField, '.team-optional-fields');
return this;
......@@ -97,7 +90,8 @@
}
},
createTeam: function () {
createTeam: function (event) {
event.preventDefault();
var view = this,
teamLanguage = this.teamLanguageField.fieldValue(),
teamCountry = this.teamCountryField.fieldValue();
......
......@@ -93,10 +93,8 @@
true
);
},
action: function (event) {
var url = 'teams/' + this.teamModel().get('topic_id') + '/' + this.teamModel().get('id');
event.preventDefault();
this.router.navigate(url, {trigger: true});
actionUrl: function () {
return '#teams/' + this.teamModel().get('topic_id') + '/' + this.teamModel().get('id');
}
});
return TeamCardView;
......
......@@ -9,6 +9,15 @@
var TeamsView = PaginatedView.extend({
type: 'teams',
events: {
'click button.action': '' // entry point for team creation
},
srInfo: {
id: "heading-browse-teams",
text: gettext('All teams')
},
initialize: function (options) {
this.topic = options.topic;
this.teamMemberships = options.teamMemberships;
......@@ -18,7 +27,8 @@
topic: options.topic,
maxTeamSize: options.maxTeamSize,
countries: this.selectorOptionsArrayToHashWithBlank(options.teamParams.countries),
languages: this.selectorOptionsArrayToHashWithBlank(options.teamParams.languages)
languages: this.selectorOptionsArrayToHashWithBlank(options.teamParams.languages),
srInfo: this.srInfo
});
PaginatedView.prototype.initialize.call(this);
},
......
......@@ -22,6 +22,13 @@
TopicModel, TopicCollection, TeamModel, TeamCollection, TeamMembershipCollection,
TopicsView, TeamProfileView, MyTeamsView, TopicTeamsView, TeamEditView,
teamsTemplate) {
var TeamsHeaderModel = HeaderModel.extend({
initialize: function (attributes) {
_.extend(this.defaults, {nav_aria_label: gettext('teams')});
HeaderModel.prototype.initialize.call(this);
}
});
var ViewWithHeader = Backbone.View.extend({
initialize: function (options) {
this.header = options.header;
......@@ -57,6 +64,12 @@
router = this.router = new Backbone.Router();
_.each([
[':default', _.bind(this.routeNotFound, this)],
['content', _.bind(function () {
// The backbone router unfortunately usurps the
// default behavior of in-page-links. This hack
// prevents the screen reader in-page-link from
// being picked up by the backbone router.
}, this)],
['topics/:topic_id(/)', _.bind(this.browseTopic, this)],
['topics/:topic_id/create-team(/)', _.bind(this.newTeam, this)],
['teams/:topic_id/:team_id(/)', _.bind(this.browseTeam, this)],
......@@ -102,7 +115,7 @@
this.mainView = this.tabbedView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
model: new TeamsHeaderModel({
description: gettext("See all teams in your course, organized by topic. Join a team to collaborate with other learners who are interested in the same topic as you are."),
title: gettext("Teams")
})
......@@ -113,7 +126,11 @@
url: 'my-teams',
view: this.myTeamsView
}, {
title: gettext('Browse'),
title: interpolate(
// Translators: sr_start and sr_end surround text meant only for screen readers. The whole string will be shown to users as "Browse teams" if they are using a screenreader, and "Browse" otherwise.
gettext("Browse %(sr_start)s teams %(sr_end)s"),
{"sr_start": '<span class="sr">', "sr_end": '</span>'}, true
),
url: 'browse',
view: this.topicsView
}],
......@@ -165,7 +182,7 @@
this.getTeamsView(topicID).done(function (teamsView) {
self.mainView = new ViewWithHeader({
header: new HeaderView({
model: new HeaderModel({
model: new TeamsHeaderModel({
description: gettext("Create a new team if you can't find existing teams to join, or if you would like to learn with friends you know."),
title: gettext("Create a New Team"),
breadcrumbs: [
......@@ -277,7 +294,7 @@
});
}
headerView = new HeaderView({
model: new HeaderModel({
model: new TeamsHeaderModel({
description: subject.get('description'),
title: subject.get('name'),
breadcrumbs: breadcrumbs
......
......@@ -30,9 +30,8 @@
CardView.prototype.initialize.apply(this, arguments);
},
action: function (event) {
event.preventDefault();
this.router.navigate('topics/' + this.model.get('id'), {trigger: true});
actionUrl: function () {
return '#topics/' + this.model.get('id');
},
configuration: 'square_card',
......
......@@ -7,8 +7,16 @@
var TopicsView = PaginatedView.extend({
type: 'topics',
srInfo: {
id: "heading-browse-topics",
text: gettext("All topics")
},
initialize: function (options) {
this.itemViewClass = TopicCardView.extend({router: options.router});
this.itemViewClass = TopicCardView.extend({
router: options.router,
srInfo: this.srInfo
});
PaginatedView.prototype.initialize.call(this);
}
});
......
<form>
<div class="create-team wrapper-msg is-incontext urgency-low warning is-hidden" tabindex="-1">
<div class="msg">
<div class="msg-content">
......@@ -20,6 +21,16 @@
</div>
<div class="team-optional-fields">
<fieldset>
<div class="u-field u-field-optional_description">
<legend aria-describedby="optional-characteristics-help">
<p class="u-field-title"><%- gettext('Optional Characteristics') %></p>
</legend>
<span id="optional-characteristics-help" class="u-field-message">
<p class="u-field-message-help"><%- gettext('Help other learners decide whether to join your team by specifying some characteristics for your team. Choose carefully, because fewer people might be interested in joining your team if it seems too restrictive.') %></p>
</span>
</div>
</fieldset>
</div>
</div>
......@@ -45,3 +56,4 @@
%>
</button>
</div>
</form>
......@@ -23,6 +23,8 @@
'text!templates/components/card/card.underscore'],
function ($, _, Backbone, cardTemplate) {
var CardView = Backbone.View.extend({
tagName: 'li',
events: {
'click .action' : 'action'
},
......@@ -82,7 +84,8 @@
action_class: this.callIfFunction(this.actionClass),
action_url: this.callIfFunction(this.actionUrl),
action_content: this.callIfFunction(this.actionContent),
configuration: this.callIfFunction(this.configuration)
configuration: this.callIfFunction(this.configuration),
srInfo: this.srInfo
}));
var detailsEl = this.$el.find('.card-meta');
_.each(this.callIfFunction(this.details), function (detail) {
......
......@@ -8,7 +8,8 @@ define(['backbone'], function (Backbone) {
defaults: {
'title': '',
'description': '',
'breadcrumbs': null
'breadcrumbs': null,
'nav_aria_label': ''
}
});
......
......@@ -48,7 +48,11 @@
}));
self.$('.page-content-nav').append(tabEl);
});
if(Backbone.history.getHash() === "") {
// 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;
......@@ -70,7 +74,7 @@
view.setElement(this.$('.page-content-main')).render();
this.$('.sr-is-focusable.sr-tab').focus();
if (this.router) {
this.router.navigate(tab.url, {replace: true, trigger: true});
this.router.navigate(tab.url, {replace: true});
}
},
......
......@@ -13,7 +13,7 @@
var testBreadcrumbs = function (breadcrumbs) {
model.set('breadcrumbs', breadcrumbs);
expect(view.$el.html()).toContain('<nav class="breadcrumbs">');
expect(view.$('nav.breadcrumbs').length).toBe(1);
_.each(view.$('.nav-item'), function (el, index) {
expect($(el).attr('href')).toEqual(breadcrumbs[index].url);
expect($(el).text()).toEqual(breadcrumbs[index].title);
......
......@@ -101,7 +101,7 @@
view.$('.nav-item[data-index=1]').click();
expect(Backbone.history.navigate).toHaveBeenCalledWith(
'test 2',
{replace: true, trigger: true}
{replace: true}
);
});
......
......@@ -118,7 +118,7 @@
.u-field-message-help,
.u-field-message-notification {
color: $gray-l1;
color: $gray;
}
}
......
......@@ -4,7 +4,12 @@
<% if (pennant) { %>
<small class="card-type"><%- pennant %></small>
<% } %>
<h3 class="card-title"><%- title %></h3>
<h3 class="card-title"
<% if (!_.isUndefined(srInfo)) { %>
aria-describedby="<%= srInfo.id %>"
<% } %>
><%- title %>
</h3>
<p class="card-description"><%- description %></p>
</div>
</div>
......@@ -21,7 +26,12 @@
<% if (pennant) { %>
<small class="card-type"><%- pennant %></small>
<% } %>
<h3 class="card-title"><%- title %></h3>
<h3 class="card-title"
<% if (!_.isUndefined(srInfo)) { %>
aria-describedby="<%= srInfo.id %>"
<% } %>
><%- title %>
</h3>
<p class="card-description"><%- description %></p>
</div>
<div class="card-actions">
......
<header class="page-header has-secondary">
<div class="page-header-main">
<% if (breadcrumbs !== null && breadcrumbs.length > 0) { %>
<nav class="breadcrumbs">
<nav class="breadcrumbs" aria-label="<%- nav_aria_label %>">
<% _.each(breadcrumbs, function (breadcrumb) { %>
<a class="nav-item" href="<%= breadcrumb.url %>"><%- breadcrumb.title %></a>
<span class="icon fa-angle-right" aria-hidden="true"></span>
......
<a class="nav-item" href="" data-url="<%= url %>" data-index="<%= index %>" role="tab" aria-selected="false"><%- title %></a>
<a class="nav-item" href="" data-url="<%= url %>" data-index="<%= index %>" role="tab" aria-selected="false"><%= title %></a>
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