| Server IP : 104.21.80.248 / Your IP : 162.159.115.41 Web Server : Apache/2.4.25 (Win32) OpenSSL/1.0.2j PHP/5.6.30 System : Windows NT WIN-ECQAAA40806 6.2 build 9200 (Windows Server 2012 Standard Edition) i586 User : SYSTEM ( 0) PHP Version : 5.6.30 Disable Function : NONE MySQL : ON | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : E:/Inetpub/www/myschool/triamudom/tuprblearn/lib/amd/src/local/aria/ |
Upload File : |
// 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
* @class tablock
* @package core
* @copyright 2019 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const selectors = {
focusable: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
};
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.
*
* @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;
}
const lockRegion = getCurrentLockRegion();
if (!lockRegion.parentNode) {
// The lock region does not exist.
// Perhaps it was removed without being untrapped.
untrapFocus();
}
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.
*
* @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.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.
*
* @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.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.
*
* @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.
*
* @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.
*
* @returns {HTMLElement}
*/
const getCurrentLockRegion = () => {
return lockRegionStack[lockRegionStack.length - 1];
};
/**
* Add a new lock region to the stack.
*
* @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.
*/
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.
*
* @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.
*/
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;
};