%PDF- %PDF-
Direktori : /home/vacivi36/ava/lib/amd/src/local/aria/ |
Current File : /home/vacivi36/ava/lib/amd/src/local/aria/aria-hidden.js |
// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * ARIA helpers related to the aria-hidden attribute. * * @module core/local/aria/aria-hidden. * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import {getList} from 'core/normalise'; import Selectors from './selectors'; // The map of MutationObserver objects for an object. const childObserverMap = new Map(); const siblingObserverMap = new Map(); /** * Determine whether the browser supports the MutationObserver system. * * @method * @returns {Bool} */ const supportsMutationObservers = () => (MutationObserver && typeof MutationObserver === 'function'); /** * Disable element focusability, disabling the tabindex for child elements which are normally focusable. * * @method * @param {HTMLElement} target */ const disableElementFocusability = target => { if (!(target instanceof HTMLElement)) { // This element is not an HTMLElement. // This can happen for Text Nodes. return; } if (target.matches(Selectors.elements.focusable)) { disableAndStoreTabIndex(target); } target.querySelectorAll(Selectors.elements.focusable).forEach(disableAndStoreTabIndex); }; /** * Remove the current tab-index and store it for later restoration. * * @method * @param {HTMLElement} element */ const disableAndStoreTabIndex = element => { if (typeof element.dataset.ariaHiddenTabIndex !== 'undefined') { // This child already has a hidden attribute. // Do not modify it as the original value will be lost. return; } // Store the old tabindex in a data attribute. if (element.getAttribute('tabindex')) { element.dataset.ariaHiddenTabIndex = element.getAttribute('tabindex'); } else { element.dataset.ariaHiddenTabIndex = ''; } element.setAttribute('tabindex', -1); }; /** * Re-enable element focusability, restoring any tabindex. * * @method * @param {HTMLElement} target */ const enableElementFocusability = target => { if (!(target instanceof HTMLElement)) { // This element is not an HTMLElement. // This can happen for Text Nodes. return; } if (target.matches(Selectors.elements.focusableToUnhide)) { restoreTabIndex(target); } target.querySelectorAll(Selectors.elements.focusableToUnhide).forEach(restoreTabIndex); }; /** * Restore the tab-index of the supplied element. * * When disabling focusability the current tab-index is stored in the ariaHiddenTabIndex data attribute. * This is used to restore the tab-index, but only whilst the parent nodes remain unhidden. * * @method * @param {HTMLElement} element */ const restoreTabIndex = element => { if (element.closest(Selectors.aria.hidden)) { // This item still has a hidden parent, or is hidden itself. Do not unhide it. return; } const oldTabIndex = element.dataset.ariaHiddenTabIndex; if (oldTabIndex === '') { element.removeAttribute('tabindex'); } else { element.setAttribute('tabindex', oldTabIndex); } delete element.dataset.ariaHiddenTabIndex; }; /** * Update the supplied DOM Module to be hidden. * * @method * @param {HTMLElement} target * @returns {Array} */ export const hide = target => getList(target).forEach(_hide); const _hide = target => { if (!(target instanceof HTMLElement)) { // This element is not an HTMLElement. // This can happen for Text Nodes. return; } if (target.closest(Selectors.aria.hidden)) { // This Element, or a parent Element, is already hidden. // Stop processing. return; } // Set the aria-hidden attribute to true. target.setAttribute('aria-hidden', true); // Based on advice from https://dequeuniversity.com/rules/axe/3.3/aria-hidden-focus, upon setting the aria-hidden // attribute, all focusable elements underneath that element should be modified such that they are not focusable. disableElementFocusability(target); if (supportsMutationObservers()) { // Add a MutationObserver to check for new children to the tree. const mutationObserver = new MutationObserver(mutationList => { mutationList.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(disableElementFocusability); } else if (mutation.type === 'attributes') { // The tabindex has been updated on a hidden attribute. // Ensure that it is stored, ad set to -1 to prevent breakage. const element = mutation.target; const proposedTabIndex = element.getAttribute('tabindex'); if (proposedTabIndex !== "-1") { element.dataset.ariaHiddenTabIndex = proposedTabIndex; element.setAttribute('tabindex', -1); } } }); }); mutationObserver.observe(target, { // Watch for changes to the entire subtree. subtree: true, // Watch for new nodes. childList: true, // Watch for attribute changes to the tabindex. attributes: true, attributeFilter: ['tabindex'], }); childObserverMap.set(target, mutationObserver); } }; /** * Reverse the effect of the hide action. * * @method * @param {HTMLElement} target * @returns {Array} */ export const unhide = target => getList(target).forEach(_unhide); const _unhide = target => { if (!(target instanceof HTMLElement)) { return; } // Note: The aria-hidden attribute should be removed, and not set to false. // The presence of the attribute is sufficient for some browsers to treat it as being true, regardless of its value. target.removeAttribute('aria-hidden'); // Restore the tabindex across all child nodes of the target. enableElementFocusability(target); // Remove the focusability MutationObserver watching this tree. if (childObserverMap.has(target)) { childObserverMap.get(target).disconnect(); childObserverMap.delete(target); } }; /** * Correctly mark all siblings of the supplied target Element as hidden. * * @method * @param {HTMLElement} target * @returns {Array} */ export const hideSiblings = target => getList(target).forEach(_hideSiblings); const _hideSiblings = target => { if (!(target instanceof HTMLElement)) { return; } if (!target.parentElement) { return; } target.parentElement.childNodes.forEach(node => { if (node === target) { // Skip self; return; } hide(node); }); if (supportsMutationObservers()) { // Add a MutationObserver to check for new children to the tree. const newNodeObserver = new MutationObserver(mutationList => { mutationList.forEach(mutation => { mutation.addedNodes.forEach(node => { if (target.contains(node)) { // Skip self, and children of self. return; } hide(node); }); }); }); newNodeObserver.observe(target.parentElement, {childList: true, subtree: true}); siblingObserverMap.set(target.parentElement, newNodeObserver); } }; /** * Correctly reverse the hide action of all children of the supplied target Element. * * @method * @param {HTMLElement} target * @returns {Array} */ export const unhideSiblings = target => getList(target).forEach(_unhideSiblings); const _unhideSiblings = target => { if (!(target instanceof HTMLElement)) { return; } if (!target.parentElement) { return; } target.parentElement.childNodes.forEach(node => { if (node === target) { // Skip self; return; } unhide(node); }); // Remove the sibling MutationObserver watching this tree. if (siblingObserverMap.has(target.parentElement)) { siblingObserverMap.get(target.parentElement).disconnect(); siblingObserverMap.delete(target.parentElement); } };