diff --git a/common/test/acceptance/pages/lms/course_nav.py b/common/test/acceptance/pages/lms/course_nav.py index e204260..a7bde28 100644 --- a/common/test/acceptance/pages/lms/course_nav.py +++ b/common/test/acceptance/pages/lms/course_nav.py @@ -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) # Add one to convert from list index to CSS index - section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1) + section_css = 'nav>div.chapter:nth-of-type({0})>h3>a'.format(sec_index + 1) self.q(css=section_css).first.click() # Get the subsection by index @@ -94,10 +94,9 @@ class CourseNavPage(PageObject): return # Convert list indices (start at zero) to CSS indices (start at 1) - subsection_css = ( - ".course-navigation .chapter-content-container:nth-of-type({0}) " - ".chapter-menu .menu-item:nth-of-type({1})" - ).format(sec_index + 1, subsec_index + 1) + subsection_css = "nav>div.chapter:nth-of-type({0})>ul>li:nth-of-type({1})>a".format( + sec_index + 1, subsec_index + 1 + ) # Click the subsection and ensure that the page finishes reloading self.q(css=subsection_css).first.click() @@ -131,7 +130,7 @@ class CourseNavPage(PageObject): """ Return a list of all section titles on the page. """ - chapter_css = '.course-navigation .chapter .group-heading' + chapter_css = 'nav > div.chapter > h3 > a' return self.q(css=chapter_css).map(lambda el: el.text.strip()).results def _subsection_titles(self, section_index): @@ -141,10 +140,7 @@ class CourseNavPage(PageObject): """ # Retrieve the subsection title for the section # Add one to the list index to get the CSS index, which starts at one - subsection_css = ( - ".course-navigation .chapter-content-container:nth-of-type({0}) " - ".chapter-menu .menu-item a p:nth-of-type(1)" - ).format(section_index) + subsection_css = 'nav>div.chapter:nth-of-type({0})>ul>li>a>p:nth-of-type(1)'.format(section_index) # If the element is visible, we can get its text directly # Otherwise, we need to get the HTML @@ -175,8 +171,8 @@ class CourseNavPage(PageObject): 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). """ - current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text - current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text + current_section_list = self.q(css='nav>div.chapter.is-open>h3>a').text + current_subsection_list = self.q(css='nav>div.chapter.is-open li.active>a>p').text if len(current_section_list) == 0: self.warning("Could not find the current section") diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py index 6f61da7..bb84268 100644 --- a/common/test/acceptance/pages/lms/courseware.py +++ b/common/test/acceptance/pages/lms/courseware.py @@ -14,7 +14,7 @@ class CoursewarePage(CoursePage): url_path = "courseware/" xblock_component_selector = '.vert .xblock' section_selector = '.chapter' - subsection_selector = '.chapter-content-container .chapter-menu a' + subsection_selector = '.chapter ul li' def is_browser_on_page(self): return self.q(css='body.courseware').present @@ -102,7 +102,7 @@ class CoursewarePage(CoursePage): """ return the url of the active subsection in the left nav """ - return self.q(css='.chapter-content-container .chapter-menu .menu-item.active a').attrs('href')[0] + return self.q(css='.chapter ul li.active a').attrs('href')[0] @property def can_start_proctored_exam(self): diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index 7a945b7..ca0a6a2 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -1121,7 +1121,7 @@ class EntranceExamTest(UniqueCourseTest): When I view the courseware that has an entrance exam Then there should be an "Entrance Exam" chapter.' """ - entrance_exam_link_selector = '.accordion .course-navigation .chapter .group-heading' + entrance_exam_link_selector = 'div#accordion nav div h3 a' # visit courseware page and make sure there is not entrance exam chapter. self.courseware_page.visit() self.courseware_page.wait_for_page() diff --git a/lms/djangoapps/courseware/features/navigation.py b/lms/djangoapps/courseware/features/navigation.py index 1b3613f..72eb109 100644 --- a/lms/djangoapps/courseware/features/navigation.py +++ b/lms/djangoapps/courseware/features/navigation.py @@ -92,7 +92,7 @@ def when_i_navigate_to_a_section(step): world.disable_jquery_animations() # Open the 2nd section - world.css_click(css_selector='button.chapter', index=1) + world.css_click(css_selector='div.chapter', index=1) subsection_css = 'a[href*="Test_Subsection_2/"]' # Click on the subsection to see the content diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 43b9866..ddb321e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -74,7 +74,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.x_module import XModuleDescriptor from xmodule.mixin import wrap_with_license 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 import milestones_helpers from verify_student.services import ReverificationService @@ -166,7 +165,6 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_ for chapter in chapters: # Only show required content, if there is required content # chapter.hide_from_toc is read-only (boo) - display_id = slugify(chapter.display_name_with_default) local_hide_from_toc = False if required_content: if unicode(chapter.location) not in required_content: @@ -248,7 +246,6 @@ def toc_for_course(user, request, course, active_chapter, active_section, field_ sections.append(section_context) toc_chapters.append({ 'display_name': chapter.display_name_with_default, - 'display_id': display_id, 'url_name': chapter.url_name, 'sections': sections, 'active': chapter.url_name == active_chapter diff --git a/lms/djangoapps/courseware/tests/test_entrance_exam.py b/lms/djangoapps/courseware/tests/test_entrance_exam.py index ae1fd86..636dcc1 100644 --- a/lms/djangoapps/courseware/tests/test_entrance_exam.py +++ b/lms/djangoapps/courseware/tests/test_entrance_exam.py @@ -155,8 +155,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): } ], 'url_name': u'Entrance_Exam_Section_-_Chapter_1', - 'display_name': u'Entrance Exam Section - Chapter 1', - 'display_id': u'entrance-exam-section-chapter-1', + 'display_name': u'Entrance Exam Section - Chapter 1' } ] ) @@ -183,22 +182,19 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): } ], 'url_name': u'Overview', - 'display_name': u'Overview', - 'display_id': u'overview' + 'display_name': u'Overview' }, { 'active': False, 'sections': [], 'url_name': u'Week_1', - 'display_name': u'Week 1', - 'display_id': u'week-1' + 'display_name': u'Week 1' }, { 'active': False, 'sections': [], 'url_name': u'Instructor', - 'display_name': u'Instructor', - 'display_id': u'instructor' + 'display_name': u'Instructor' }, { 'active': True, @@ -213,8 +209,7 @@ class EntranceExamTestCases(LoginEnrollmentTestCase, ModuleStoreTestCase): } ], 'url_name': u'Entrance_Exam_Section_-_Chapter_1', - 'display_name': u'Entrance Exam Section - Chapter 1', - 'display_id': u'entrance-exam-section-chapter-1' + 'display_name': u'Entrance Exam Section - Chapter 1' } ] ) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 3f4da9b..8f8849a 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -633,7 +633,6 @@ class TestTOC(ModuleStoreTestCase): def test_toc_toy_from_chapter(self, default_ms, setup_finds, setup_sends, toc_finds): with self.store.default_store(default_ms): self.setup_modulestore(default_ms, setup_finds, setup_sends) - expected = ([{'active': True, 'sections': [{'url_name': 'Toy_Videos', 'display_name': u'Toy Videos', 'graded': True, 'format': u'Lecture Sequence', 'due': None, 'active': False}, @@ -643,11 +642,11 @@ class TestTOC(ModuleStoreTestCase): 'format': '', 'due': None, 'active': False}, {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True, 'format': '', 'due': None, 'active': False}], - 'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'}, + 'url_name': 'Overview', 'display_name': u'Overview'}, {'active': False, 'sections': [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, 'format': '', 'due': None, 'active': False}], - 'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}]) + 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) course = self.store.get_course(self.toy_course.id, depth=2) with check_mongo_calls(toc_finds): @@ -683,11 +682,11 @@ class TestTOC(ModuleStoreTestCase): 'format': '', 'due': None, 'active': False}, {'url_name': 'video_4f66f493ac8f', 'display_name': 'Video', 'graded': True, 'format': '', 'due': None, 'active': False}], - 'url_name': 'Overview', 'display_name': u'Overview', 'display_id': u'overview'}, + 'url_name': 'Overview', 'display_name': u'Overview'}, {'active': False, 'sections': [{'url_name': 'toyvideo', 'display_name': 'toyvideo', 'graded': True, 'format': '', 'due': None, 'active': False}], - 'url_name': 'secret:magic', 'display_name': 'secret:magic', 'display_id': 'secretmagic'}]) + 'url_name': 'secret:magic', 'display_name': 'secret:magic'}]) with check_mongo_calls(toc_finds): actual = render.toc_for_course( diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 0e669ed..ec8b15f 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -107,6 +107,7 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): 'chapter': 'Chrome', 'section': displayname, })) + self.assertEquals('open_close_accordion' in response.content, accordion) self.assertEquals('course-tabs' in response.content, tabs) self.assertTabInactive('progress', response) diff --git a/lms/djangoapps/courseware/testutils.py b/lms/djangoapps/courseware/testutils.py index bd0c804..b0e4e8b 100644 --- a/lms/djangoapps/courseware/testutils.py +++ b/lms/djangoapps/courseware/testutils.py @@ -25,6 +25,7 @@ class RenderXBlockTestMixin(object): # DOM elements that appear in the LMS Courseware, # but are excluded from the xBlock-only rendering. COURSEWARE_CHROME_HTML_ELEMENTS = [ + '<header id="open_close_accordion"', '<ol class="course-tabs"', '<footer id="footer-openedx"', '<div class="window-wrap"', diff --git a/lms/static/coffee/fixtures/accordion.html b/lms/static/coffee/fixtures/accordion.html new file mode 100644 index 0000000..148c245 --- /dev/null +++ b/lms/static/coffee/fixtures/accordion.html @@ -0,0 +1,6 @@ +<div class="course-wrapper"> + <header id="open_close_accordion"> + <a href="#">close</a> + </header> + <div id="accordion"></div> +</div> diff --git a/lms/static/coffee/spec/courseware_spec.coffee b/lms/static/coffee/spec/courseware_spec.coffee index 129b430..c6b0c60 100644 --- a/lms/static/coffee/spec/courseware_spec.coffee +++ b/lms/static/coffee/spec/courseware_spec.coffee @@ -1,5 +1,10 @@ describe 'Courseware', -> describe 'start', -> + it 'create the navigation', -> + spyOn(window, 'Navigation') + Courseware.start() + expect(window.Navigation).toHaveBeenCalled() + it 'binds the Logger', -> spyOn(Logger, 'bind') Courseware.start() diff --git a/lms/static/coffee/spec/navigation_spec.coffee b/lms/static/coffee/spec/navigation_spec.coffee new file mode 100644 index 0000000..162eff3 --- /dev/null +++ b/lms/static/coffee/spec/navigation_spec.coffee @@ -0,0 +1,72 @@ +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' diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index 49e27c4..50fcb3e 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -2,6 +2,7 @@ class @Courseware @prefix: '' constructor: -> + new Navigation Logger.bind() @render() diff --git a/lms/static/coffee/src/navigation.coffee b/lms/static/coffee/src/navigation.coffee new file mode 100644 index 0000000..06c38b7 --- /dev/null +++ b/lms/static/coffee/src/navigation.coffee @@ -0,0 +1,33 @@ +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 diff --git a/lms/static/js/fixtures/accordion.html b/lms/static/js/fixtures/accordion.html deleted file mode 100644 index ae3f8bc..0000000 --- a/lms/static/js/fixtures/accordion.html +++ /dev/null @@ -1,56 +0,0 @@ -<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 diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 5040318..cb97519 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -64,7 +64,6 @@ '_split': 'js/split', 'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer', 'MathJaxProcessor': 'coffee/src/customwmd', - 'js/utils/navigation': 'js/utils/navigation', // Manually specify LMS files that are not converted to RequireJS 'history': 'js/vendor/history', @@ -804,8 +803,6 @@ '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/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); diff --git a/lms/static/js/spec/navigation_spec.js b/lms/static/js/spec/navigation_spec.js deleted file mode 100644 index 85e37fd..0000000 --- a/lms/static/js/spec/navigation_spec.js +++ /dev/null @@ -1,74 +0,0 @@ -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 diff --git a/lms/static/js/utils/navigation.js b/lms/static/js/utils/navigation.js deleted file mode 100644 index 1ba7058..0000000 --- a/lms/static/js/utils/navigation.js +++ /dev/null @@ -1,70 +0,0 @@ -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(); diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 345c093..6d4b0d6 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -516,7 +516,7 @@ div.course-wrapper { } } - .accordion { + div#accordion { visibility: hidden; width: 10px; padding: 0; @@ -524,6 +524,11 @@ div.course-wrapper { nav { white-space: pre; overflow: hidden; + + ul { + overflow: hidden; + white-space: nowrap; + } } } } diff --git a/lms/static/sass/course/courseware/_sidebar.scss b/lms/static/sass/course/courseware/_sidebar.scss index d2ab1ed..07a0bdf 100644 --- a/lms/static/sass/course/courseware/_sidebar.scss +++ b/lms/static/sass/course/courseware/_sidebar.scss @@ -1,161 +1,234 @@ .course-index { - @include transition( all .2s $ease-in-out-quad 0s); - @include border-right(1px solid $border-color-2); - @include border-radius(3px, 0, 0, 3px); - display: table-cell; // needed to extend the sidebar the full height of the area + @extend .sidebar; + @extend .tran; + @include border-right(1px solid $border-color-2); + @include border-radius(3px, 0, 0, 3px); + + #open_close_accordion { + display: none; + } + + header { + max-height: 47px; + + h2 { + white-space: nowrap; + } + } + + div#accordion { + width: auto; + font-size: 14px; - // reseting bolded fonts for the course index h3 { - @extend %t-regular; + border-radius: 0; + margin: 0; + overflow: visible; + font-size: 16px; + + &:first-child { + border: none; + } + + &:hover, &:focus { + color: #666; + } + + &.ui-state-hover, &.ui-state-focus { + a { + color: #666; + } + } + + &.ui-accordion-header { + border-bottom: none; + color: $black; + + a { + border-radius: 0; + box-shadow: none; + @include padding-left(19px); + color: $link-color; + } + + &.ui-state-active { + @extend .active; + border-bottom: none; + + &:hover, &:focus { + background: none; + } + } + + span.ui-icon { + @include left(0); + opacity: 0.3; + background-image: url("/static/images/ui-icons_222222_256x240.png"); // jQuery UI sprite + + &.ui-icon-triangle-1-e { + + // CASE: left to right layout + @include ltr { + background-position: -32px -16px; // jQuery UI east arrow position + } + + // CASE: right to left layout + @include rtl { + background-position: -96px -16px; // jQuery UI west arrow position + } + } + } + } + } + + .chapter { + width: 100% !important; + @include box-sizing(border-box); + padding: 11px 14px; + @include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom); + background-color: $sidebar-chapter-bg; + box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset; + @include transition(background-color .1s linear 0s); + + &.is-open { + background: $white; + } + + &:first-child { + border-radius: 3px 0 0 0; + } + + &:last-child { + border-radius: 0 0 0 3px; + box-shadow: 0 1px 0 $white inset; + } + + &:hover, &:focus { + background-color: $white; + } } - // we're targetting the .class now, instead of the #id - .accordion { - @extend %t-copy-sub1; - - .course-navigation { - - .button-chapter { - @include box-sizing(border-box); - @include linear-gradient(top, $sidebar-chapter-bg-top, $sidebar-chapter-bg-bottom); - @include transition(background-color .1s linear 0s); - width: 100% !important; - border: 0; - border-radius: 0; - padding: 0; - box-shadow: 0 1px 0 $white inset, 0 -1px 0 $shadow-l1 inset; - background-color: $sidebar-chapter-bg; - color: $link-color; - - &.is-open { - background: $white; - box-shadow: none; - } - - &.active { - - .group-heading { - @extend %t-ultrastrong; - color: $base-font-color; - } - } - - .group-heading { - @extend %t-title6; - position: relative; - padding: ($baseline*.75) $baseline ($baseline*.75) ($baseline*2); - color: $link-color; - - .icon { - position: absolute; - @include left($baseline); - top: $baseline; - color: $gray-d1; - } - } + ul.ui-accordion-content { + background: transparent; + border: none; + border-radius: 0; + margin: 0; + padding: 9px 0 9px 9px; + overflow: auto; + width: 100%; + + li { + border-bottom: 0; + border-radius: 0; + margin-bottom: ($baseline/5); + + a { + background: transparent; + border-radius: 4px; + display: block; + @include padding( ($baseline/4), ($baseline*1.5), ($baseline/4), ($baseline/2)); + position: relative; + text-decoration: none; + + p { + font-weight: bold; + font-family: $sans-serif; + margin-bottom: 0; + line-height: 1.3; + + &.subtitle { + @extend %t-copy-sub2; + @extend %t-weight2; + display: inline-block; + width: 90%; + color: $gray-d1; + margin: 0; + + &:empty { + display: none; + } + + // definitions for proctored exam attempt status indicators + i.verified { + color: $success-color; + } + + i.rejected { + color: $alert-color; + } + + i.error { + color: $alert-color; + } } + } - .chapter-content-container { - display: none; // programmatically shown when the section is open; closed at the start - padding: 0 14px ($baseline/2) 14px; - border-bottom: 1px solid $shadow-l1; - background: $white; - - &:focus { - // we don't need the blue glow to be here at this point - // although focus is sent for correct keyboard/a11y - outline: none; - } - - .chapter-menu { - padding: 0 ($baseline*.75); - background: inherit; - overflow: hidden; - - .menu-item { - @extend %t-strong; - @include padding(($baseline/4) ($baseline/2)); - margin: ($baseline/5) 0; - border-radius: ($baseline/5); - background: inherit; - - a { - position: relative; - display: block; - color: $base-font-color; - - p { - @extend %t-action3; - @extend %t-strong; - font-family: $sans-serif; - - &.subtitle { - @extend %t-action3; - @extend %t-regular; - display: block; - margin: 0; - color: $gray-d1; - - &:empty { - // hide empty subtitles - display: none; - } - - // definitions for proctored exam attempt status indicators - .verified { - color: $success-color; - } - - .rejected { - color: $alert-color; - } - - .error { - color: $alert-color; - } - } - } - - &:hover, - &:focus { - color: $link-color; - - .subtitle { - color: $gray-d1; - } - } - } - - &.graded { - - .menu-icon { - @include right(0); - position: absolute; - bottom: 0; - } - } - - &.active { - @extend %t-strong; - background: linear-gradient(to bottom, $gray-l4, #d6d6d6); - - // pre-existing; but not sure where it appears - &:after { - @extend %t-regular; - @include transition(none); - @include right($baseline); - position: absolute; - top: 50%; - margin-top: -13px; - font-size: 30px; - color: $gray-d3; - content: '›'; - opacity: 0; - } - } - } - } + &:hover, &:focus { + background: $shadow-l1; + + > a p { + color: $gray-d3; } + } + + &:active { + box-shadow: inset 0 1px 14px 0 $shadow-l1; + + &:after { + opacity: 1.0; + right: 15px; + } + } + } + + &.active { + @extend %t-weight5; + + &:after { + content: '›'; + position: absolute; + top: 50%; + right: 20px; + margin-top: -13px; + font-size: 30px; + font-weight: normal; + color: #333; + opacity: 0; + @include transition(none); + } + + > a { + border: 1px solid $border-color-1; + box-shadow: 0 1px 0 rgba(255, 255, 255, .35) inset; + background: $sidebar-active-image; + + &:after { + opacity: 1.0; + right: 15px; + } + + p { + color: #333; + } + } + + span.subtitle { + @extend %t-weight2; + } + } + + &.graded { + > a { + .icon { + vertical-align: middle; + } + } + + &.active > a { + background: linear-gradient(to bottom, #e6e6e6, #d6d6d6); + } } + } } -} \ No newline at end of file + } +} diff --git a/lms/templates/courseware/accordion.html b/lms/templates/courseware/accordion.html index 7cfb7fb..2d429ef 100644 --- a/lms/templates/courseware/accordion.html +++ b/lms/templates/courseware/accordion.html @@ -6,44 +6,73 @@ %> <%def name="make_chapter(chapter)"> -<% -if chapter.get('active'): - aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name']) - active_class = 'active"' -else: - aria_label = chapter['display_name'] - active_class = '' -%> -<button class="button-chapter chapter ${active_class}" id="${chapter['display_id']}-parent" aria-controls="${chapter['display_id']}-child" aria-pressed="false"> - <h3 class="group-heading ${active_class}" aria-label="${aria_label}"> - <span class="icon fa fa-caret-right" aria-hidden="true"></span> - ${chapter['display_name']} - </h3> -</button> -<div class="chapter-content-container" tabindex="-1" aria-expanded="false"> - <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']: - <div class="menu-item ${'active' if 'active' in section and section['active'] else ''} ${'graded' if 'graded' in section and section['graded'] else ''}"> + <div class="chapter"> + <% + if chapter.get('active'): + aria_label = _('{chapter}, current chapter').format(chapter=chapter['display_name']) + active_class = ' class="active"' + else: + aria_label = chapter['display_name'] + active_class = '' + %> + <h3 ${active_class} aria-label="${aria_label}"> + <a href="#"> + ${chapter['display_name']} + </a> + </h3> + + <ul> + % 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 ''}"> <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: due_date = '' else: 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) - %> - <p class="subtitle">${section['format']} ${due_date}</p> - % if 'graded' in section and section['graded']: - <img class="menu-icon" src="/static/images/graded.png" alt="Graded Section"> + %> + + ## There is behavior differences between + ## rending of sections which have proctoring/timed examinations + ## 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> + <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 + </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> - </div> - % endfor - </div> -</div> + </li> + % endfor + </ul> + </div> </%def> % for chapter in toc: ${make_chapter(chapter)} -% endfor \ No newline at end of file +% endfor diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 28b70cc..224a4ef 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -165,6 +165,9 @@ ${fragment.foot_html()} % if disable_accordion is UNDEFINED or not disable_accordion: <div class="course-index" role="navigation"> + <header id="open_close_accordion"> + <a href="#">${_("close")}</a> + </header> % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): <div id="courseware-search-bar" class="search-bar courseware-search-bar" role="search" aria-label="Course"> @@ -183,8 +186,8 @@ ${fragment.foot_html()} </div> % endif - <div class="accordion"> - <nav class="course-navigation" aria-label="${_('Course Navigation')}"> + <div id="accordion" style="display: none"> + <nav aria-label="${_('Course Navigation')}"> % if accordion.strip(): ${accordion} % else: diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index 0b1c766..5a0daee 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -204,11 +204,11 @@ import pytz activeHeader: "ui-icon-carat-1-s" }; var act = 0; - $(".accordion").accordion( + $("#accordion").accordion( { heightStyle: 'content', 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', active); $('#error-msg').val(''); diff --git a/lms/templates/main.html b/lms/templates/main.html index e1ebaff..40c62ee 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -159,7 +159,6 @@ from branding import api as branding_api <%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/utils/navigation.js')}" charset="utf-8"></script> </body> </html> diff --git a/lms/templates/staticbook.html b/lms/templates/staticbook.html index a55b864..5664adc 100644 --- a/lms/templates/staticbook.html +++ b/lms/templates/staticbook.html @@ -71,6 +71,9 @@ $("#open_close_accordion a").click(function(){ <div class="book-wrapper"> <section aria-label="${_('Textbook Navigation')}" class="book-sidebar"> + <header id="open_close_accordion"> + <a href="#">close</a> + </header> <ul id="booknav" class="treeview-booknav"> <%def name="print_entry(entry)">