From b3c3f4e0ba7946e3b37faf3f1737f9b412de96b9 Mon Sep 17 00:00:00 2001 From: Mathew May Date: Wed, 15 Dec 2021 16:18:09 +0800 Subject: [PATCH 1/4] MDL-70792 aria: Only set focus on items when triggered by keypress --- theme/boost/amd/build/aria.min.js | 2 +- theme/boost/amd/build/aria.min.js.map | 2 +- theme/boost/amd/src/aria.js | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/theme/boost/amd/build/aria.min.js b/theme/boost/amd/build/aria.min.js index f3154ce0364..90ae54135eb 100644 --- a/theme/boost/amd/build/aria.min.js +++ b/theme/boost/amd/build/aria.min.js @@ -1,2 +1,2 @@ -define ("theme_boost/aria",["exports","jquery","core/pending"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var e=function(){var a=!1,d=function(){a=!0},e=function(){var b=a;a=!1;return b};document.addEventListener("keydown",function(a){if(a.target.matches("[data-toggle=\"dropdown\"]")){var b=a.key;if("ArrowUp"==b){d()}if(" "==b||"Enter"==b){a.preventDefault();a.target.click()}}});var f=function(a){setTimeout(function delayedFocus(b){a.focus();b.resolve()},50,new c.default("core/aria:delayed-focus"))};(0,b.default)(".dropdown").on("shown.bs.dropdown",function(a){var b=a.target.querySelector("[role=\"menu\"]"),c=!1,d=!1;if(b){c=b.querySelectorAll("[role=\"menuitem\"]")}if(c&&0.\n\n/**\n * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n const setFocusEnd = () => {\n focusEnd = true;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for \"up\" keyboard control.\n document.addEventListener('keydown', e => {\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n const trigger = e.key;\n\n // Up key opens the menu at the end.\n if (trigger == 'ArrowUp') {\n // Focus the end of the menu, not the beginning.\n setFocusEnd();\n }\n\n // Space key or Enter key opens the menu.\n if (trigger == ' ' || trigger == 'Enter') {\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n }\n });\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = element => {\n const delayedFocus = pendingPromise => {\n element.focus();\n pendingPromise.resolve();\n };\n setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));\n };\n\n $('.dropdown').on('shown.bs.dropdown', e => {\n // We need to focus on the first menuitem.\n const menu = e.target.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n }\n if (menuItems && menuItems.length > 0) {\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n if (foundMenuItem) {\n shiftFocus(foundMenuItem);\n }\n });\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n }\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n if (trigger) {\n shiftFocus(trigger);\n }\n });\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n break;\n case 'Enter':\n case ' ':\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n autoFocus();\n tabElementFix();\n};\n"],"file":"aria.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/aria.js"],"names":["dropdownFix","focusEnd","setFocusEnd","getFocusEnd","result","document","addEventListener","e","target","matches","trigger","key","preventDefault","click","shiftFocus","element","setTimeout","delayedFocus","pendingPromise","focus","resolve","Pending","on","menu","parentElement","querySelector","menuItems","foundMenuItem","querySelectorAll","length","closest","toLowerCase","i","item","itemText","text","trim","indexOf","next","autoFocus","window","alerts","Array","prototype","forEach","call","autofocusElement","innerHTML","removeAttribute","updateTabFocus","tabList","vertical","getAttribute","rtl","right_to_left","arrowNext","arrowPrevious","tabs","filter","tab","getComputedStyle","display","index","tabIndex","tabElementFix","includes","init"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n const setFocusEnd = () => {\n focusEnd = true;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for \"up\" keyboard control.\n document.addEventListener('keydown', e => {\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n const trigger = e.key;\n\n // Up key opens the menu at the end.\n if (trigger == 'ArrowUp') {\n // Focus the end of the menu, not the beginning.\n setFocusEnd();\n }\n\n // Space key or Enter key opens the menu.\n if (trigger == ' ' || trigger == 'Enter') {\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n }\n });\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = element => {\n const delayedFocus = pendingPromise => {\n element.focus();\n pendingPromise.resolve();\n };\n setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));\n };\n\n // We only want to set focus when users access the dropdown via keyboard as per\n // guidelines defined in w3 aria practices 1.1 menu-button.\n $(document).on('keydown', '[data-toggle=\"dropdown\"]', (e) => {\n // We need to focus on the first menuitem.\n const menu = e.target.parentElement.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n }\n if (menuItems && menuItems.length > 0) {\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n if (foundMenuItem) {\n shiftFocus(foundMenuItem);\n }\n });\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n }\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n if (trigger) {\n shiftFocus(trigger);\n }\n });\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n break;\n case 'Enter':\n case ' ':\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n autoFocus();\n tabElementFix();\n};\n"],"file":"aria.min.js"} \ No newline at end of file diff --git a/theme/boost/amd/src/aria.js b/theme/boost/amd/src/aria.js index 1acf8673aef..05cb560ced0 100644 --- a/theme/boost/amd/src/aria.js +++ b/theme/boost/amd/src/aria.js @@ -68,9 +68,11 @@ const dropdownFix = () => { setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus')); }; - $('.dropdown').on('shown.bs.dropdown', e => { + // We only want to set focus when users access the dropdown via keyboard as per + // guidelines defined in w3 aria practices 1.1 menu-button. + $(document).on('keydown', '[data-toggle="dropdown"]', (e) => { // We need to focus on the first menuitem. - const menu = e.target.querySelector('[role="menu"]'); + const menu = e.target.parentElement.querySelector('[role="menu"]'); let menuItems = false; let foundMenuItem = false; From 62a90218e1ba19a5695a7014b5aee2291a2d0131 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Tue, 25 Jan 2022 12:13:13 +0100 Subject: [PATCH 2/4] MDL-70792 aria: Dropdown menu keyboard interaction fixes * Move the focus to the last menu item when the menu is displayed by pressing the Up arrow key. * When the menu is open and Tab/Shift-Tab is pressed, focus on the next/previous focusable element on the DOM instead of focusing back on the menu trigger. * Combine event handling for the dropdown menu trigger. --- theme/boost/amd/build/aria.min.js | 2 +- theme/boost/amd/build/aria.min.js.map | 2 +- theme/boost/amd/src/aria.js | 86 ++++++++++++++++++--------- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/theme/boost/amd/build/aria.min.js b/theme/boost/amd/build/aria.min.js index 90ae54135eb..6c69446b662 100644 --- a/theme/boost/amd/build/aria.min.js +++ b/theme/boost/amd/build/aria.min.js @@ -1,2 +1,2 @@ -define ("theme_boost/aria",["exports","jquery","core/pending"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var e=function(){var a=!1,d=function(){a=!0},e=function(){var b=a;a=!1;return b};document.addEventListener("keydown",function(a){if(a.target.matches("[data-toggle=\"dropdown\"]")){var b=a.key;if("ArrowUp"==b){d()}if(" "==b||"Enter"==b){a.preventDefault();a.target.click()}}});var f=function(a){setTimeout(function delayedFocus(b){a.focus();b.resolve()},50,new c.default("core/aria:delayed-focus"))};(0,b.default)(document).on("keydown","[data-toggle=\"dropdown\"]",function(a){var b=a.target.parentElement.querySelector("[role=\"menu\"]"),c=!1,d=!1;if(b){c=b.querySelectorAll("[role=\"menuitem\"]")}if(c&&0.\n\n/**\n * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n const setFocusEnd = () => {\n focusEnd = true;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for \"up\" keyboard control.\n document.addEventListener('keydown', e => {\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n const trigger = e.key;\n\n // Up key opens the menu at the end.\n if (trigger == 'ArrowUp') {\n // Focus the end of the menu, not the beginning.\n setFocusEnd();\n }\n\n // Space key or Enter key opens the menu.\n if (trigger == ' ' || trigger == 'Enter') {\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n }\n });\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = element => {\n const delayedFocus = pendingPromise => {\n element.focus();\n pendingPromise.resolve();\n };\n setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));\n };\n\n // We only want to set focus when users access the dropdown via keyboard as per\n // guidelines defined in w3 aria practices 1.1 menu-button.\n $(document).on('keydown', '[data-toggle=\"dropdown\"]', (e) => {\n // We need to focus on the first menuitem.\n const menu = e.target.parentElement.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n }\n if (menuItems && menuItems.length > 0) {\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n if (foundMenuItem) {\n shiftFocus(foundMenuItem);\n }\n });\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n }\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n if (trigger) {\n shiftFocus(trigger);\n }\n });\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n break;\n case 'Enter':\n case ' ':\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n autoFocus();\n tabElementFix();\n};\n"],"file":"aria.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/aria.js"],"names":["dropdownFix","focusEnd","focusBackToTrigger","setFocusEnd","end","getFocusEnd","result","shiftFocus","element","setTimeout","delayedFocus","pendingPromise","focus","resolve","Pending","handleMenuButton","e","trigger","key","fixFocus","preventDefault","target","click","menu","parentElement","querySelector","menuItems","foundMenuItem","querySelectorAll","length","document","addEventListener","matches","closest","toLowerCase","i","item","itemText","text","trim","indexOf","next","on","autoFocus","window","alerts","Array","prototype","forEach","call","autofocusElement","innerHTML","removeAttribute","updateTabFocus","tabList","vertical","getAttribute","rtl","right_to_left","arrowNext","arrowPrevious","tabs","filter","tab","getComputedStyle","display","index","tabIndex","tabElementFix","includes","init"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Enhancements to Bootstrap components for accessibility.\n *\n * @module theme_boost/aria\n * @copyright 2018 Damyon Wiese \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n let focusEnd = false;\n let focusBackToTrigger = true;\n const setFocusEnd = (end = true) => {\n focusEnd = end;\n };\n const getFocusEnd = () => {\n const result = focusEnd;\n focusEnd = false;\n return result;\n };\n\n // Special handling for navigation keys when menu is open.\n const shiftFocus = element => {\n const delayedFocus = pendingPromise => {\n element.focus();\n pendingPromise.resolve();\n };\n setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));\n };\n\n // Event handling for the dropdown menu button.\n const handleMenuButton = e => {\n const trigger = e.key;\n let fixFocus = false;\n\n // Space key or Enter key opens the menu.\n if (trigger === ' ' || trigger === 'Enter') {\n fixFocus = true;\n // Cancel random scroll.\n e.preventDefault();\n // Open the menu instead.\n e.target.click();\n }\n\n // Up and Down keys also open the menu.\n if (trigger === 'ArrowUp' || trigger === 'ArrowDown') {\n fixFocus = true;\n }\n\n // Pressing tab on the menu button should focus on the next element in the DOM and not back to the menu trigger.\n if (trigger === 'Tab') {\n focusBackToTrigger = false;\n }\n\n if (!fixFocus) {\n // No need to fix the focus. Return early.\n return;\n }\n\n // Fix the focus on the menu items when the menu is opened.\n const menu = e.target.parentElement.querySelector('[role=\"menu\"]');\n let menuItems = false;\n let foundMenuItem = false;\n\n if (menu) {\n menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n }\n if (menuItems && menuItems.length > 0) {\n // Up key opens the menu at the end.\n if (trigger === 'ArrowUp') {\n setFocusEnd();\n } else {\n setFocusEnd(false);\n }\n\n if (getFocusEnd()) {\n foundMenuItem = menuItems[menuItems.length - 1];\n } else {\n // The first menu entry, pretty reasonable.\n foundMenuItem = menuItems[0];\n }\n }\n\n if (foundMenuItem) {\n shiftFocus(foundMenuItem);\n }\n };\n\n // Search for menu items by finding the first item that has\n // text starting with the typed character (case insensitive).\n document.addEventListener('keypress', e => {\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const menu = e.target.closest('[role=\"menu\"]');\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n\n const trigger = e.key.toLowerCase();\n\n for (let i = 0; i < menuItems.length; i++) {\n const item = menuItems[i];\n const itemText = item.text.trim().toLowerCase();\n if (itemText.indexOf(trigger) == 0) {\n shiftFocus(item);\n break;\n }\n }\n }\n });\n\n // Keyboard navigation for arrow keys, home and end keys.\n document.addEventListener('keydown', e => {\n\n // We only want to set focus when users access the dropdown via keyboard as per\n // guidelines defined in w3 aria practices 1.1 menu-button.\n if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n handleMenuButton(e);\n }\n\n if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n const trigger = e.key;\n let next = false;\n const menu = e.target.closest('[role=\"menu\"]');\n\n if (!menu) {\n return;\n }\n const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n if (!menuItems) {\n return;\n }\n // Down key.\n if (trigger == 'ArrowDown') {\n for (let i = 0; i < menuItems.length - 1; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i + 1];\n break;\n }\n }\n if (!next) {\n // Wrap to first item.\n next = menuItems[0];\n }\n\n } else if (trigger == 'ArrowUp') {\n // Up key.\n for (let i = 1; i < menuItems.length; i++) {\n if (menuItems[i] == e.target) {\n next = menuItems[i - 1];\n break;\n }\n }\n if (!next) {\n // Wrap to last item.\n next = menuItems[menuItems.length - 1];\n }\n\n } else if (trigger == 'Home') {\n // Home key.\n next = menuItems[0];\n\n } else if (trigger == 'End') {\n // End key.\n next = menuItems[menuItems.length - 1];\n } else if (trigger == 'Tab') {\n // Pressing tab in the menu should focus on the next element in the DOM and not back to the menu trigger.\n focusBackToTrigger = false;\n }\n\n // Variable next is set if we do want to act on the keypress.\n if (next) {\n e.preventDefault();\n shiftFocus(next);\n }\n return;\n }\n });\n\n $('.dropdown').on('hidden.bs.dropdown', e => {\n // We need to focus on the menu trigger.\n const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n if (trigger && focusBackToTrigger) {\n shiftFocus(trigger);\n }\n // Reset flag to focus back to the menu trigger.\n focusBackToTrigger = true;\n });\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n window.addEventListener(\"load\", () => {\n const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n Array.prototype.forEach.call(alerts, autofocusElement => {\n // According to the specification an role=\"alert\" region is only read out on change to the content\n // of that region.\n autofocusElement.innerHTML += ' ';\n autofocusElement.removeAttribute('data-aria-autofocus');\n });\n });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n const tabList = e.target.closest('[role=\"tablist\"]');\n const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n const rtl = window.right_to_left();\n const arrowNext = vertical ? 'ArrowDown' : (rtl ? 'ArrowLeft' : 'ArrowRight');\n const arrowPrevious = vertical ? 'ArrowUp' : (rtl ? 'ArrowRight' : 'ArrowLeft');\n const tabs = Array.prototype.filter.call(\n tabList.querySelectorAll('[role=\"tab\"]'),\n tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.\n\n for (let i = 0; i < tabs.length; i++) {\n tabs[i].index = i;\n }\n\n switch (e.key) {\n case arrowNext:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n tabs[e.target.index + 1].focus();\n } else {\n tabs[0].focus();\n }\n break;\n case arrowPrevious:\n e.preventDefault();\n if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n tabs[e.target.index - 1].focus();\n } else {\n tabs[tabs.length - 1].focus();\n }\n break;\n case 'Home':\n e.preventDefault();\n tabs[0].focus();\n break;\n case 'End':\n e.preventDefault();\n tabs[tabs.length - 1].focus();\n break;\n case 'Enter':\n case ' ':\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n document.addEventListener('keydown', e => {\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Enter', ' '].includes(e.key)) {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n updateTabFocus(e);\n }\n }\n });\n\n document.addEventListener('click', e => {\n if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]');\n e.preventDefault();\n $(e.target).tab('show');\n tabs.forEach(tab => {\n tab.tabIndex = -1;\n });\n e.target.tabIndex = 0;\n }\n });\n};\n\nexport const init = () => {\n dropdownFix();\n autoFocus();\n tabElementFix();\n};\n"],"file":"aria.min.js"} \ No newline at end of file diff --git a/theme/boost/amd/src/aria.js b/theme/boost/amd/src/aria.js index 05cb560ced0..1f8724d8315 100644 --- a/theme/boost/amd/src/aria.js +++ b/theme/boost/amd/src/aria.js @@ -29,8 +29,9 @@ import Pending from 'core/pending'; */ const dropdownFix = () => { let focusEnd = false; - const setFocusEnd = () => { - focusEnd = true; + let focusBackToTrigger = true; + const setFocusEnd = (end = true) => { + focusEnd = end; }; const getFocusEnd = () => { const result = focusEnd; @@ -38,27 +39,6 @@ const dropdownFix = () => { return result; }; - // Special handling for "up" keyboard control. - document.addEventListener('keydown', e => { - if (e.target.matches('[data-toggle="dropdown"]')) { - const trigger = e.key; - - // Up key opens the menu at the end. - if (trigger == 'ArrowUp') { - // Focus the end of the menu, not the beginning. - setFocusEnd(); - } - - // Space key or Enter key opens the menu. - if (trigger == ' ' || trigger == 'Enter') { - // Cancel random scroll. - e.preventDefault(); - // Open the menu instead. - e.target.click(); - } - } - }); - // Special handling for navigation keys when menu is open. const shiftFocus = element => { const delayedFocus = pendingPromise => { @@ -68,10 +48,36 @@ const dropdownFix = () => { setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus')); }; - // We only want to set focus when users access the dropdown via keyboard as per - // guidelines defined in w3 aria practices 1.1 menu-button. - $(document).on('keydown', '[data-toggle="dropdown"]', (e) => { - // We need to focus on the first menuitem. + // Event handling for the dropdown menu button. + const handleMenuButton = e => { + const trigger = e.key; + let fixFocus = false; + + // Space key or Enter key opens the menu. + if (trigger === ' ' || trigger === 'Enter') { + fixFocus = true; + // Cancel random scroll. + e.preventDefault(); + // Open the menu instead. + e.target.click(); + } + + // Up and Down keys also open the menu. + if (trigger === 'ArrowUp' || trigger === 'ArrowDown') { + fixFocus = true; + } + + // Pressing tab on the menu button should focus on the next element in the DOM and not back to the menu trigger. + if (trigger === 'Tab') { + focusBackToTrigger = false; + } + + if (!fixFocus) { + // No need to fix the focus. Return early. + return; + } + + // Fix the focus on the menu items when the menu is opened. const menu = e.target.parentElement.querySelector('[role="menu"]'); let menuItems = false; let foundMenuItem = false; @@ -80,6 +86,13 @@ const dropdownFix = () => { menuItems = menu.querySelectorAll('[role="menuitem"]'); } if (menuItems && menuItems.length > 0) { + // Up key opens the menu at the end. + if (trigger === 'ArrowUp') { + setFocusEnd(); + } else { + setFocusEnd(false); + } + if (getFocusEnd()) { foundMenuItem = menuItems[menuItems.length - 1]; } else { @@ -87,10 +100,12 @@ const dropdownFix = () => { foundMenuItem = menuItems[0]; } } + if (foundMenuItem) { shiftFocus(foundMenuItem); } - }); + }; + // Search for menu items by finding the first item that has // text starting with the typed character (case insensitive). document.addEventListener('keypress', e => { @@ -119,6 +134,13 @@ const dropdownFix = () => { // Keyboard navigation for arrow keys, home and end keys. document.addEventListener('keydown', e => { + + // We only want to set focus when users access the dropdown via keyboard as per + // guidelines defined in w3 aria practices 1.1 menu-button. + if (e.target.matches('[data-toggle="dropdown"]')) { + handleMenuButton(e); + } + if (e.target.matches('.dropdown [role="menu"] [role="menuitem"]')) { const trigger = e.key; let next = false; @@ -164,7 +186,11 @@ const dropdownFix = () => { } else if (trigger == 'End') { // End key. next = menuItems[menuItems.length - 1]; + } else if (trigger == 'Tab') { + // Pressing tab in the menu should focus on the next element in the DOM and not back to the menu trigger. + focusBackToTrigger = false; } + // Variable next is set if we do want to act on the keypress. if (next) { e.preventDefault(); @@ -177,9 +203,11 @@ const dropdownFix = () => { $('.dropdown').on('hidden.bs.dropdown', e => { // We need to focus on the menu trigger. const trigger = e.target.querySelector('[data-toggle="dropdown"]'); - if (trigger) { + if (trigger && focusBackToTrigger) { shiftFocus(trigger); } + // Reset flag to focus back to the menu trigger. + focusBackToTrigger = true; }); }; From ef132f0ab41e9674046a2cc8e2902b257f69452c Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 23 Feb 2022 22:39:25 +0800 Subject: [PATCH 3/4] MDL-70792 output: Menu items should have -1 tab index * As per WAI ARIA Authoring Practices 1.1 guidelines for menus, menu items should have a tab index of -1. Navigation between menu items is done via arrow keys. See https://www.w3.org/TR/wai-aria-practices-1.1/#menu --- lib/outputcomponents.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/outputcomponents.php b/lib/outputcomponents.php index cea427f26cd..a4c91cffbe6 100644 --- a/lib/outputcomponents.php +++ b/lib/outputcomponents.php @@ -4321,6 +4321,7 @@ class action_menu implements renderable, templatable { public function add_primary_action($action) { if ($action instanceof action_link || $action instanceof pix_icon) { $action->attributes['role'] = 'menuitem'; + $action->attributes['tabindex'] = '-1'; if ($action instanceof action_menu_link) { $action->actionmenu = $this; } @@ -4336,6 +4337,7 @@ class action_menu implements renderable, templatable { public function add_secondary_action($action) { if ($action instanceof action_link || $action instanceof pix_icon) { $action->attributes['role'] = 'menuitem'; + $action->attributes['tabindex'] = '-1'; if ($action instanceof action_menu_link) { $action->actionmenu = $this; } @@ -4393,7 +4395,8 @@ class action_menu implements renderable, templatable { 'title' => $title, 'aria-label' => $label, 'id' => 'action-menu-toggle-'.$this->instance, - 'role' => 'menuitem' + 'role' => 'menuitem', + 'tabindex' => '-1', ); $link = html_writer::link('#', $string . $this->menutrigger . $pixicon, $attributes); if ($this->prioritise) { From 1bd98e16413d524411f4dc295e31924e6754e8ff Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 23 Feb 2022 22:49:41 +0800 Subject: [PATCH 4/4] MDL-70792 theme_boost: Remove default outline on focused menu items * Remove default browser outline in :focus-visible pseudo-class for links within .dropdown-item elements. --- theme/boost/scss/moodle/core.scss | 3 +++ theme/boost/style/moodle.css | 2 ++ theme/classic/style/moodle.css | 2 ++ 3 files changed, 7 insertions(+) diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 4915c7fa46b..a8c56c179ea 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2363,6 +2363,9 @@ $footer-link-color: $bg-inverse-link-color !default; display: block; width: 100%; color: $body-color; + &:focus-visible { + outline: 0; + } } &:active, &:hover, diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index c2e97112d76..9e97b730497 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -11560,6 +11560,8 @@ ul { display: block; width: 100%; color: #1d2125; } + .dropdown-item a:focus-visible { + outline: 0; } .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within { outline: 0; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 9e41afdaa6d..5be8318aafe 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -11778,6 +11778,8 @@ ul { display: block; width: 100%; color: #1d2125; } + .dropdown-item a:focus-visible { + outline: 0; } .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within { outline: 0;