MDL-69301 core: Enhanced WAI-ARIA support for tab elements
authorShamim Rezaie <shamim@moodle.com>
Wed, 16 Sep 2020 15:12:54 +0000 (01:12 +1000)
committerShamim Rezaie <shamim@moodle.com>
Wed, 2 Dec 2020 17:22:09 +0000 (04:22 +1100)
lib/templates/settings_link_page.mustache
theme/boost/amd/build/aria.min.js
theme/boost/amd/build/aria.min.js.map
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/aria.js
theme/boost/amd/src/loader.js
theme/boost/templates/admin_setting_tabs.mustache

index 2991753..07f0705 100644 (file)
@@ -40,7 +40,7 @@
 <ul class="nav nav-tabs" role="tablist">
 <!-- First the top most node and immediate children -->
     <li class="nav-item">
 <ul class="nav nav-tabs" role="tablist">
 <!-- First the top most node and immediate children -->
     <li class="nav-item">
-        <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab">{{node.text}}</a>
+        <a class="nav-link active" href="#link{{node.key}}" data-toggle="tab" role="tab" aria-selected="true">{{node.text}}</a>
     </li>
 <!-- Now the first level children with sub nodes -->
 {{#node.children}}
     </li>
 <!-- Now the first level children with sub nodes -->
 {{#node.children}}
@@ -48,7 +48,7 @@
         {{#display}}
             {{^is_short_branch}}
                 <li class="nav-item">
         {{#display}}
             {{^is_short_branch}}
                 <li class="nav-item">
-                    <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab">{{text}}</a>
+                    <a class="nav-link" href="#link{{key}}" data-toggle="tab" role="tab" aria-selected="false" tabindex="-1">{{text}}</a>
                 </li>
             {{/is_short_branch}}
         {{/display}}
                 </li>
             {{/is_short_branch}}
         {{/display}}
index e266186..2265ffa 100644 (file)
Binary files a/theme/boost/amd/build/aria.min.js and b/theme/boost/amd/build/aria.min.js differ
index 509f868..72602e5 100644 (file)
Binary files a/theme/boost/amd/build/aria.min.js.map and b/theme/boost/amd/build/aria.min.js.map differ
index 188f865..4f9b804 100644 (file)
Binary files a/theme/boost/amd/build/loader.min.js and b/theme/boost/amd/build/loader.min.js differ
index 63d1fc7..f9e4a1b 100644 (file)
Binary files a/theme/boost/amd/build/loader.min.js.map and b/theme/boost/amd/build/loader.min.js.map differ
index 812aad7..de4049e 100644 (file)
@@ -21,7 +21,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-import {end, escape, arrowUp, arrowDown, home, enter, space} from 'core/key_codes';
+import {end, escape, arrowUp, arrowDown, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';
 import $ from 'jquery';
 import Pending from 'core/pending';
 
 import $ from 'jquery';
 import Pending from 'core/pending';
 
@@ -206,7 +206,87 @@ const autoFocus = () => {
     });
 };
 
     });
 };
 
+/**
+ * Changes the focus to the correct tab based on the key that is pressed.
+ * @param {KeyboardEvent} e
+ */
+const updateTabFocus = e => {
+    const tabList = e.target.closest('[role="tablist"]');
+    const vertical = tabList.getAttribute('aria-orientation') == 'vertical';
+    const rtl = window.right_to_left();
+    const arrowNext = vertical ? arrowDown : (rtl ? arrowLeft : arrowRight);
+    const arrowPrevious = vertical ? arrowUp : (rtl ? arrowRight : arrowLeft);
+    const tabs = Array.prototype.filter.call(
+        tabList.querySelectorAll('[role="tab"]'),
+        tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.
+
+    for (let i = 0; i < tabs.length; i++) {
+        tabs[i].index = i;
+    }
+
+    switch (e.keyCode) {
+        case arrowNext:
+            e.preventDefault();
+            if (e.target.index !== undefined && tabs[e.target.index + 1]) {
+                tabs[e.target.index + 1].focus();
+            } else {
+                tabs[0].focus();
+            }
+            break;
+        case arrowPrevious:
+            e.preventDefault();
+            if (e.target.index !== undefined && tabs[e.target.index - 1]) {
+                tabs[e.target.index - 1].focus();
+            } else {
+                tabs[tabs.length - 1].focus();
+            }
+            break;
+        case home:
+            e.preventDefault();
+            tabs[0].focus();
+            break;
+        case end:
+            e.preventDefault();
+            tabs[tabs.length - 1].focus();
+            break;
+        case enter:
+        case space:
+            e.preventDefault();
+            $(e.target).tab('show');
+            tabs.forEach(tab => {
+                tab.tabIndex = -1;
+            });
+            e.target.tabIndex = 0;
+    }
+};
+
+/**
+ * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.
+ */
+const tabElementFix = () => {
+    document.addEventListener('keydown', e => {
+        if ([arrowUp, arrowDown, arrowLeft, arrowRight, home, end, enter, space].includes(e.keyCode)) {
+            if (e.target.matches('[role="tablist"] [role="tab"]')) {
+                updateTabFocus(e);
+            }
+        }
+    });
+
+    document.addEventListener('click', e => {
+        if (e.target.matches('[role="tablist"] [role="tab"]')) {
+            const tabs = e.target.closest('[role="tablist"]').querySelectorAll('[role="tab"]');
+            e.preventDefault();
+            $(e.target).tab('show');
+            tabs.forEach(tab => {
+                tab.tabIndex = -1;
+            });
+            e.target.tabIndex = 0;
+        }
+    });
+};
+
 export const init = () => {
     dropdownFix();
     autoFocus();
 export const init = () => {
     dropdownFix();
     autoFocus();
+    tabElementFix();
 };
 };
index c5b8ea5..f92a379 100644 (file)
@@ -43,9 +43,12 @@ const rememberTabs = () => {
             location.hash = hash;
         }
     });
             location.hash = hash;
         }
     });
