%PDF- %PDF-
Direktori : /home/vacivi36/ava/lib/amd/src/local/aria/ |
Current File : /home/vacivi36/ava/lib/amd/src/local/aria/focuslock.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/>. /** * Tab locking system. * * This is based on code and examples provided in the ARIA specification. * https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html * * @module core/tablock * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import Selectors from './selectors'; const lockRegionStack = []; const initialFocusElementStack = []; const finalFocusElementStack = []; let lastFocus = null; let ignoreFocusChanges = false; let isLocked = false; /** * The lock handler. * * This is the item that does a majority of the work. * The overall logic from this comes from the examles in the WCAG guidelines. * * The general idea is that if the focus is not held within by an Element within the lock region, then we replace focus * on the first element in the lock region. If the first element is the element previously selected prior to the * user-initiated focus change, then instead jump to the last element in the lock region. * * This gives us a solution which supports focus locking of any kind, which loops in both directions, and which * prevents the lock from escaping the modal entirely. * * @method * @param {Event} event The event from the focus change */ const lockHandler = event => { if (ignoreFocusChanges) { // The focus change was made by an internal call to set focus. return; } // Find the current lock region. let lockRegion = getCurrentLockRegion(); while (lockRegion) { if (document.contains(lockRegion)) { break; } // The lock region does not exist. // Perhaps it was removed without being untrapped. untrapFocus(); lockRegion = getCurrentLockRegion(); } if (!lockRegion) { return; } if (lockRegion.contains(event.target)) { lastFocus = event.target; } else { focusFirstDescendant(); if (lastFocus == document.activeElement) { focusLastDescendant(); } lastFocus = document.activeElement; } }; /** * Focus the first descendant of the current lock region. * * @method * @returns {Bool} Whether a node was focused */ const focusFirstDescendant = () => { const lockRegion = getCurrentLockRegion(); // Grab all elements in the lock region and attempt to focus each element until one is focused. // We can capture most of this in the query selector, but some cases may still reject focus. // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector // to capture this. // The use of Array.some just ensures that we stop as soon as we have a successful focus. const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable)); // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues. // We must include it in the calculation of descendants to ensure that looping works correctly. focusableElements.unshift(lockRegion); return focusableElements.some(focusableElement => attemptFocus(focusableElement)); }; /** * Focus the last descendant of the current lock region. * * @method * @returns {Bool} Whether a node was focused */ const focusLastDescendant = () => { const lockRegion = getCurrentLockRegion(); // Grab all elements in the lock region, reverse them, and attempt to focus each element until one is focused. // We can capture most of this in the query selector, but some cases may still reject focus. // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector // to capture this. // The use of Array.some just ensures that we stop as soon as we have a successful focus. const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable)).reverse(); // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues. // We must include it in the calculation of descendants to ensure that looping works correctly. focusableElements.push(lockRegion); return focusableElements.some(focusableElement => attemptFocus(focusableElement)); }; /** * Check whether the supplied focusTarget is actually focusable. * There are cases where a normally focusable element can reject focus. * * Note: This example is a wholesale copy of the WCAG example. * * @method * @param {HTMLElement} focusTarget * @returns {Bool} */ const isFocusable = focusTarget => { if (focusTarget.tabIndex > 0 || (focusTarget.tabIndex === 0 && focusTarget.getAttribute('tabIndex') !== null)) { return true; } if (focusTarget.disabled) { return false; } switch (focusTarget.nodeName) { case 'A': return !!focusTarget.href && focusTarget.rel != 'ignore'; case 'INPUT': return focusTarget.type != 'hidden' && focusTarget.type != 'file'; case 'BUTTON': case 'SELECT': case 'TEXTAREA': return true; default: return false; } }; /** * Attempt to focus the supplied focusTarget. * * Note: This example is a heavily inspired by the WCAG example. * * @method * @param {HTMLElement} focusTarget * @returns {Bool} Whether focus was successful o rnot. */ const attemptFocus = focusTarget => { if (!isFocusable(focusTarget)) { return false; } // The ignoreFocusChanges variable prevents the focus event handler from interfering and entering a fight with itself. ignoreFocusChanges = true; try { focusTarget.focus(); } catch (e) { // Ignore failures. We will just try to focus the next element in the list. // eslint-disable-line } ignoreFocusChanges = false; // If focus was successful the activeElement will be the one we focused. return (document.activeElement === focusTarget); }; /** * Get the current lock region from the top of the stack. * * @method * @returns {HTMLElement} */ const getCurrentLockRegion = () => { return lockRegionStack[lockRegionStack.length - 1]; }; /** * Add a new lock region to the stack. * * @method * @param {HTMLElement} newLockRegion */ const addLockRegionToStack = newLockRegion => { if (newLockRegion === getCurrentLockRegion()) { return; } lockRegionStack.push(newLockRegion); const currentLockRegion = getCurrentLockRegion(); // Append an empty div which can be focused just outside of the item locked. // This locks tab focus to within the tab region, and does not allow it to extend back into the window by // guaranteeing the existence of a tabable item after the lock region which can be focused but which will be caught // by the handler. const element = document.createElement('div'); element.tabIndex = 0; element.style.position = 'fixed'; element.style.top = 0; element.style.left = 0; const initialNode = element.cloneNode(); currentLockRegion.parentNode.insertBefore(initialNode, currentLockRegion); initialFocusElementStack.push(initialNode); const finalNode = element.cloneNode(); currentLockRegion.parentNode.insertBefore(finalNode, currentLockRegion.nextSibling); finalFocusElementStack.push(finalNode); }; /** * Remove the top lock region from the stack. * * @method */ const removeLastLockRegionFromStack = () => { // Take the top element off the stack, and replce the current lockRegion value. lockRegionStack.pop(); const finalNode = finalFocusElementStack.pop(); if (finalNode) { // The final focus element may have been removed if it was part of a parent item. finalNode.remove(); } const initialNode = initialFocusElementStack.pop(); if (initialNode) { // The initial focus element may have been removed if it was part of a parent item. initialNode.remove(); } }; /** * Whether any region is left in the stack. * * @return {Bool} */ const hasTrappedRegionsInStack = () => { return !!lockRegionStack.length; }; /** * Start trapping the focus and lock it to the specified newLockRegion. * * @method * @param {HTMLElement} newLockRegion The container to lock focus to */ export const trapFocus = newLockRegion => { // Update the lock region stack. // This allows us to support nesting. addLockRegionToStack(newLockRegion); if (!isLocked) { // Add the focus handler. document.addEventListener('focus', lockHandler, true); } // Attempt to focus on the first item in the lock region. if (!focusFirstDescendant()) { const currentLockRegion = getCurrentLockRegion(); // No focusable descendants found in the region yet. // This can happen when the region is locked before content is generated. // Focus on the region itself for now. const originalRegionTabIndex = currentLockRegion.tabIndex; currentLockRegion.tabIndex = 0; attemptFocus(currentLockRegion); currentLockRegion.tabIndex = originalRegionTabIndex; } // Keep track of the last item focused. lastFocus = document.activeElement; isLocked = true; }; /** * Stop trapping the focus. * * @method */ export const untrapFocus = () => { // Remove the top region from the stack. removeLastLockRegionFromStack(); if (hasTrappedRegionsInStack()) { // The focus manager still has items in the stack. return; } document.removeEventListener('focus', lockHandler, true); lastFocus = null; ignoreFocusChanges = false; isLocked = false; };