Commit 52a8bc50 by clrux

Merge pull request #9367 from edx/clrux/ac-76-course-nav

Course navigation menu accessibility issue
parents 69ddbc0d ab569feb
...@@ -82,7 +82,7 @@ class CourseNavPage(PageObject): ...@@ -82,7 +82,7 @@ class CourseNavPage(PageObject):
# Click the section to ensure it's open (no harm in clicking twice if it's already open) # Click the section to ensure it's open (no harm in clicking twice if it's already open)
# Add one to convert from list index to CSS index # Add one to convert from list index to CSS index
section_css = 'nav>div.chapter:nth-of-type({0})>h3>a'.format(sec_index + 1) section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1)
self.q(css=section_css).first.click() self.q(css=section_css).first.click()
# Get the subsection by index # Get the subsection by index
...@@ -94,9 +94,10 @@ class CourseNavPage(PageObject): ...@@ -94,9 +94,10 @@ class CourseNavPage(PageObject):
return return
# Convert list indices (start at zero) to CSS indices (start at 1) # Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css = "nav>div.chapter:nth-of-type({0})>ul>li:nth-of-type({1})>a".format( subsection_css = (
sec_index + 1, subsec_index + 1 ".course-navigation .chapter-content-container:nth-of-type({0}) "
) ".chapter-menu .menu-item:nth-of-type({1})"
).format(sec_index + 1, subsec_index + 1)
# Click the subsection and ensure that the page finishes reloading # Click the subsection and ensure that the page finishes reloading
self.q(css=subsection_css).first.click() self.q(css=subsection_css).first.click()
...@@ -130,7 +131,7 @@ class CourseNavPage(PageObject): ...@@ -130,7 +131,7 @@ class CourseNavPage(PageObject):
""" """
Return a list of all section titles on the page. Return a list of all section titles on the page.
""" """
chapter_css = 'nav > div.chapter > h3 > a' chapter_css = '.course-navigation .chapter .group-heading'
return self.q(css=chapter_css).map(lambda el: el.text.strip()).results return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index): def _subsection_titles(self, section_index):
...@@ -140,7 +141,10 @@ class CourseNavPage(PageObject): ...@@ -140,7 +141,10 @@ class CourseNavPage(PageObject):
""" """
# Retrieve the subsection title for the section # Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one # Add one to the list index to get the CSS index, which starts at one
subsection_css = 'nav>div.chapter:nth-of-type({0})>ul>li>a>p:nth-of-type(1)'.format(section_index) subsection_css = (
".course-navigation .chapter-content-container:nth-of-type({0}) "
".chapter-menu .menu-item a p:nth-of-type(1)"
).format(section_index)
# If the element is visible, we can get its text directly # If the element is visible, we can get its text directly
# Otherwise, we need to get the HTML # Otherwise, we need to get the HTML
...@@ -171,8 +175,8 @@ class CourseNavPage(PageObject): ...@@ -171,8 +175,8 @@ class CourseNavPage(PageObject):
That's true right after we click the section/subsection, but not true in general That's true right after we click the section/subsection, but not true in general
(the user could go to a section, then expand another tab). (the user could go to a section, then expand another tab).
""" """
current_section_list = self.q(css='nav>div.chapter.is-open>h3>a').text current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text
current_subsection_list = self.q(css='nav>div.chapter.is-open li.active>a>p').text current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text
if len(current_section_list) == 0: if len(current_section_list) == 0:
self.warning("Could not find the current section") self.warning("Could not find the current section")
......
...@@ -14,7 +14,7 @@ class CoursewarePage(CoursePage): ...@@ -14,7 +14,7 @@ class CoursewarePage(CoursePage):
url_path = "courseware/" url_path = "courseware/"
xblock_component_selector = '.vert .xblock' xblock_component_selector = '.vert .xblock'
section_selector = '.chapter' section_selector = '.chapter'
subsection_selector = '.chapter ul li' subsection_selector = '.chapter-content-container .chapter-menu a'
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='body.courseware').present return self.q(css='body.courseware').present
...@@ -102,7 +102,7 @@ class CoursewarePage(CoursePage): ...@@ -102,7 +102,7 @@ class CoursewarePage(CoursePage):
""" """
return the url of the active subsection in the left nav return the url of the active subsection in the left nav
""" """
return self.q(css='.chapter ul li.active a').attrs('href')[0] return self.q(css='.chapter-content-container .chapter-menu .menu-item.active a').attrs('href')[0]
@property @property
def can_start_proctored_exam(self): def can_start_proctored_exam(self):
......
...@@ -1121,7 +1121,7 @@ class EntranceExamTest(UniqueCourseTest): ...@@ -1121,7 +1121,7 @@ class EntranceExamTest(UniqueCourseTest):
When I view the courseware that has an entrance exam When I view the courseware that has an entrance exam
Then there should be an "Entrance Exam" chapter.' Then there should be an "Entrance Exam" chapter.'
""" """
entrance_exam_link_selector = 'div#accordion nav div h3 a' entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading'
# visit courseware page and make sure there is not entrance exam chapter. # visit courseware page and make sure there is not entrance exam chapter.
self.courseware_page.visit() self.courseware_page.visit()
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
......
...@@ -92,7 +92,7 @@ def when_i_navigate_to_a_section(step): ...@@ -92,7 +92,7 @@ def when_i_navigate_to_a_section(step):
world.disable_jquery_animations() world.disable_jquery_animations()
# Open the 2nd section # Open the 2nd section
world.css_click(css_selector='div.chapter', index=1) world.css_click(css_selector='button.chapter', index=1)
subsection_css = 'a[href*="Test_Subsection_2/"]' subsection_css = 'a[href*="Test_Subsection_2/"]'
# Click on the subsection to see the content # Click on the subsection to see the content
......
...@@ -74,6 +74,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -74,6 +74,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.mixin import wrap_with_license from xmodule.mixin import wrap_with_license
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.model_utils import slugify
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
from util import milestones_helpers from util import milestones_helpers
from verify_student.services import ReverificationService from verify_student.services import ReverificationService
...@@ -165,6 +166,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_ ...@@ -165,6 +166,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
for chapter in chapters: for chapter in chapters:
# Only show required content, if there is required content # Only show required content, if there is required content
# chapter.hide_from_toc is read-only (boo) # chapter.hide_from_toc is read-only (boo)
display_id = slugify(chapter.display_name_with_default)
local_hide_from_toc = False local_hide_from_toc = False
if required_content: if required_content:
if unicode(chapter.location) not in required_content: if unicode(chapter.location) not in required_content:
...@@ -246,6 +248,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_ ...@@ -246,6 +248,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_
sections.append(section_context) sections.append(section_context)
toc_chapters.append({ toc_chapters.append({
'display_name': chapter.display_name_with_default, 'display_name': chapter.display_name_with_default,
'display_id': display_id,
'url_name': chapter.url_name, 'url_name': chapter.url_name,
'sections': sections, 'sections': sections,
'active': chapter.url_name == active_chapter 'active': chapter.url_name == active_chapter
......
...@@ -155,7 +155,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -155,7 +155,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
} }
], ],
'url_name': u'Entrance_Exam_Section_-_Chapter_1', 'url_name': u'Entrance_Exam_Section_-_Chapter_1',
'display_name': u'Entrance Exam Section - Chapter 1' 'display_name': u'Entrance Exam Section - Chapter 1',
'display_id': u'entrance-exam-section-chapter-1',
} }
] ]
) )
...@@ -182,19 +183,22 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -182,19 +183,22 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
} }
], ],
'url_name': u'Overview', 'url_name': u'Overview',
'display_name': u'Overview' 'display_name': u'Overview',
'display_id': u'overview'
}, },
{ {
'active': False, 'active': False,
'sections': [], 'sections': [],
'url_name': u'Week_1', 'url_name': u'Week_1',
'display_name': u'Week 1' 'display_name': u'Week 1',
'display_id': u'week-1'
}, },
{ {
'active': False, 'active': False,
'sections': [], 'sections': [],
'url_name': u'Instructor', 'url_name': u'Instructor',
'display_name': u'Instructor' 'display_name': u'Instructor',
'display_id': u'instructor'
}, },
{ {
'active': True, 'active': True,
...@@ -209,7 +213,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): ...@@ -209,7 +213,8 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase):
} }
], ],
'url_name': u'Entrance_Exam_Section_-_Chapter_1', 'url_name': u'Entrance_Exam_Section_-_Chapter_1',
'display_name': u'Entrance Exam Section - Chapter 1' 'display_name': u'Entrance Exam Section - Chapter 1',
'display_id': u'entrance-exam-section-chapter-1'
} }
] ]
) )
......
...@@ -633,6 +633,7 @@ class TestTOC(ModuleStoreTestCase): ...@@ -633,6 +633,7 @@ class TestTOC(ModuleStoreTestCase):
def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds): def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds):
with self.store.default_store(default_ms): with self.store.default_store(default_ms):
self.setup_modulestore(default_ms, setup_finds, setup_sends) self.setup_modulestore(default_ms, setup_finds, setup_sends)
expected = ([{'active': True, 'sections': expected = ([{'active': True, 'sections':
[{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True,
'format': u'Lecture Sequence', 'due': None, 'active': False}, 'format': u'Lecture Sequence', 'due': None, 'active': False},
...@@ -642,11 +643,11 @@ class TestTOC(ModuleStoreTestCase): ...@@ -642,11 +643,11 @@ class TestTOC(ModuleStoreTestCase):
'format': '', 'due': None, 'active': False}, 'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}])
course = self.store.get_course(self.toy_course.id, depth=2) course = self.store.get_course(self.toy_course.id, depth=2)
with check_mongo_calls(toc_finds): with check_mongo_calls(toc_finds):
...@@ -682,11 +683,11 @@ class TestTOC(ModuleStoreTestCase): ...@@ -682,11 +683,11 @@ class TestTOC(ModuleStoreTestCase):
'format': '', 'due': None, 'active': False}, 'format': '', 'due': None, 'active': False},
{'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True, {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True,
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'Overview', 'display_name': u'Overview'}, 'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'},
{'active': False, 'sections': {'active': False, 'sections':
[{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True,
'format': '', 'due': None, 'active': False}], 'format': '', 'due': None, 'active': False}],
'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) 'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}])
with check_mongo_calls(toc_finds): with check_mongo_calls(toc_finds):
actual = render.toc_for_course( actual = render.toc_for_course(
......
...@@ -107,7 +107,6 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -107,7 +107,6 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase):
'chapter': 'Chrome', 'chapter': 'Chrome',
'section': displayname, 'section': displayname,
})) }))
self.assertEquals('open_close_accordion' in response.content, accordion)
self.assertEquals('course-tabs' in response.content, tabs) self.assertEquals('course-tabs' in response.content, tabs)
self.assertTabInactive('progress', response) self.assertTabInactive('progress', response)
......
...@@ -25,7 +25,6 @@ class RenderXBlockTestMixin(object): ...@@ -25,7 +25,6 @@ class RenderXBlockTestMixin(object):
# DOM elements that appear in the LMS Courseware, # DOM elements that appear in the LMS Courseware,
# but are excluded from the xBlock-only rendering. # but are excluded from the xBlock-only rendering.
COURSEWARE_CHROME_HTML_ELEMENTS = [ COURSEWARE_CHROME_HTML_ELEMENTS = [
'<header id="open_close_accordion"',
'<ol class="course-tabs"', '<ol class="course-tabs"',
'<footer id="footer-openedx"', '<footer id="footer-openedx"',
'<div class="window-wrap"', '<div class="window-wrap"',
......
<div class="course-wrapper">
<header id="open_close_accordion">
<a href="#">close</a>
</header>
<div id="accordion"></div>
</div>
describe 'Courseware', -> describe 'Courseware', ->
describe 'start', -> describe 'start', ->
it 'create the navigation', ->
spyOn(window, 'Navigation')
Courseware.start()
expect(window.Navigation).toHaveBeenCalled()
it 'binds the Logger', -> it 'binds the Logger', ->
spyOn(Logger, 'bind') spyOn(Logger, 'bind')
Courseware.start() Courseware.start()
......
describe 'Navigation', ->
beforeEach ->
loadFixtures 'coffee/fixtures/accordion.html'
@navigation = new Navigation
describe 'constructor', ->
describe 'when the #accordion exists', ->
describe 'when there is an active section', ->
beforeEach ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li class="active"></li></ul>')
new Navigation
it 'activate the accordion with correct active section', ->
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 1
header: 'h3'
autoHeight: false
heightStyle: 'content'
describe 'when there is no active section', ->
beforeEach ->
spyOn $.fn, 'accordion'
$('#accordion').append('<ul><li></li></ul><ul><li></li></ul>')
new Navigation
it 'activate the accordian with no section as active', ->
expect($('#accordion').accordion).toHaveBeenCalledWith
active: 0
header: 'h3'
autoHeight: false
heightStyle: 'content'
it 'binds the accordionchange event', ->
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', ->
beforeEach ->
$('#accordion').remove()
it 'does not activate the accordion', ->
spyOn $.fn, 'accordion'
expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', ->
it 'toggle closed class on the wrapper', ->
$('.course-wrapper').removeClass('closed')
@navigation.toggle()
expect($('.course-wrapper')).toHaveClass('closed')
@navigation.toggle()
expect($('.course-wrapper')).not.toHaveClass('closed')
describe 'log', ->
beforeEach ->
spyOn Logger, 'log'
it 'submit event log', ->
@navigation.log {}, {
newHeader:
text: -> "new"
oldHeader:
text: -> "old"
}
expect(Logger.log).toHaveBeenCalledWith 'accordion',
newheader: 'new'
oldheader: 'old'
...@@ -2,7 +2,6 @@ class @Courseware ...@@ -2,7 +2,6 @@ class @Courseware
@prefix: '' @prefix: ''
constructor: -> constructor: ->
new Navigation
Logger.bind() Logger.bind()
@render() @render()
......
class @Navigation
constructor: ->
if $('#accordion').length
# First look for an active section
active = $('#accordion ul:has(li.active)').index('#accordion ul')
# if we didn't find one, look for an active chapter
if active < 0
active = $('#accordion h3.active').index('#accordion h3')
# if that didn't work either, default to 0
if active < 0
active = 0
$('#accordion').bind('accordionchange', @log).accordion
active: active
header: 'h3'
autoHeight: false
heightStyle: 'content'
$('#accordion .ui-state-active').closest('.chapter').addClass('is-open')
$('#open_close_accordion a').click @toggle
$('#accordion').show()
$('#accordion a').click @setChapter
log: (event, ui) ->
Logger.log 'accordion',
newheader: ui.newHeader.text()
oldheader: ui.oldHeader.text()
toggle: ->
$('.course-wrapper').toggleClass('closed')
setChapter: ->
$('#accordion .is-open').removeClass('is-open')
$(this).closest('.chapter').addClass('is-open')
\ No newline at end of file
<div class="course-wrapper">
<div class="accordion">
<button class="button-chapter chapter" aria-controls="accordion-menu-1" aria-pressed="true">
<h3 class="group-heading">
Introduction Chapter
</h3>
</button>
<div class="chapter-content-container" tabindex="-1" aria-expanded="true">
<div class="chapter-menu" id="accordion-menu-1">
<div class="menu-item">
<a href="#">
<p>edX Homepage</p>
<p class="subtitle">Ungraded</p>
</a>
</div>
<div class="menu-item active">
<a href="#">
<p>The edX Blog</p>
</a>
</div>
<div class="menu-item graded">
<a href="#">
<p>Courses Dashboard</p>
<p class="subtitle">Graded</p>
</a>
</div>
</div>
</div>
<button class="button-chapter chapter" aria-controls="accordion-menu-2" aria-pressed="false">
<h3 class="group-heading">
Another Chapter
</h3>
</button>
<div class="chapter-content-container" tabindex="-1" aria-expanded="false">
<div class="chapter-menu" id="accordion-menu-2">
<div class="menu-item">
<a href="#">
<p>edX Homepage</p>
<p class="subtitle">Ungraded</p>
</a>
</div>
<div class="menu-item">
<a href="#">
<p>The edX Blog</p>
</a>
</div>
<div class="menu-item">
<a class="graded" href="#">
<p>Courses Dashboard</p>
<p class="subtitle">Graded</p>
</a>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
...@@ -64,6 +64,7 @@ ...@@ -64,6 +64,7 @@
'_split': 'js/split', '_split': 'js/split',
'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer', 'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer',
'MathJaxProcessor': 'coffee/src/customwmd', 'MathJaxProcessor': 'coffee/src/customwmd',
'js/utils/navigation': 'js/utils/navigation',
// Manually specify LMS files that are not converted to RequireJS // Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history', 'history': 'js/vendor/history',
...@@ -803,6 +804,8 @@ ...@@ -803,6 +804,8 @@
'lms/include/teams/js/spec/views/topic_teams_spec.js', 'lms/include/teams/js/spec/views/topic_teams_spec.js',
'lms/include/teams/js/spec/views/topics_spec.js', 'lms/include/teams/js/spec/views/topics_spec.js',
'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js' 'lms/include/teams/js/spec/views/team_profile_header_actions_spec.js'
'lms/include/teams/js/spec/views/team_join_spec.js',
'lms/include/js/spec/navigation_spec.js'
]); ]);
}).call(this, requirejs, define); }).call(this, requirejs, define);
define(['jquery', 'js/utils/navigation'], function($) {
'use strict';
describe('Course Navigation Accordion', function() {
var accordion, button, heading, chapterContent, chapterMenu;
beforeEach(function() {
loadFixtures('js/fixtures/accordion.html');
accordion = $('.accordion');
button = accordion.children('.button-chapter');
heading = button.children('.group-heading');
chapterContent = accordion.children('.chapter-content-container');
chapterMenu = chapterContent.children('.chapter-menu');
spyOn($.fn, 'focus').andCallThrough();
edx.util.navigation.init();
});
describe('constructor', function() {
describe('always', function() {
it('ensures accordion is present', function() {
expect(accordion.length).toBe(1);
});
it('ensures aria attributes are present', function() {
expect(button).toHaveAttr({
'aria-pressed': 'true'
});
expect(chapterContent).toHaveAttr({
'aria-expanded': 'true'
});
});
it('ensures only one active item', function() {
expect(chapterMenu.find('.active').length).toBe(1);
});
});
describe('open section', function() {
it('ensures new section is opened and previous section is closed', function() {
button.last().click();
expect(chapterContent.first()).toBeHidden();
expect(chapterContent.last()).not.toBeHidden();
expect(button.first()).not.toHaveClass('is-open');
expect(button.last()).toHaveClass('is-open');
expect(chapterContent.last().focus).toHaveBeenCalled();
});
it('ensure proper aria and attrs', function() {
expect(button.last()).toHaveAttr({
'aria-pressed': 'false'
});
expect(button.first()).toHaveAttr({
'aria-pressed': 'true'
});
expect(chapterContent.last()).toHaveAttr({
'aria-expanded': 'false'
});
expect(chapterContent.first()).toHaveAttr({
'aria-expanded': 'true'
})
});
});
});
});
});
\ No newline at end of file
var edx = edx || {},
Navigation = (function() {
var navigation = {
init: function() {
if ($('.accordion').length) {
navigation.openAccordion();
}
},
openAccordion: function() {
navigation.checkForCurrent();
navigation.listenForClick();
},
checkForCurrent: function() {
var active = $('.accordion .chapter-content-container .chapter-menu:has(.active)').index('.accordion .chapter-content-container .chapter-menu') ? $('.accordion .chapter-content-container .chapter-menu:has(.active)').index('.accordion .chapter-content-container .chapter-menu') : 0,
activeSection = $('.accordion .button-chapter:eq(' + active + ')');
navigation.closeAccordions();
navigation.openAccordionSection(activeSection);
},
listenForClick: function() {
$('.accordion').on('click', '.button-chapter', function(event) {
navigation.closeAccordions();
navigation.openAccordionSection(event.currentTarget);
});
},
closeAccordions: function() {
$('.chapter-content-container').hide();
$('.chapter-content-container .chapter-menu').hide();
$('.accordion .button-chapter').each(function(event) {
var el = $(this);
el.removeClass('is-open').attr('aria-pressed', 'false');
el.next('.chapter-content-container').attr('aria-expanded', 'false');
el.children('.group-heading').removeClass('active');
el.children('.group-heading').find('.icon').addClass('fa-caret-right').removeClass('fa-caret-down');
});
},
openAccordionSection: function(section) {
var elSection = $(section).next('.chapter-content-container');
elSection.show().focus();
elSection.find('.chapter-menu').show();
$(section).addClass('is-open').attr('aria-pressed', 'true');
$(section).next('.chapter-content-container').attr('aria-expanded', 'true');
$(section).children('.group-heading').addClass('active');
$(section).children('.group-heading').find('.icon').removeClass('fa-caret-right').addClass('fa-caret-down');
}
};
return {
init: navigation.init
};
})();
edx.util = edx.util || {};
edx.util.navigation = Navigation;
edx.util.navigation.init();
...@@ -516,7 +516,7 @@ div.course-wrapper { ...@@ -516,7 +516,7 @@ div.course-wrapper {
} }
} }
div#accordion { .accordion {
visibility: hidden; visibility: hidden;
width: 10px; width: 10px;
padding: 0; padding: 0;
...@@ -524,11 +524,6 @@ div.course-wrapper { ...@@ -524,11 +524,6 @@ div.course-wrapper {
nav { nav {
white-space: pre; white-space: pre;
overflow: hidden; overflow: hidden;
ul {
overflow: hidden;
white-space: nowrap;
}
} }
} }
} }
......
...@@ -6,73 +6,44 @@ ...@@ -6,73 +6,44 @@
%> %>
<%def name="make_chapter(chapter)"> <%def name="make_chapter(chapter)">
<div class="chapter"> <%
<% if chapter.get('active'):
if chapter.get('active'): aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name'])
aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name']) active_class = 'active"'
active_class = ' class="active"' else:
else: aria_label = chapter['display_name']
aria_label = chapter['display_name'] active_class = ''
active_class = '' %>
%> <button class="button-chapter chapter ${active_class}" id="${chapter['display_id']}-parent" aria-controls="${chapter['display_id']}-child" aria-pressed="false">
<h3 ${active_class} aria-label="${aria_label}"> <h3 class="group-heading ${active_class}" aria-label="${aria_label}">
<a href="#"> <span class="icon fa fa-caret-right" aria-hidden="true"></span>
${chapter['display_name']} ${chapter['display_name']}
</a> </h3>
</h3> </button>
<div class="chapter-content-container" tabindex="-1" aria-expanded="false">
<ul> <div class="chapter-menu" id="${chapter['display_id']}-child" role="region" aria-labelledby="${chapter['display_id']}-parent" aria-hidden="false">
% for section in chapter['sections']: % for section in chapter['sections']:
<li class="${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}"> <div class="menu-item ${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}">
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}"> <a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
<p>${section['display_name']} ${'<span class="sr">, current section</span>' if 'active' in section and section['active'] else ''}</p> <p>${section['display_name']} ${'<span class="sr">, current section</span>' if 'active' in section and section['active'] else ''}</p>
<% <%
if section.get('due') is None: if section.get('due') is None:
due_date = '' due_date = ''
else: else:
formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=settings.TIME_ZONE_DISPLAYED_FOR_DEADLINES) formatted_string = get_time_display(section['due'], due_date_display_format, coerce_tz=settings.TIME_ZONE_DISPLAYED_FOR_DEADLINES)
due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string) due_date = '' if len(formatted_string)==0 else _('due {date}').format(date=formatted_string)
%> %>
<p class="subtitle">${section['format']} ${due_date}</p>
## There is behavior differences between % if 'graded' in section and section['graded']:
## rending of sections which have proctoring/timed examinations <img class="menu-icon" src="/static/images/graded.png" alt="Graded Section">
## and those that do not.
##
## Proctoring exposes a exam status message field as well as
## a status icon
% if section['format'] or due_date or 'proctoring' in section:
<p class="subtitle">
% if 'proctoring' in section:
## Display the proctored exam status icon and status message
<i class="fa ${section['proctoring'].get('suggested_icon', 'fa-lock')} ${section['proctoring'].get('status', 'eligible')}"></i>&nbsp;
<span class="subtitle-name">${section['proctoring'].get('short_description', '')}
</span>
## completed proctored exam statuses should not show the due date
## since the exam has already been submitted by the user
% if not section['proctoring'].get('in_completed_state', False):
<span class="subtitle-name">${due_date}</span>
% endif
% else:
## non-proctored section, we just show the exam format and the due date
## this is the standard case in edx-platform
<span class="subtitle-name">${section['format']} ${due_date}
</span>
% endif % endif
</p>
% endif
% if 'graded' in section and section['graded']:
## sections that are graded should indicate this through an icon
<i class="icon fa fa-pencil-square-o" aria-hidden="true" data-tooltip="${_("This section is graded.")}"></i>
% endif
</a> </a>
</li> </div>
% endfor % endfor
</ul> </div>
</div> </div>
</%def> </%def>
% for chapter in toc: % for chapter in toc:
${make_chapter(chapter)} ${make_chapter(chapter)}
% endfor % endfor
\ No newline at end of file
...@@ -165,9 +165,6 @@ ${fragment.foot_html()} ...@@ -165,9 +165,6 @@ ${fragment.foot_html()}
% if disable_accordion is UNDEFINED or not disable_accordion: % if disable_accordion is UNDEFINED or not disable_accordion:
<div class="course-index" role="navigation"> <div class="course-index" role="navigation">
<header id="open_close_accordion">
<a href="#">${_("close")}</a>
</header>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
<div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course"> <div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course">
...@@ -186,8 +183,8 @@ ${fragment.foot_html()} ...@@ -186,8 +183,8 @@ ${fragment.foot_html()}
</div> </div>
% endif % endif
<div id="accordion" style="display: none"> <div class="accordion">
<nav aria-label="${_('Course Navigation')}"> <nav class="course-navigation" aria-label="${_('Course Navigation')}">
% if accordion.strip(): % if accordion.strip():
${accordion} ${accordion}
% else: % else:
......
...@@ -204,11 +204,11 @@ import pytz ...@@ -204,11 +204,11 @@ import pytz
activeHeader: "ui-icon-carat-1-s" activeHeader: "ui-icon-carat-1-s"
}; };
var act = 0; var act = 0;
$("#accordion").accordion( $(".accordion").accordion(
{ {
heightStyle: 'content', heightStyle: 'content',
activate: function(event, ui) { activate: function(event, ui) {
var active = jQuery("#accordion").accordion('option', 'active'); var active = jQuery(".accordion").accordion('option', 'active');
$.cookie('saved_index', null); $.cookie('saved_index', null);
$.cookie('saved_index', active); $.cookie('saved_index', active);
$('#error-msg').val(''); $('#error-msg').val('');
......
...@@ -159,6 +159,7 @@ from branding import api as branding_api ...@@ -159,6 +159,7 @@ from branding import api as branding_api
<%block name="js_extra"/> <%block name="js_extra"/>
<script type="text/javascript" src="${static.url('js/vendor/noreferrer.js')}" charset="utf-8"></script> <script type="text/javascript" src="${static.url('js/vendor/noreferrer.js')}" charset="utf-8"></script>
<script type="text/javascript" src="${static.url('js/utils/navigation.js')}" charset="utf-8"></script>
</body> </body>
</html> </html>
......
...@@ -71,9 +71,6 @@ $("#open_close_accordion a").click(function(){ ...@@ -71,9 +71,6 @@ $("#open_close_accordion a").click(function(){
<div class="book-wrapper"> <div class="book-wrapper">
<section aria-label="${_('Textbook Navigation')}" class="book-sidebar"> <section aria-label="${_('Textbook Navigation')}" class="book-sidebar">
<header id="open_close_accordion">
<a href="#">close</a>
</header>
<ul id="booknav" class="treeview-booknav"> <ul id="booknav" class="treeview-booknav">
<%def name="print_entry(entry)"> <%def name="print_entry(entry)">
......
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