MDL-67321 core_course: Add support for tabbing activities in the chooser
authorMathew May <mathewm@hotmail.co.nz>
Mon, 3 Feb 2020 05:13:14 +0000 (13:13 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Wed, 19 Feb 2020 05:28:54 +0000 (13:28 +0800)
14 files changed:
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/build/local/activitychooser/selectors.min.js
course/amd/build/local/activitychooser/selectors.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
course/amd/src/local/activitychooser/selectors.js
course/templates/chooser.mustache
course/tests/behat/activity_chooser.feature
course/tests/behat/behat_course.php
lang/en/course.php
lang/en/moodle.php

index 39ab5cf..eabdac2 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 77cf4e6..4a79805 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index e1d3ec5..c9ef8b7 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index d2c6bcd..391f7f6 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index d709bb4..d826dbf 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js and b/course/amd/build/local/activitychooser/selectors.min.js differ
index 98e3367..229f9dd 100644 (file)
Binary files a/course/amd/build/local/activitychooser/selectors.min.js.map and b/course/amd/build/local/activitychooser/selectors.min.js.map differ
index 6f6613b..5659961 100644 (file)
@@ -123,8 +123,26 @@ const modalBuilder = data => buildModal(templateDataBuilder(data));
  * @return {Object} Our built object ready to render out
  */
 const templateDataBuilder = (data) => {
+    // Filter the incoming data to find favourite & recommended modules.
+    const favourites = [];
+    const recommended = [];
+
+    // Given the results of the above filters lets figure out what tab to set active.
+
+    // We have some favourites.
+    const favouritesFirst = !!favourites.length;
+    // Check if we have no favourites but have some recommended.
+    const recommendedFirst = !!(recommended.length && favouritesFirst === false);
+    // We have nothing fallback to show all modules.
+    const fallback = favouritesFirst === false && recommendedFirst === false;
+
     return {
         'default': data,
+        favourites: favourites,
+        recommended: recommended,
+        favouritesFirst: favouritesFirst,
+        recommendedFirst: recommendedFirst,
+        fallback: fallback,
     };
 };
 
index 4be4341..b8407e0 100644 (file)
@@ -137,11 +137,71 @@ const registerListenerEvents = (modal, mappedModules) => {
  * Initialise the keyboard navigation controls for the chooser.
  *
  * @method initKeyboardNavigation
- * @param {NodeElement} body Our modal that we are working with
+ * @param {HTMLElement} body Our modal that we are working with
  * @param {Map} mappedModules A map of all of the modules we are working with with K: mod_name V: {Object}
  */
 const initKeyboardNavigation = (body, mappedModules) => {
 
+    // Set up the tab handlers.
+    const favTabNav = body.querySelector(selectors.regions.favouriteTabNav);
+    const recommendedTabNav = body.querySelector(selectors.regions.recommendedTabNav);
+    const defaultTabNav = body.querySelector(selectors.regions.defaultTabNav);
+    const tabNavArray = [favTabNav, recommendedTabNav, defaultTabNav];
+    tabNavArray.forEach((element) => {
+        return element.addEventListener('keyup', (e) => {
+            const firstLink = e.target.parentElement.parentElement.firstElementChild.firstElementChild;
+            const lastLink = e.target.parentElement.parentElement.lastElementChild.firstElementChild;
+
+            if (e.keyCode === arrowRight) {
+                const nextLink = e.target.parentElement.nextElementSibling;
+                if (nextLink === null) {
+                    e.srcElement.tabIndex = -1;
+                    firstLink.tabIndex = 0;
+                    firstLink.focus();
+                } else if (nextLink.firstElementChild.classList.contains('d-none')) {
+                    e.srcElement.tabIndex = -1;
+                    lastLink.tabIndex = 0;
+                    lastLink.focus();
+                } else {
+                    e.srcElement.tabIndex = -1;
+                    nextLink.firstElementChild.tabIndex = 0;
+                    nextLink.firstElementChild.focus();
+                }
+            }
+            if (e.keyCode === arrowLeft) {
+                const previousLink = e.target.parentElement.previousElementSibling;
+                if (previousLink === null) {
+                    e.srcElement.tabIndex = -1;
+                    lastLink.tabIndex = 0;
+                    lastLink.focus();
+                } else if (previousLink.firstElementChild.classList.contains('d-none')) {
+                    e.srcElement.tabIndex = -1;
+                    firstLink.tabIndex = 0;
+                    firstLink.focus();
+                } else {
+                    e.srcElement.tabIndex = -1;
+                    previousLink.firstElementChild.tabIndex = 0;
+                    previousLink.firstElementChild.focus();
+                }
+            }
+            if (e.keyCode === home) {
+                e.srcElement.tabIndex = -1;
+                firstLink.tabIndex = 0;
+                firstLink.focus();
+            }
+            if (e.keyCode === end) {
+                e.srcElement.tabIndex = -1;
+                lastLink.tabIndex = 0;
+                lastLink.focus();
+            }
+            if (e.keyCode === space) {
+                e.preventDefault();
+                e.target.click();
+            }
+        });
+    });
+
+    // Set up the handlers for the modules.
     const chooserOptions = body.querySelectorAll(selectors.regions.chooserOption.container);
 
     Array.from(chooserOptions).forEach((element) => {
index adeb07f..f00b620 100644 (file)
@@ -51,6 +51,12 @@ export default {
         carousel: getDataSelector('region', 'carousel'),
         help: getDataSelector('region', 'help'),
         modules: getDataSelector('region', 'modules'),
+        favouriteTabNav: getDataSelector('region', 'favourite-tab-nav'),
+        recommendedTabNav: getDataSelector('region', 'recommended-tab-nav'),
+        defaultTabNav: getDataSelector('region', 'default-tab-nav'),
+        favouriteTab: getDataSelector('region', 'favourites'),
+        recommendedTab: getDataSelector('region', 'recommended'),
+        defaultTab: getDataSelector('region', 'default'),
         getModuleSelector: modname => `[role="menuitem"][data-modname="${modname}"]`
     },
     actions: {
index c503c9b..46c0762 100644 (file)
 }}
 <div data-region="carousel" class="carousel slide">
     <div class="carousel-inner" aria-live="polite">
-        <div class="carousel-item active" data-region="modules">
-            <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
-                <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container">
-                    {{#default}}
-                        {{>core_course/chooser_item}}
-                    {{/default}}
+        <div class="carousel-item p-4 active" data-region="modules">
+            <ul class="nav nav-tabs mb-2" id="activities-{{uniqid}}" role="tablist">
+                <li class="nav-item">
+                    <a class="nav-link {{#favouritesFirst}}active{{/favouritesFirst}} {{^favourites}}d-none{{/favourites}}"
+                       id="starred-tab-{{uniqid}}"
+                       data-toggle="tab"
+                       data-region="favourite-tab-nav"
+                       href="#starred-{{uniqid}}"
+                       role="tab"
+                       aria-label="{{#str}} aria:favouritestab, core_course {{/str}}"
+                       aria-controls="starred-{{uniqid}}"
+                       aria-selected="{{#favouritesFirst}}true{{/favouritesFirst}}{{^favouritesFirst}}false{{/favouritesFirst}}"
+                       tabindex="{{#favouritesFirst}}0{{/favouritesFirst}}{{^favouritesFirst}}-1{{/favouritesFirst}}"
+                    >
+                        {{#str}}favourites{{/str}}
+                    </a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link {{#recommendedFirst}}active{{/recommendedFirst}} {{^recommended}}d-none{{/recommended}}"
+                       id="recommended-tab-{{uniqid}}"
+                       data-region="recommended-tab-nav"
+                       data-toggle="tab"
+                       href="#recommended-{{uniqid}}"
+                       role="tab"
+                       aria-label="{{#str}} aria:recommendedtab, core_course {{/str}}"
+                       aria-controls="recommended-{{uniqid}}"
+                       aria-selected="{{#recommendedFirst}}true{{/recommendedFirst}}{{^recommendedFirst}}false{{/recommendedFirst}}"
+                       tabindex="{{#recommendedFirst}}0{{/recommendedFirst}}{{^recommendedFirst}}-1{{/recommendedFirst}}">
+                        {{#str}}recommended{{/str}}
+                    </a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link {{#fallback}}active{{/fallback}}"
+                       id="all-tab-{{uniqid}}"
+                       data-toggle="tab"
+                       data-region="default-tab-nav"
+                       href="#all-{{uniqid}}"
+                       role="tab"
+                       aria-label="{{#str}} aria:defaulttab, core_course {{/str}}"
+                       aria-controls="all-{{uniqid}}"
+                       aria-selected="{{#fallback}}true{{/fallback}}{{^fallback}}false{{/fallback}}"
+                       tabindex="{{#fallback}}0{{/fallback}}{{^fallback}}-1{{/fallback}}"
+                    >
+                        {{#str}}activities{{/str}}
+                    </a>
+                </li>
+            </ul>
+            <div class="tab-content" id="tabbed-activities-{{uniqid}}">
+                <div class="tab-pane {{#favouritesFirst}}active{{/favouritesFirst}}" id="starred-{{uniqid}}" data-region="favourites" role="tabpanel" aria-labelledby="starred-tab-{{uniqid}}">
+                    <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
+                        <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container" data-render="favourites-area">
+                            {{#favourites}}
+                                {{>core_course/chooser_item}}
+                            {{/favourites}}
+                        </div>
+                    </div>
+                </div>
+                <div class="tab-pane {{#recommendedFirst}}active{{/recommendedFirst}}" id="recommended-{{uniqid}}" data-region="recommended" role="tabpanel" aria-labelledby="recommended-tab-{{uniqid}}">
+                    <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
+                        <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container">
+                            {{#recommended}}
+                                {{>core_course/chooser_item}}
+                            {{/recommended}}
+                        </div>
+                    </div>
+                </div>
+                <div class="tab-pane {{#fallback}}active{{/fallback}}" id="all-{{uniqid}}" data-region="default" role="tabpanel" aria-labelledby="all-tab-{{uniqid}}">
+                    <div class="modchoosercontainer" data-region="chooser-container" aria-label="{{#str}} activitymodules {{/str}}">
+                        <div class="optionscontainer d-flex flex-wrap mw-100 p-3 position-relative" role="menubar" data-region="chooser-options-container">
+                            {{#default}}
+                                {{>core_course/chooser_item}}
+                            {{/default}}
+                        </div>
+                    </div>
                 </div>
             </div>
         </div>
index 9d3f2b9..b8be7a9 100644 (file)
@@ -53,3 +53,9 @@ Feature: Display and choose from the available activities in course
     And "help" "core_course > Activity chooser screen" should not exist
     And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
     And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
+
+  # Currently stubbed out in MDL-67321 as further issues will add more tabs.
+  Scenario: Navigate between module tabs
+    Given I open the activity chooser
+    And I should see "Activities" in the "Add an activity or resource" "dialogue"
+    Then I should see "Forum" in the "default" "core_course > Activity chooser tab"
index 8453529..ed41a46 100644 (file)
@@ -54,6 +54,11 @@ class behat_course extends behat_base {
                     "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' carousel-item ')]"
                 ]
             ),
+            new behat_component_named_selector(
+                'Activity chooser tab', [
+                    "%core_course/activityChooser%//*[@data-region=%locator%][contains(concat(' ', @class, ' '), ' tab-pane ')]"
+                ]
+            ),
         ];
     }
 
index 2573793..ae9eefb 100644 (file)
@@ -26,7 +26,10 @@ $string['aria:coursecategory'] = 'Course category';
 $string['aria:courseimage'] = 'Course image';
 $string['aria:courseshortname'] = 'Course short name';
 $string['aria:coursename'] = 'Course name';
+$string['aria:defaulttab'] = 'The default modules';
 $string['aria:favourite'] = 'Course is starred';
+$string['aria:favouritestab'] = 'Your starred modules';
+$string['aria:recommendedtab'] = 'The recommended modules';
 $string['coursealreadyfinished'] = 'Course already finished';
 $string['coursenotyetstarted'] = 'The course has not yet started';
 $string['coursenotyetfinished'] = 'The course has not yet finished';
index 5010450..4995dc7 100644 (file)
@@ -1637,6 +1637,7 @@ $string['readme'] = 'README';
 $string['recentactivity'] = 'Recent activity';
 $string['recentactivityreport'] = 'Full report of recent activity...';
 $string['recipientslist'] = 'Recipients list';
+$string['recommended'] = 'Recommended';
 $string['recreatedcategory'] = 'Recreated category {$a}';
 $string['redirect'] = 'Redirect';
 $string['reducesections'] = 'Reduce the number of sections';