-    var hash = window.location.hash;
+    const hash = window.location.hash;
     if (hash) {
     if (hash) {
-       $('.nav-link[href="' + hash + '"]').tab('show');
+        const tab = document.querySelector('.nav-link[href="' + hash + '"]');
+        if (tab) {
+            tab.click();
+        }
     }
 };
 
     }
 };
 
@@ -83,6 +86,9 @@ const pendingPromise = new Pending('theme_boost/loader:init');
 // Add pending promise event listeners to relevant Bootstrap custom events.
 setupBootstrapPendingChecks();
 
 // Add pending promise event listeners to relevant Bootstrap custom events.
 setupBootstrapPendingChecks();
 
+// Setup Aria helpers for Bootstrap features.
+Aria.init();
+
 // Remember the last visited tabs.
 rememberTabs();
 
 // Remember the last visited tabs.
 rememberTabs();
 
@@ -98,9 +104,6 @@ enableTooltips();
 // Disables flipping the dropdowns up and getting hidden behind the navbar.
 $.fn.dropdown.Constructor.Default.flip = false;
 
 // Disables flipping the dropdowns up and getting hidden behind the navbar.
 $.fn.dropdown.Constructor.Default.flip = false;
 
-// Setup Aria helpers for Bootstrap features.
-Aria.init();
-
 pendingPromise.resolve();
 
 export {
 pendingPromise.resolve();
 
 export {
index ec25e9f..162314a 100644 (file)
@@ -39,7 +39,9 @@
 <ul class="nav nav-tabs" role="tablist">
     {{#tabs}}
         <li class="nav-item">
 <ul class="nav nav-tabs" role="tablist">
     {{#tabs}}
         <li class="nav-item">
-            <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab">{{displayname}}</a>
+            <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab"
+                {{#active}}aria-selected="true"{{/active}}
+                {{^active}}aria-selected="false" tabindex="-1"{{/active}}>{{displayname}}</a>
         </li>
     {{/tabs}}
 </ul>
         </li>
     {{/tabs}}
 </ul>