Commit 3b55d882 by Daniel Friedman

Implement paginated Team Topics UI

Authors:
  - Dan Friedman
  - Ben McMorran
  - Peter Fogg

TNL-1892
parent 351e491c
"""
Common mixin for paginated UIs.
"""
from selenium.webdriver.common.keys import Keys
class PaginatedUIMixin(object):
"""Common methods used for paginated UI."""
PAGINATION_FOOTER_CSS = 'nav.bottom'
PAGE_NUMBER_INPUT_CSS = 'input#page-number-input'
NEXT_PAGE_BUTTON_CSS = 'button.next-page-link'
PREVIOUS_PAGE_BUTTON_CSS = 'button.previous-page-link'
PAGINATION_HEADER_TEXT_CSS = 'div.search-tools'
CURRENT_PAGE_NUMBER_CSS = 'span.current-page'
def get_pagination_header_text(self):
"""Return the text showing which items the user is currently viewing."""
return self.q(css=self.PAGINATION_HEADER_TEXT_CSS).text[0]
def pagination_controls_visible(self):
"""Return true if the pagination controls in the footer are visible."""
footer_nav = self.q(css=self.PAGINATION_FOOTER_CSS).results[0]
# The footer element itself is non-generic, so check above it
footer_el = footer_nav.find_element_by_xpath('..')
return 'hidden' not in footer_el.get_attribute('class').split()
def get_current_page_number(self):
"""Return the the current page number."""
return int(self.q(css=self.CURRENT_PAGE_NUMBER_CSS).text[0])
def go_to_page(self, page_number):
"""Go to the given page_number in the paginated list results."""
self.q(css=self.PAGE_NUMBER_INPUT_CSS).results[0].send_keys(unicode(page_number), Keys.ENTER)
self.wait_for_ajax()
def press_next_page_button(self):
"""Press the next page button in the paginated list results."""
self.q(css=self.NEXT_PAGE_BUTTON_CSS).click()
self.wait_for_ajax()
def press_previous_page_button(self):
"""Press the previous page button in the paginated list results."""
self.q(css=self.PREVIOUS_PAGE_BUTTON_CSS).click()
self.wait_for_ajax()
def is_next_page_button_enabled(self):
"""Return whether the 'next page' button can be clicked."""
return self.is_enabled(self.NEXT_PAGE_BUTTON_CSS)
def is_previous_page_button_enabled(self):
"""Return whether the 'previous page' button can be clicked."""
return self.is_enabled(self.PREVIOUS_PAGE_BUTTON_CSS)
def is_enabled(self, css):
"""Return whether the given element is not disabled."""
return 'is-disabled' not in self.q(css=css).attrs('class')[0]
...@@ -4,6 +4,11 @@ Teams page. ...@@ -4,6 +4,11 @@ Teams page.
""" """
from .course_page import CoursePage from .course_page import CoursePage
from ..common.paging import PaginatedUIMixin
TOPIC_CARD_CSS = 'div.wrapper-card-core'
BROWSE_BUTTON_CSS = 'a.nav-item[data-index="1"]'
class TeamsPage(CoursePage): class TeamsPage(CoursePage):
...@@ -24,3 +29,27 @@ class TeamsPage(CoursePage): ...@@ -24,3 +29,27 @@ class TeamsPage(CoursePage):
description="Body text is present" description="Body text is present"
) )
return self.q(css=main_page_content_css).text[0] return self.q(css=main_page_content_css).text[0]
def browse_topics(self):
""" View the Browse tab of the Teams page. """
self.q(css=BROWSE_BUTTON_CSS).click()
class BrowseTopicsPage(CoursePage, PaginatedUIMixin):
"""
The 'Browse' tab of the Teams page.
"""
url_path = "teams/#browse"
def is_browser_on_page(self):
"""Check if the Browse tab is being viewed."""
button_classes = self.q(css=BROWSE_BUTTON_CSS).attrs('class')
if len(button_classes) == 0:
return False
return 'is-active' in button_classes[0]
@property
def topic_cards(self):
"""Return a list of the topic cards present on the page."""
return self.q(css=TOPIC_CARD_CSS).results
...@@ -17,7 +17,7 @@ class PaginatedMixin(object): ...@@ -17,7 +17,7 @@ class PaginatedMixin(object):
To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'. To specify a specific arrow, pass an iterable with a single element, 'next' or 'previous'.
""" """
return all([ return all([
self.q(css='nav.%s * a.%s-page-link.is-disabled' % (position, arrow)) self.q(css='nav.%s * .%s-page-link.is-disabled' % (position, arrow))
for arrow in arrows for arrow in arrows
]) ])
...@@ -25,14 +25,14 @@ class PaginatedMixin(object): ...@@ -25,14 +25,14 @@ class PaginatedMixin(object):
""" """
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'. Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
""" """
self.q(css='nav.%s * a.previous-page-link' % position)[0].click() self.q(css='nav.%s * .previous-page-link' % position)[0].click()
self.wait_until_ready() self.wait_until_ready()
def move_forward(self, position): def move_forward(self, position):
""" """
Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'. Clicks one of the forward nav buttons. Position can be 'top' or 'bottom'.
""" """
self.q(css='nav.%s * a.next-page-link' % position)[0].click() self.q(css='nav.%s * .next-page-link' % position)[0].click()
self.wait_until_ready() self.wait_until_ready()
def go_to_page(self, number): def go_to_page(self, number):
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Acceptance tests for the teams feature. Acceptance tests for the teams feature.
""" """
from ..helpers import UniqueCourseTest from ..helpers import UniqueCourseTest
from ...pages.lms.teams import TeamsPage from ...pages.lms.teams import TeamsPage, BrowseTopicsPage
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from ...fixtures.course import CourseFixture from ...fixtures.course import CourseFixture
from ...pages.lms.tab_nav import TabNavPage from ...pages.lms.tab_nav import TabNavPage
...@@ -21,7 +21,10 @@ class TeamsTabTest(UniqueCourseTest): ...@@ -21,7 +21,10 @@ class TeamsTabTest(UniqueCourseTest):
self.tab_nav = TabNavPage(self.browser) self.tab_nav = TabNavPage(self.browser)
self.course_info_page = CourseInfoPage(self.browser, self.course_id) self.course_info_page = CourseInfoPage(self.browser, self.course_id)
self.teams_page = TeamsPage(self.browser, self.course_id) self.teams_page = TeamsPage(self.browser, self.course_id)
self.test_topic = {u"name": u"a topic", u"description": u"test topic", u"id": 0}
def create_topics(self, num_topics):
"""Create `num_topics` test topics."""
return [{u"description": str(i), u"name": str(i), u"id": i} for i in xrange(num_topics)]
def set_team_configuration(self, configuration, enroll_in_course=True, global_staff=False): def set_team_configuration(self, configuration, enroll_in_course=True, global_staff=False):
""" """
...@@ -75,11 +78,15 @@ class TeamsTabTest(UniqueCourseTest): ...@@ -75,11 +78,15 @@ class TeamsTabTest(UniqueCourseTest):
""" """
Scenario: teams tab should not be present if student is not enrolled in the course Scenario: teams tab should not be present if student is not enrolled in the course
Given there is a course with team configuration and topics Given there is a course with team configuration and topics
And I am not enrolled in that course, and am not global staff And I am not enrolled in that course, and am not global staff
When I view the course info page When I view the course info page
Then I should not see the Teams tab Then I should not see the Teams tab
""" """
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False) self.set_team_configuration(
{u"max_team_size": 10, u"topics": self.create_topics(1)},
enroll_in_course=False
)
self.verify_teams_present(False) self.verify_teams_present(False)
def test_teams_enabled(self): def test_teams_enabled(self):
...@@ -90,7 +97,7 @@ class TeamsTabTest(UniqueCourseTest): ...@@ -90,7 +97,7 @@ class TeamsTabTest(UniqueCourseTest):
Then I should see the Teams tab Then I should see the Teams tab
And the correct content should be on the page And the correct content should be on the page
""" """
self.set_team_configuration({u"max_team_size": 10, u"topics": [self.test_topic]}) self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(1)})
self.verify_teams_present(True) self.verify_teams_present(True)
def test_teams_enabled_global_staff(self): def test_teams_enabled_global_staff(self):
...@@ -103,6 +110,121 @@ class TeamsTabTest(UniqueCourseTest): ...@@ -103,6 +110,121 @@ class TeamsTabTest(UniqueCourseTest):
And the correct content should be on the page And the correct content should be on the page
""" """
self.set_team_configuration( self.set_team_configuration(
{u"max_team_size": 10, u"topics": [self.test_topic]}, enroll_in_course=False, global_staff=True {u"max_team_size": 10, u"topics": self.create_topics(1)},
enroll_in_course=False,
global_staff=True
) )
self.verify_teams_present(True) self.verify_teams_present(True)
@attr('shard_5')
class BrowseTopicsTest(TeamsTabTest):
"""
Tests for the Browse tab of the Teams page.
"""
def setUp(self):
super(BrowseTopicsTest, self).setUp()
self.topics_page = BrowseTopicsPage(self.browser, self.course_id)
def test_list_topics(self):
"""
Scenario: a list of topics should be visible in the "Browse" tab
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
Then I should see a list of topics for the course
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(2)})
self.topics_page.visit()
self.assertEqual(len(self.topics_page.topic_cards), 2)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-2 out of 2 total')
self.assertFalse(self.topics_page.pagination_controls_visible())
self.assertFalse(self.topics_page.is_previous_page_button_enabled())
self.assertFalse(self.topics_page.is_next_page_button_enabled())
def test_topic_pagination(self):
"""
Scenario: a list of topics should be visible in the "Browse" tab, paginated 12 per page
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
Then I should see only the first 12 topics
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(20)})
self.topics_page.visit()
self.assertEqual(len(self.topics_page.topic_cards), 12)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 20 total')
self.assertTrue(self.topics_page.pagination_controls_visible())
self.assertFalse(self.topics_page.is_previous_page_button_enabled())
self.assertTrue(self.topics_page.is_next_page_button_enabled())
def test_go_to_numbered_page(self):
"""
Scenario: topics should be able to be navigated by page number
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
And I enter a valid page number in the page number input
Then I should see that page of topics
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(25)})
self.topics_page.visit()
self.topics_page.go_to_page(3)
self.assertEqual(len(self.topics_page.topic_cards), 1)
self.assertTrue(self.topics_page.is_previous_page_button_enabled())
self.assertFalse(self.topics_page.is_next_page_button_enabled())
def test_go_to_invalid_page(self):
"""
Scenario: browsing topics should not respond to invalid page numbers
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
And I enter an invalid page number in the page number input
Then I should stay on the current page
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)})
self.topics_page.visit()
self.topics_page.go_to_page(3)
self.assertEqual(self.topics_page.get_current_page_number(), 1)
def test_page_navigation_buttons(self):
"""
Scenario: browsing topics should not respond to invalid page numbers
Given I am enrolled in a course with team configuration and topics
When I visit the Teams page
And I browse topics
When I press the next page button
Then I should move to the next page
When I press the previous page button
Then I should move to the previous page
"""
self.set_team_configuration({u"max_team_size": 10, u"topics": self.create_topics(13)})
self.topics_page.visit()
self.topics_page.press_next_page_button()
self.assertEqual(len(self.topics_page.topic_cards), 1)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 13-13 out of 13 total')
self.topics_page.press_previous_page_button()
self.assertEqual(len(self.topics_page.topic_cards), 12)
self.assertEqual(self.topics_page.get_pagination_header_text(), 'Showing 1-12 out of 13 total')
def test_topic_description_truncation(self):
"""
Scenario: excessively long topic descriptions should be truncated so
as to fit within a topic card.
Given I am enrolled in a course with a team configuration and a topic
with a long description
When I visit the Teams page
And I browse topics
Then I should see a truncated topic description
"""
initial_description = "A" + " really" * 50 + " long description"
self.set_team_configuration(
{u"max_team_size": 1, u"topics": [{"name": "", "id": "", "description": initial_description}]}
)
self.topics_page.visit()
truncated_description = self.topics_page.topic_cards[0].text
self.assertLess(len(truncated_description), len(initial_description))
self.assertTrue(truncated_description.endswith('...'))
self.assertIn(truncated_description.split('...')[0], initial_description)
;(function (define) {
'use strict';
define(['common/js/components/collections/paging_collection', 'teams/js/models/topic', 'gettext'],
function(PagingCollection, TopicModel, gettext) {
var TopicCollection = PagingCollection.extend({
initialize: function(topics, options) {
PagingCollection.prototype.initialize.call(this);
this.course_id = options.course_id;
this.perPage = topics.results.length;
this.server_api['course_id'] = function () { return encodeURIComponent(this.course_id); };
this.server_api['order_by'] = function () { return this.sortField; };
delete this.server_api['sort_order']; // Sort order is not specified for the Team API
this.registerSortableField('name', gettext('name'));
this.registerSortableField('team_count', gettext('team count'));
},
model: TopicModel
});
return TopicCollection;
});
}).call(this, define || RequireJS.define);
...@@ -13,5 +13,5 @@ ...@@ -13,5 +13,5 @@
} }
}); });
return Topic; return Topic;
}) });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
...@@ -7,7 +7,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"], ...@@ -7,7 +7,7 @@ define(["jquery", "backbone", "teams/js/teams_tab_factory"],
beforeEach(function() { beforeEach(function() {
setFixtures('<section class="teams-content"></section>'); setFixtures('<section class="teams-content"></section>');
teamsTab = new TeamsTabFactory(); teamsTab = new TeamsTabFactory({results: []}, '', 'edX/DemoX/Demo_Course');
}); });
afterEach(function() { afterEach(function() {
......
define(['URI', 'underscore', 'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic'],
function (URI, _, AjaxHelpers, TopicCollection) {
'use strict';
describe('TopicCollection', function () {
var topicCollection;
beforeEach(function () {
topicCollection = new TopicCollection(
{
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": [
{
"description": "asdf description",
"name": "asdf",
"id": "_asdf"
},
{
"description": "bar description",
"name": "bar",
"id": "_bar"
},
{
"description": "baz description",
"name": "baz",
"id": "_baz"
},
{
"description": "foo description",
"name": "foo",
"id": "_foo"
},
{
"description": "qwerty description",
"name": "qwerty",
"id": "_qwerty"
}
],
"sort_order": "name"
},
{course_id: 'my/course/id', parse: true});
});
var testRequestParam = function (self, param, value) {
var requests = AjaxHelpers.requests(self),
url,
params;
topicCollection.fetch();
expect(requests.length).toBe(1);
url = new URI(requests[0].url);
params = url.query(true);
expect(params[param]).toBe(value);
};
it('sets its perPage based on initial page size', function () {
expect(topicCollection.perPage).toBe(5);
});
it('sorts by name', function () {
testRequestParam(this, 'order_by', 'name');
});
it('passes a course_id to the server', function () {
testRequestParam(this, 'course_id', 'my/course/id');
});
it('URL encodes its course_id ', function () {
topicCollection.course_id = 'my+course+id';
testRequestParam(this, 'course_id', 'my+course+id');
});
});
});
define([
'common/js/spec_helpers/ajax_helpers', 'teams/js/collections/topic', 'teams/js/views/topics'
], function (AjaxHelpers, TopicCollection, TopicsView) {
'use strict';
describe('TopicsView', function () {
var initialTopics, topicCollection, topicsView, nextPageButtonCss;
nextPageButtonCss = '.next-page-link';
function generateTopics(startIndex, stopIndex) {
return _.map(_.range(startIndex, stopIndex + 1), function (i) {
return {
"description": "description " + i,
"name": "topic " + i,
"id": "id " + i,
"team_count": 0
};
});
}
beforeEach(function () {
setFixtures('<div class="topics-container"></div>');
initialTopics = generateTopics(1, 5);
topicCollection = new TopicCollection(
{
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": initialTopics
},
{course_id: 'my/course/id', parse: true}
);
topicsView = new TopicsView({el: '.topics-container', collection: topicCollection}).render();
});
/**
* Verify that the topics view's header reflects the page we're currently viewing.
* @param matchString the header we expect to see
*/
function expectHeader(matchString) {
expect(topicsView.$('.topics-paging-header').text()).toMatch(matchString);
}
/**
* Verify that the topics list view renders the expected topics
* @param expectedTopics an array of topic objects we expect to see
*/
function expectTopics(expectedTopics) {
var topicCards;
topicCards = topicsView.$('.topic-card');
_.each(expectedTopics, function (topic, index) {
var currentCard = topicCards.eq(index);
expect(currentCard.text()).toMatch(topic.name);
expect(currentCard.text()).toMatch(topic.description);
expect(currentCard.text()).toMatch(topic.team_count + ' Teams');
});
}
/**
* Verify that the topics footer reflects the current pagination
* @param options a parameters hash containing:
* - currentPage: the one-indexed page we expect to be viewing
* - totalPages: the total number of pages to page through
* - isHidden: whether the footer is expected to be visible
*/
function expectFooter(options) {
var footerEl = topicsView.$('.topics-paging-footer');
expect(footerEl.text())
.toMatch(new RegExp(options.currentPage + '\\s+out of\\s+\/\\s+' + topicCollection.totalPages));
expect(footerEl.hasClass('hidden')).toBe(options.isHidden);
}
it('can render the first of many pages', function () {
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
});
it('can render the only page', function () {
initialTopics = generateTopics(1, 1);
topicCollection.set(
{
"count": 1,
"num_pages": 1,
"current_page": 1,
"start": 0,
"results": initialTopics
},
{parse: true}
);
expectHeader('Showing 1 out of 1 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 1, isHidden: true});
});
it('can change to the next page', function () {
var requests = AjaxHelpers.requests(this),
newTopics = generateTopics(1, 1);
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
expect(requests.length).toBe(0);
topicsView.$(nextPageButtonCss).click();
expect(requests.length).toBe(1);
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": newTopics
});
expectHeader('Showing 6-6 out of 6 total');
expectTopics(newTopics);
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
});
it('can change to the previous page', function () {
var requests = AjaxHelpers.requests(this),
previousPageTopics;
initialTopics = generateTopics(1, 1);
topicCollection.set(
{
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": initialTopics
},
{parse: true}
);
expectHeader('Showing 6-6 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 2, totalPages: 2, isHidden: false});
topicsView.$('.previous-page-link').click();
previousPageTopics = generateTopics(1, 5);
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 1,
"start": 0,
"results": previousPageTopics
});
expectHeader('Showing 1-5 out of 6 total');
expectTopics(previousPageTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
});
it('sets focus for screen readers', function () {
var requests = AjaxHelpers.requests(this);
spyOn($.fn, 'focus');
topicsView.$(nextPageButtonCss).click();
AjaxHelpers.respondWithJson(requests, {
"count": 6,
"num_pages": 2,
"current_page": 2,
"start": 5,
"results": generateTopics(1, 1)
});
expect(topicsView.$('.sr-is-focusable').focus).toHaveBeenCalled();
});
it('does not change on server error', function () {
var requests = AjaxHelpers.requests(this),
expectInitialState = function () {
expectHeader('Showing 1-5 out of 6 total');
expectTopics(initialTopics);
expectFooter({currentPage: 1, totalPages: 2, isHidden: false});
};
expectInitialState();
topicsView.$(nextPageButtonCss).click();
requests[0].respond(500);
expectInitialState();
});
});
});
;(function (define) { ;(function (define) {
'use strict'; 'use strict';
define(['jquery','teams/js/views/teams_tab'], define(['jquery', 'teams/js/views/teams_tab', 'teams/js/collections/topic'],
function ($, TeamsTabView) { function ($, TeamsTabView, TopicCollection) {
return function () { return function (topics, topics_url, course_id) {
var topicCollection = new TopicCollection(topics, {url: topics_url, course_id: course_id, parse: true});
topicCollection.bootstrap();
var view = new TeamsTabView({ var view = new TeamsTabView({
el: $('.teams-content') el: $('.teams-content'),
topicCollection: topicCollection
}); });
view.render(); view.render();
}; };
......
...@@ -6,10 +6,11 @@ ...@@ -6,10 +6,11 @@
'gettext', 'gettext',
'js/components/header/views/header', 'js/components/header/views/header',
'js/components/header/models/header', 'js/components/header/models/header',
'js/components/tabbed/views/tabbed_view'], 'js/components/tabbed/views/tabbed_view',
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView) { 'teams/js/views/topics'],
function (Backbone, _, gettext, HeaderView, HeaderModel, TabbedView, TopicsView) {
var TeamTabView = Backbone.View.extend({ var TeamTabView = Backbone.View.extend({
initialize: function() { initialize: function(options) {
this.headerModel = new HeaderModel({ this.headerModel = new HeaderModel({
description: gettext("Course teams are organized into topics created by course instructors. Try to join others in an existing team before you decide to create a new team!"), description: gettext("Course teams are organized into topics created by course instructors. Try to join others in an existing team before you decide to create a new team!"),
title: gettext("Teams") title: gettext("Teams")
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
}, },
render: function () { render: function () {
this.$el.text(this.text) this.$el.text(this.text);
} }
}); });
this.tabbedView = new TabbedView({ this.tabbedView = new TabbedView({
...@@ -35,7 +36,9 @@ ...@@ -35,7 +36,9 @@
}, { }, {
title: gettext('Browse'), title: gettext('Browse'),
url: 'browse', url: 'browse',
view: new TempTabView({text: 'Browse team topics here.'}) view: new TopicsView({
collection: options.topicCollection
})
}] }]
}); });
Backbone.history.start(); Backbone.history.start();
......
;(function (define) {
'use strict';
define([
'backbone',
'underscore',
'gettext',
'common/js/components/views/list',
'common/js/components/views/paging_header',
'common/js/components/views/paging_footer',
'teams/js/views/topic_card',
'text!teams/templates/topics.underscore'
], function (Backbone, _, gettext, ListView, PagingHeader, PagingFooterView, TopicCardView, topics_template) {
var TopicsListView = ListView.extend({
tagName: 'div',
className: 'topics-container',
itemViewClass: TopicCardView
});
var TopicsView = Backbone.View.extend({
initialize: function() {
this.listView = new TopicsListView({collection: this.collection});
this.headerView = new PagingHeader({collection: this.collection});
this.pagingFooterView = new PagingFooterView({
collection: this.collection, hideWhenOnePage: true
});
// Focus top of view for screen readers
this.collection.on('page_changed', function () {
this.$('.sr-is-focusable.sr-topics-view').focus();
}, this);
},
render: function() {
this.$el.html(_.template(topics_template));
this.assign(this.listView, '.topics-list');
this.assign(this.headerView, '.topics-paging-header');
this.assign(this.pagingFooterView, '.topics-paging-footer');
return this;
},
/**
* Helper method to render subviews and re-bind events.
*
* Borrowed from http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/
*
* @param view The Backbone view to render
* @param selector The string CSS selector which the view should attach to
*/
assign: function(view, selector) {
view.setElement(this.$(selector)).render();
}
});
return TopicsView;
});
}).call(this, define || RequireJS.define);
<div class="sr-is-focusable sr-topics-view" tabindex="-1"></div>
<div class="topics-paging-header"></div>
<div class="topics-list"></div>
<div class="topics-paging-footer"></div>
## mako ## mako
<%! import json %>
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from openedx.core.lib.json_utils import EscapedEdxJSONEncoder %>
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%inherit file="/main.html" /> <%inherit file="/main.html" />
...@@ -22,7 +24,7 @@ ...@@ -22,7 +24,7 @@
<script type="text/javascript"> <script type="text/javascript">
(function (require) { (function (require) {
require(['teams/js/teams_tab_factory'], function (TeamsTabFactory) { require(['teams/js/teams_tab_factory'], function (TeamsTabFactory) {
var pageView = new TeamsTabFactory(); new TeamsTabFactory(${ json.dumps(topics, cls=EscapedEdxJSONEncoder) }, '${ topics_url }', '${ unicode(course.id) }');
}); });
}).call(this, require || RequireJS.require); }).call(this, require || RequireJS.require);
</script> </script>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
function (Backbone, _, $, tabbedViewTemplate, tabTemplate) { function (Backbone, _, $, tabbedViewTemplate, tabTemplate) {
var TabbedView = Backbone.View.extend({ var TabbedView = Backbone.View.extend({
events: { events: {
'click .nav-item': 'switchTab' 'click .nav-item[role="tab"]': 'switchTab'
}, },
template: _.template(tabbedViewTemplate), template: _.template(tabbedViewTemplate),
...@@ -45,9 +45,8 @@ ...@@ -45,9 +45,8 @@
view = tab.view; view = tab.view;
this.$('a.is-active').removeClass('is-active').attr('aria-selected', 'false'); this.$('a.is-active').removeClass('is-active').attr('aria-selected', 'false');
this.$('a[data-index='+index+']').addClass('is-active').attr('aria-selected', 'true'); this.$('a[data-index='+index+']').addClass('is-active').attr('aria-selected', 'true');
view.render(); view.setElement(this.$('.page-content-main')).render();
this.$('.page-content-main').html(view.$el.html()); this.$('.sr-is-focusable.sr-tab').focus();
this.$('.sr-is-focusable').focus();
this.router.navigate(tab.url, {replace: true}); this.router.navigate(tab.url, {replace: true});
}, },
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
}); });
it('can render itself', function () { it('can render itself', function () {
expect(view.$el.html()).toContain('<nav class="page-content-nav" role="tablist">') expect(view.$el.html()).toContain('<nav class="page-content-nav"');
}); });
it('shows its first tab by default', function () { it('shows its first tab by default', function () {
...@@ -77,6 +77,12 @@ ...@@ -77,6 +77,12 @@
view.$('.nav-item[data-index=1]').click(); view.$('.nav-item[data-index=1]').click();
expect(Backbone.history.navigate).toHaveBeenCalledWith('test 2', {replace: true}); expect(Backbone.history.navigate).toHaveBeenCalledWith('test 2', {replace: true});
}); });
it('sets focus for screen readers', function () {
spyOn($.fn, 'focus');
view.$('.nav-item[data-index=1]').click();
expect(view.$('.sr-is-focusable.sr-tab').focus).toHaveBeenCalled();
});
}); });
} }
); );
......
...@@ -614,6 +614,8 @@ ...@@ -614,6 +614,8 @@
// Run the LMS tests // Run the LMS tests
'lms/include/teams/js/spec/teams_factory_spec.js', 'lms/include/teams/js/spec/teams_factory_spec.js',
'lms/include/teams/js/spec/topic_card_spec.js', 'lms/include/teams/js/spec/topic_card_spec.js',
'lms/include/teams/js/spec/topic_collection_spec.js',
'lms/include/teams/js/spec/topics_spec.js',
'lms/include/js/spec/components/header/header_spec.js', 'lms/include/js/spec/components/header/header_spec.js',
'lms/include/js/spec/components/tabbed/tabbed_view_spec.js', 'lms/include/js/spec/components/tabbed/tabbed_view_spec.js',
'lms/include/js/spec/components/card/card_spec.js', 'lms/include/js/spec/components/card/card_spec.js',
......
<div class="page-content"> <div class="page-content">
<nav class="page-content-nav" role="tablist"></nav> <nav class="page-content-nav" aria-label="Teams"></nav>
<div class="sr-is-focusable" tabindex="-1"></div> <div class="sr-is-focusable sr-tab" tabindex="-1"></div>
<div class="page-content-main"></div> <div class="page-content-main"></div>
</div> </div>
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