From 5a010c604dfbf5f72d1f16f75550c2cb44b4f2f8 Mon Sep 17 00:00:00 2001 From: Jose Date: Mon, 8 Jul 2024 14:32:38 +1000 Subject: [PATCH] MDL-82298 navigation: Add setting data-disableactive on moremenu items This setting will avoid conflicts between checkmark and bfcache. The removed function added as part of the MDL-77732 is no longer required as the behaviour on the menu items (including the secondary navigation) is not altered anymore by the menuItemHelper function (menu_navigation file). --- lib/amd/build/menu_navigation.min.js | 2 +- lib/amd/build/menu_navigation.min.js.map | 2 +- lib/amd/src/menu_navigation.js | 31 ------------------------ lib/templates/moremenu_children.mustache | 4 ++- 4 files changed, 5 insertions(+), 34 deletions(-) diff --git a/lib/amd/build/menu_navigation.min.js b/lib/amd/build/menu_navigation.min.js index d6d9e932b04..4871c55c304 100644 --- a/lib/amd/build/menu_navigation.min.js +++ b/lib/amd/build/menu_navigation.min.js @@ -7,6 +7,6 @@ define("core/menu_navigation",["exports"],(function(_exports){Object.definePrope * @author Mathew May * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const SELECTORS_menuitem='[role="menuitem"]',SELECTORS_tab='[role="tab"]',SELECTORS_dropdowntoggle='[data-toggle="dropdown"]',SELECTORS_primarymenuitemactive='.primary-navigation .dropdown-item[aria-current="true"]';let openDropdownNode=null;const clickErrorHandler=(item,fallback)=>null!==item?item:fallback,menuItemHelper=src=>{let parent;if(!src.dataset.disableactive){if(src.classList.contains("dropdown-item")){parent=src.closest(".dropdown-menu");const dropDownToggle=document.getElementById(parent.getAttribute("aria-labelledby"));dropDownToggle.classList.add("active"),dropDownToggle.setAttribute("tabindex",0)}else{if(!src.matches("".concat(SELECTORS_tab,",").concat(SELECTORS_menuitem))||src.matches(SELECTORS_dropdowntoggle))return;parent=src.parentElement.parentElement.querySelector(".dropdown-menu")}Array.prototype.forEach.call(parent.children,(node=>{const menuItem=node.querySelector(SELECTORS_menuitem);null!==menuItem&&(menuItem.classList.remove("active"),menuItem.removeAttribute("aria-current"))})),"menuitem"===src.getAttribute("role")&&src.setAttribute("aria-current","true")}},keyboardListenerEvents=e=>{const src=e.srcElement,firstNode=e.currentTarget.firstElementChild,lastNode=findUsableLastNode(e.currentTarget);if(src.classList.contains("dropdown-item"))"ArrowRight"!=e.key&&"ArrowLeft"!=e.key||(e.preventDefault(),null!==openDropdownNode&&openDropdownNode.parentElement.click())," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),menuItemHelper(src),src.parentElement.classList.contains("dropdown")||src.click());else{const rtl=window.right_to_left(),arrowNext=rtl?"ArrowLeft":"ArrowRight",arrowPrevious=rtl?"ArrowRight":"ArrowLeft";"menuitem"===src.getAttribute("role")&&(e.key==arrowNext&&(e.preventDefault(),setFocusNext(src,firstNode)),e.key==arrowPrevious&&(e.preventDefault(),setFocusPrev(src,lastNode)),"ArrowUp"!=e.key&&"ArrowDown"!=e.key||(openDropdownNode=src,e.preventDefault()),"Home"==e.key&&(e.preventDefault(),setFocusHomeEnd(firstNode)),"End"==e.key&&(e.preventDefault(),setFocusHomeEnd(lastNode)))," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),src.parentElement.classList.contains("dropdown")||src.click())}},clickListenerEvents=e=>{const src=e.srcElement;menuItemHelper(src)};_exports.default=elementRoot=>{elementRoot.removeEventListener("keydown",keyboardListenerEvents),elementRoot.removeEventListener("click",clickListenerEvents),elementRoot.addEventListener("keydown",keyboardListenerEvents),elementRoot.addEventListener("click",clickListenerEvents)},window.addEventListener("pageshow",(function(){const items=document.querySelectorAll(SELECTORS_primarymenuitemactive);null!==items&&items.length>1&&items.forEach((function(e){const href=e.getAttribute("href"),windowHref=window.location.href||"",windowPath=window.location.pathname||"";href!==windowHref&&href!==windowPath&&href!==windowHref+"/index.php"&&href!==windowPath+"index.php"&&(e.classList.remove("active"),e.removeAttribute("aria-current"))}))}));const setFocusNext=(currentNode,firstNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.nextElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,firstNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusPrev=(currentNode,lastNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.previousElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,lastNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusHomeEnd=node=>{node.querySelector(SELECTORS_menuitem).focus()},findUsableLastNode=elementRoot=>{if(elementRoot.lastElementChild.classList.contains("d-none")){const nodesToUse=Array.prototype.map.call(elementRoot.children,(node=>node)).reverse().filter((node=>{if(!node.classList.contains("d-none"))return node}));return 0!==nodesToUse.length?nodesToUse[0]:elementRoot.firstElementChild}return elementRoot.lastElementChild};return _exports.default})); +const SELECTORS_menuitem='[role="menuitem"]',SELECTORS_tab='[role="tab"]',SELECTORS_dropdowntoggle='[data-toggle="dropdown"]';let openDropdownNode=null;const clickErrorHandler=(item,fallback)=>null!==item?item:fallback,menuItemHelper=src=>{let parent;if(!src.dataset.disableactive){if(src.classList.contains("dropdown-item")){parent=src.closest(".dropdown-menu");const dropDownToggle=document.getElementById(parent.getAttribute("aria-labelledby"));dropDownToggle.classList.add("active"),dropDownToggle.setAttribute("tabindex",0)}else{if(!src.matches("".concat(SELECTORS_tab,",").concat(SELECTORS_menuitem))||src.matches(SELECTORS_dropdowntoggle))return;parent=src.parentElement.parentElement.querySelector(".dropdown-menu")}Array.prototype.forEach.call(parent.children,(node=>{const menuItem=node.querySelector(SELECTORS_menuitem);null!==menuItem&&(menuItem.classList.remove("active"),menuItem.removeAttribute("aria-current"))})),"menuitem"===src.getAttribute("role")&&src.setAttribute("aria-current","true")}},keyboardListenerEvents=e=>{const src=e.srcElement,firstNode=e.currentTarget.firstElementChild,lastNode=findUsableLastNode(e.currentTarget);if(src.classList.contains("dropdown-item"))"ArrowRight"!=e.key&&"ArrowLeft"!=e.key||(e.preventDefault(),null!==openDropdownNode&&openDropdownNode.parentElement.click())," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),menuItemHelper(src),src.parentElement.classList.contains("dropdown")||src.click());else{const rtl=window.right_to_left(),arrowNext=rtl?"ArrowLeft":"ArrowRight",arrowPrevious=rtl?"ArrowRight":"ArrowLeft";"menuitem"===src.getAttribute("role")&&(e.key==arrowNext&&(e.preventDefault(),setFocusNext(src,firstNode)),e.key==arrowPrevious&&(e.preventDefault(),setFocusPrev(src,lastNode)),"ArrowUp"!=e.key&&"ArrowDown"!=e.key||(openDropdownNode=src,e.preventDefault()),"Home"==e.key&&(e.preventDefault(),setFocusHomeEnd(firstNode)),"End"==e.key&&(e.preventDefault(),setFocusHomeEnd(lastNode)))," "!=e.key&&"Enter"!=e.key||(e.preventDefault(),src.parentElement.classList.contains("dropdown")||src.click())}},clickListenerEvents=e=>{const src=e.srcElement;menuItemHelper(src)};_exports.default=elementRoot=>{elementRoot.removeEventListener("keydown",keyboardListenerEvents),elementRoot.removeEventListener("click",clickListenerEvents),elementRoot.addEventListener("keydown",keyboardListenerEvents),elementRoot.addEventListener("click",clickListenerEvents)};const setFocusNext=(currentNode,firstNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.nextElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,firstNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusPrev=(currentNode,lastNode)=>{const listElement=currentNode.parentElement,nextListItem=(el=>{do{el=el.previousElementSibling}while(el&&!el.offsetHeight);return el})(listElement),nodeToSelect=clickErrorHandler(nextListItem,lastNode),itemSelector="tablist"===listElement.parentElement.getAttribute("role")?SELECTORS_tab:SELECTORS_menuitem;nodeToSelect.querySelector(itemSelector).focus()},setFocusHomeEnd=node=>{node.querySelector(SELECTORS_menuitem).focus()},findUsableLastNode=elementRoot=>{if(elementRoot.lastElementChild.classList.contains("d-none")){const nodesToUse=Array.prototype.map.call(elementRoot.children,(node=>node)).reverse().filter((node=>{if(!node.classList.contains("d-none"))return node}));return 0!==nodesToUse.length?nodesToUse[0]:elementRoot.firstElementChild}return elementRoot.lastElementChild};return _exports.default})); //# sourceMappingURL=menu_navigation.min.js.map \ No newline at end of file diff --git a/lib/amd/build/menu_navigation.min.js.map b/lib/amd/build/menu_navigation.min.js.map index 39b501cca42..d6e015c932f 100644 --- a/lib/amd/build/menu_navigation.min.js.map +++ b/lib/amd/build/menu_navigation.min.js.map @@ -1 +1 @@ -{"version":3,"file":"menu_navigation.min.js","sources":["../src/menu_navigation.js"],"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 * Keyboard initialization for a given html node.\n *\n * @module core/menu_navigation\n * @copyright 2021 Moodle\n * @author Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n 'menuitem': '[role=\"menuitem\"]',\n 'tab': '[role=\"tab\"]',\n 'dropdowntoggle': '[data-toggle=\"dropdown\"]',\n 'primarymenuitemactive': '.primary-navigation .dropdown-item[aria-current=\"true\"]',\n};\n\nlet openDropdownNode = null;\n\n/**\n * Small helper function to check if a given node is null or not.\n *\n * @param {HTMLElement|null} item The node that we want to compare.\n * @param {HTMLElement} fallback Either the first node or final node that can be focused on.\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Control classes etc of the selected dropdown item and its' parent \n *\n * @param {HTMLElement} src The node within the dropdown the user selected.\n */\nconst menuItemHelper = src => {\n let parent;\n\n // Do not apply any actions if the selected dropdown item is explicitly instructing to not display an active state.\n if (src.dataset.disableactive) {\n return;\n }\n // Handling for dropdown escapes.\n // A bulk of the handling is already done by aria.js just add polish.\n if (src.classList.contains('dropdown-item')) {\n parent = src.closest('.dropdown-menu');\n const dropDownToggle = document.getElementById(parent.getAttribute('aria-labelledby'));\n dropDownToggle.classList.add('active');\n dropDownToggle.setAttribute('tabindex', 0);\n } else if (src.matches(`${SELECTORS.tab},${SELECTORS.menuitem}`) && !src.matches(SELECTORS.dropdowntoggle)) {\n parent = src.parentElement.parentElement.querySelector('.dropdown-menu');\n } else {\n return;\n }\n // Remove active class from any other dropdown elements.\n Array.prototype.forEach.call(parent.children, node => {\n const menuItem = node.querySelector(SELECTORS.menuitem);\n if (menuItem !== null) {\n menuItem.classList.remove('active');\n // Remove aria selection state.\n menuItem.removeAttribute('aria-current');\n }\n });\n // Set the applicable element's selection state.\n if (src.getAttribute('role') === 'menuitem') {\n src.setAttribute('aria-current', 'true');\n }\n};\n\n/**\n * Check if there are sub items in a dropdown menu. There can be one element active only. That is usually controlled\n * by the server. However, when you click, the newly clicked item gets the active state as well. This is no problem\n * because the user leaves the page and a new page load happens. When the user hits the back button, the old page dom\n * is restored from the cache, with both menu items active. If there is such a case, we need to uncheck the item that\n * was clicked when leaving this page.\n * Make sure that this function is applied in the main menu only. The gradebook may contain drop down menus as well\n * were more than one item can be flagged as active.\n */\nconst dropDownMenuActiveCheck = function() {\n const items = document.querySelectorAll(SELECTORS.primarymenuitemactive);\n // Do the check only, if there is more than one subitem active.\n if (items !== null && items.length > 1) {\n items.forEach(function(e) {\n // Get the link target from the href attribute and compare it with the current url in the browser.\n const href = e.getAttribute('href');\n const windowHref = window.location.href || '';\n const windowPath = window.location.pathname || '';\n if (href !== windowHref && href !== windowPath\n && href !== windowHref + '/index.php' && href !== windowPath + 'index.php') {\n e.classList.remove('active');\n e.removeAttribute('aria-current');\n }\n });\n }\n};\n\n/**\n * Defined keyboard event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst keyboardListenerEvents = e => {\n const src = e.srcElement;\n const firstNode = e.currentTarget.firstElementChild;\n const lastNode = findUsableLastNode(e.currentTarget);\n\n // Handling for dropdown escapes.\n // A bulk of the handling is already done by aria.js just add polish.\n if (src.classList.contains('dropdown-item')) {\n if (e.key == 'ArrowRight' ||\n e.key == 'ArrowLeft') {\n e.preventDefault();\n if (openDropdownNode !== null) {\n openDropdownNode.parentElement.click();\n }\n }\n if (e.key == ' ' ||\n e.key == 'Enter') {\n e.preventDefault();\n\n menuItemHelper(src);\n\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n } else {\n const rtl = window.right_to_left();\n const arrowNext = rtl ? 'ArrowLeft' : 'ArrowRight';\n const arrowPrevious = rtl ? 'ArrowRight' : 'ArrowLeft';\n\n if (src.getAttribute('role') === 'menuitem') {\n // When not rendered within a dropdown menu, handle keyboard navigation if the element is rendered as a menu item.\n if (e.key == arrowNext) {\n e.preventDefault();\n setFocusNext(src, firstNode);\n }\n if (e.key == arrowPrevious) {\n e.preventDefault();\n setFocusPrev(src, lastNode);\n }\n // Let aria.js handle the dropdowns.\n if (e.key == 'ArrowUp' ||\n e.key == 'ArrowDown') {\n openDropdownNode = src;\n e.preventDefault();\n }\n if (e.key == 'Home') {\n e.preventDefault();\n setFocusHomeEnd(firstNode);\n }\n if (e.key == 'End') {\n e.preventDefault();\n setFocusHomeEnd(lastNode);\n }\n }\n\n if (e.key == ' ' ||\n e.key == 'Enter') {\n e.preventDefault();\n // Aria.js handles dropdowns etc.\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n }\n};\n\n/**\n * Defined click event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst clickListenerEvents = e => {\n const src = e.srcElement;\n menuItemHelper(src);\n};\n\n/**\n * The initial entry point that a given module can pass a HTMLElement.\n *\n * @param {HTMLElement} elementRoot The menu to add handlers upon.\n */\nexport default elementRoot => {\n // Remove any and all instances of old listeners on the passed element.\n elementRoot.removeEventListener('keydown', keyboardListenerEvents);\n elementRoot.removeEventListener('click', clickListenerEvents);\n // (Re)apply our event listeners to the passed element.\n elementRoot.addEventListener('keydown', keyboardListenerEvents);\n elementRoot.addEventListener('click', clickListenerEvents);\n};\n\n// We need this triggered only when the user hits the back button.\nwindow.addEventListener('pageshow', dropDownMenuActiveCheck);\n\n/**\n * Handle the focusing to the next element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} firstNode The backup node to focus as a last resort.\n */\nconst setFocusNext = (currentNode, firstNode) => {\n const listElement = currentNode.parentElement;\n const nextListItem = ((el) => {\n do {\n el = el.nextElementSibling;\n } while (el && !el.offsetHeight); // We only work with the visible tabs.\n return el;\n })(listElement);\n const nodeToSelect = clickErrorHandler(nextListItem, firstNode);\n const parent = listElement.parentElement;\n const isTabList = parent.getAttribute('role') === 'tablist';\n const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n const menuItem = nodeToSelect.querySelector(itemSelector);\n menuItem.focus();\n};\n\n/**\n * Handle the focusing to the previous element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} lastNode The backup node to focus as a last resort.\n */\nconst setFocusPrev = (currentNode, lastNode) => {\n const listElement = currentNode.parentElement;\n const nextListItem = ((el) => {\n do {\n el = el.previousElementSibling;\n } while (el && !el.offsetHeight); // We only work with the visible tabs.\n return el;\n })(listElement);\n const nodeToSelect = clickErrorHandler(nextListItem, lastNode);\n const parent = listElement.parentElement;\n const isTabList = parent.getAttribute('role') === 'tablist';\n const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n const menuItem = nodeToSelect.querySelector(itemSelector);\n menuItem.focus();\n};\n\n/**\n * Focus on either the start or end of a nav list.\n *\n * @param {HTMLElement} node The element to focus on.\n */\nconst setFocusHomeEnd = node => {\n node.querySelector(SELECTORS.menuitem).focus();\n};\n\n/**\n * We need to look within the menu to find a last node we can add focus to.\n *\n * @param {HTMLElement} elementRoot Menu to find a final child node within.\n * @return {HTMLElement}\n */\nconst findUsableLastNode = elementRoot => {\n const lastNode = elementRoot.lastElementChild;\n\n // An example is the more menu existing but hidden on the page for the time being.\n if (!lastNode.classList.contains('d-none')) {\n return elementRoot.lastElementChild;\n } else {\n // Cast the HTMLCollection & reverse it.\n const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {\n return node;\n }).reverse();\n\n // Get rid of any nodes we can not set focus on.\n const nodesToUse = extractedNodes.filter((node => {\n if (!node.classList.contains('d-none')) {\n return node;\n }\n }));\n\n // If we find no elements we can set focus on, fall back to the absolute first element.\n if (nodesToUse.length !== 0) {\n return nodesToUse[0];\n } else {\n return elementRoot.firstElementChild;\n }\n }\n};\n"],"names":["SELECTORS","openDropdownNode","clickErrorHandler","item","fallback","menuItemHelper","src","parent","dataset","disableactive","classList","contains","closest","dropDownToggle","document","getElementById","getAttribute","add","setAttribute","matches","parentElement","querySelector","Array","prototype","forEach","call","children","node","menuItem","remove","removeAttribute","keyboardListenerEvents","e","srcElement","firstNode","currentTarget","firstElementChild","lastNode","findUsableLastNode","key","preventDefault","click","rtl","window","right_to_left","arrowNext","arrowPrevious","setFocusNext","setFocusPrev","setFocusHomeEnd","clickListenerEvents","elementRoot","removeEventListener","addEventListener","items","querySelectorAll","length","href","windowHref","location","windowPath","pathname","currentNode","listElement","nextListItem","el","nextElementSibling","offsetHeight","nodeToSelect","itemSelector","focus","previousElementSibling","lastElementChild","nodesToUse","map","reverse","filter"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"menu_navigation.min.js","sources":["../src/menu_navigation.js"],"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 * Keyboard initialization for a given html node.\n *\n * @module core/menu_navigation\n * @copyright 2021 Moodle\n * @author Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n 'menuitem': '[role=\"menuitem\"]',\n 'tab': '[role=\"tab\"]',\n 'dropdowntoggle': '[data-toggle=\"dropdown\"]',\n};\n\nlet openDropdownNode = null;\n\n/**\n * Small helper function to check if a given node is null or not.\n *\n * @param {HTMLElement|null} item The node that we want to compare.\n * @param {HTMLElement} fallback Either the first node or final node that can be focused on.\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Control classes etc of the selected dropdown item and its' parent \n *\n * @param {HTMLElement} src The node within the dropdown the user selected.\n */\nconst menuItemHelper = src => {\n let parent;\n\n // Do not apply any actions if the selected dropdown item is explicitly instructing to not display an active state.\n if (src.dataset.disableactive) {\n return;\n }\n // Handling for dropdown escapes.\n // A bulk of the handling is already done by aria.js just add polish.\n if (src.classList.contains('dropdown-item')) {\n parent = src.closest('.dropdown-menu');\n const dropDownToggle = document.getElementById(parent.getAttribute('aria-labelledby'));\n dropDownToggle.classList.add('active');\n dropDownToggle.setAttribute('tabindex', 0);\n } else if (src.matches(`${SELECTORS.tab},${SELECTORS.menuitem}`) && !src.matches(SELECTORS.dropdowntoggle)) {\n parent = src.parentElement.parentElement.querySelector('.dropdown-menu');\n } else {\n return;\n }\n // Remove active class from any other dropdown elements.\n Array.prototype.forEach.call(parent.children, node => {\n const menuItem = node.querySelector(SELECTORS.menuitem);\n if (menuItem !== null) {\n menuItem.classList.remove('active');\n // Remove aria selection state.\n menuItem.removeAttribute('aria-current');\n }\n });\n // Set the applicable element's selection state.\n if (src.getAttribute('role') === 'menuitem') {\n src.setAttribute('aria-current', 'true');\n }\n};\n\n/**\n * Defined keyboard event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst keyboardListenerEvents = e => {\n const src = e.srcElement;\n const firstNode = e.currentTarget.firstElementChild;\n const lastNode = findUsableLastNode(e.currentTarget);\n\n // Handling for dropdown escapes.\n // A bulk of the handling is already done by aria.js just add polish.\n if (src.classList.contains('dropdown-item')) {\n if (e.key == 'ArrowRight' ||\n e.key == 'ArrowLeft') {\n e.preventDefault();\n if (openDropdownNode !== null) {\n openDropdownNode.parentElement.click();\n }\n }\n if (e.key == ' ' ||\n e.key == 'Enter') {\n e.preventDefault();\n\n menuItemHelper(src);\n\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n } else {\n const rtl = window.right_to_left();\n const arrowNext = rtl ? 'ArrowLeft' : 'ArrowRight';\n const arrowPrevious = rtl ? 'ArrowRight' : 'ArrowLeft';\n\n if (src.getAttribute('role') === 'menuitem') {\n // When not rendered within a dropdown menu, handle keyboard navigation if the element is rendered as a menu item.\n if (e.key == arrowNext) {\n e.preventDefault();\n setFocusNext(src, firstNode);\n }\n if (e.key == arrowPrevious) {\n e.preventDefault();\n setFocusPrev(src, lastNode);\n }\n // Let aria.js handle the dropdowns.\n if (e.key == 'ArrowUp' ||\n e.key == 'ArrowDown') {\n openDropdownNode = src;\n e.preventDefault();\n }\n if (e.key == 'Home') {\n e.preventDefault();\n setFocusHomeEnd(firstNode);\n }\n if (e.key == 'End') {\n e.preventDefault();\n setFocusHomeEnd(lastNode);\n }\n }\n\n if (e.key == ' ' ||\n e.key == 'Enter') {\n e.preventDefault();\n // Aria.js handles dropdowns etc.\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n }\n};\n\n/**\n * Defined click event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst clickListenerEvents = e => {\n const src = e.srcElement;\n menuItemHelper(src);\n};\n\n/**\n * The initial entry point that a given module can pass a HTMLElement.\n *\n * @param {HTMLElement} elementRoot The menu to add handlers upon.\n */\nexport default elementRoot => {\n // Remove any and all instances of old listeners on the passed element.\n elementRoot.removeEventListener('keydown', keyboardListenerEvents);\n elementRoot.removeEventListener('click', clickListenerEvents);\n // (Re)apply our event listeners to the passed element.\n elementRoot.addEventListener('keydown', keyboardListenerEvents);\n elementRoot.addEventListener('click', clickListenerEvents);\n};\n\n/**\n * Handle the focusing to the next element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} firstNode The backup node to focus as a last resort.\n */\nconst setFocusNext = (currentNode, firstNode) => {\n const listElement = currentNode.parentElement;\n const nextListItem = ((el) => {\n do {\n el = el.nextElementSibling;\n } while (el && !el.offsetHeight); // We only work with the visible tabs.\n return el;\n })(listElement);\n const nodeToSelect = clickErrorHandler(nextListItem, firstNode);\n const parent = listElement.parentElement;\n const isTabList = parent.getAttribute('role') === 'tablist';\n const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n const menuItem = nodeToSelect.querySelector(itemSelector);\n menuItem.focus();\n};\n\n/**\n * Handle the focusing to the previous element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} lastNode The backup node to focus as a last resort.\n */\nconst setFocusPrev = (currentNode, lastNode) => {\n const listElement = currentNode.parentElement;\n const nextListItem = ((el) => {\n do {\n el = el.previousElementSibling;\n } while (el && !el.offsetHeight); // We only work with the visible tabs.\n return el;\n })(listElement);\n const nodeToSelect = clickErrorHandler(nextListItem, lastNode);\n const parent = listElement.parentElement;\n const isTabList = parent.getAttribute('role') === 'tablist';\n const itemSelector = isTabList ? SELECTORS.tab : SELECTORS.menuitem;\n const menuItem = nodeToSelect.querySelector(itemSelector);\n menuItem.focus();\n};\n\n/**\n * Focus on either the start or end of a nav list.\n *\n * @param {HTMLElement} node The element to focus on.\n */\nconst setFocusHomeEnd = node => {\n node.querySelector(SELECTORS.menuitem).focus();\n};\n\n/**\n * We need to look within the menu to find a last node we can add focus to.\n *\n * @param {HTMLElement} elementRoot Menu to find a final child node within.\n * @return {HTMLElement}\n */\nconst findUsableLastNode = elementRoot => {\n const lastNode = elementRoot.lastElementChild;\n\n // An example is the more menu existing but hidden on the page for the time being.\n if (!lastNode.classList.contains('d-none')) {\n return elementRoot.lastElementChild;\n } else {\n // Cast the HTMLCollection & reverse it.\n const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {\n return node;\n }).reverse();\n\n // Get rid of any nodes we can not set focus on.\n const nodesToUse = extractedNodes.filter((node => {\n if (!node.classList.contains('d-none')) {\n return node;\n }\n }));\n\n // If we find no elements we can set focus on, fall back to the absolute first element.\n if (nodesToUse.length !== 0) {\n return nodesToUse[0];\n } else {\n return elementRoot.firstElementChild;\n }\n }\n};\n"],"names":["SELECTORS","openDropdownNode","clickErrorHandler","item","fallback","menuItemHelper","src","parent","dataset","disableactive","classList","contains","closest","dropDownToggle","document","getElementById","getAttribute","add","setAttribute","matches","parentElement","querySelector","Array","prototype","forEach","call","children","node","menuItem","remove","removeAttribute","keyboardListenerEvents","e","srcElement","firstNode","currentTarget","firstElementChild","lastNode","findUsableLastNode","key","preventDefault","click","rtl","window","right_to_left","arrowNext","arrowPrevious","setFocusNext","setFocusPrev","setFocusHomeEnd","clickListenerEvents","elementRoot","removeEventListener","addEventListener","currentNode","listElement","nextListItem","el","nextElementSibling","offsetHeight","nodeToSelect","itemSelector","focus","previousElementSibling","lastElementChild","nodesToUse","map","reverse","filter","length"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/menu_navigation.js b/lib/amd/src/menu_navigation.js index eed5b371c22..7b74016e9aa 100644 --- a/lib/amd/src/menu_navigation.js +++ b/lib/amd/src/menu_navigation.js @@ -26,7 +26,6 @@ const SELECTORS = { 'menuitem': '[role="menuitem"]', 'tab': '[role="tab"]', 'dropdowntoggle': '[data-toggle="dropdown"]', - 'primarymenuitemactive': '.primary-navigation .dropdown-item[aria-current="true"]', }; let openDropdownNode = null; @@ -85,33 +84,6 @@ const menuItemHelper = src => { } }; -/** - * Check if there are sub items in a dropdown menu. There can be one element active only. That is usually controlled - * by the server. However, when you click, the newly clicked item gets the active state as well. This is no problem - * because the user leaves the page and a new page load happens. When the user hits the back button, the old page dom - * is restored from the cache, with both menu items active. If there is such a case, we need to uncheck the item that - * was clicked when leaving this page. - * Make sure that this function is applied in the main menu only. The gradebook may contain drop down menus as well - * were more than one item can be flagged as active. - */ -const dropDownMenuActiveCheck = function() { - const items = document.querySelectorAll(SELECTORS.primarymenuitemactive); - // Do the check only, if there is more than one subitem active. - if (items !== null && items.length > 1) { - items.forEach(function(e) { - // Get the link target from the href attribute and compare it with the current url in the browser. - const href = e.getAttribute('href'); - const windowHref = window.location.href || ''; - const windowPath = window.location.pathname || ''; - if (href !== windowHref && href !== windowPath - && href !== windowHref + '/index.php' && href !== windowPath + 'index.php') { - e.classList.remove('active'); - e.removeAttribute('aria-current'); - } - }); - } -}; - /** * Defined keyboard event handling so we can remove listeners on nodes on resize etc. * @@ -208,9 +180,6 @@ export default elementRoot => { elementRoot.addEventListener('click', clickListenerEvents); }; -// We need this triggered only when the user hits the back button. -window.addEventListener('pageshow', dropDownMenuActiveCheck); - /** * Handle the focusing to the next element in the dropdown. * diff --git a/lib/templates/moremenu_children.mustache b/lib/templates/moremenu_children.mustache index 65fddc1bf94..43ff3f3388d 100644 --- a/lib/templates/moremenu_children.mustache +++ b/lib/templates/moremenu_children.mustache @@ -55,7 +55,7 @@ {{/action_link_actions}} {{/is_action_link}} {{^is_action_link}} - {{{text}}} @@ -87,6 +87,7 @@ href="{{tab}}" data-toggle="tab" data-text="{{{text}}}" {{#title}}title="{{.}}"{{/title}} {{#isactive}}aria-selected="true"{{/isactive}} + data-disableactive="true" {{^isactive}}tabindex="-1"{{/isactive}} > {{{text}}} @@ -110,6 +111,7 @@ href="{{{url}}}{{{action}}}" {{#title}}title="{{.}}"{{/title}} {{#isactive}}aria-current="true"{{/isactive}} + data-disableactive="true" {{^isactive}}tabindex="-1"{{/isactive}} > {{{text}}}