Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"focuslock.min.js","sources":["../../../src/local/aria/focuslock.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tab locking system.\n *\n * This is based on code and examples provided in the ARIA specification.\n * https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html\n *\n * @module     core/local/aria/focuslock\n * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Selectors from './selectors';\n\nconst lockRegionStack = [];\nconst initialFocusElementStack = [];\nconst finalFocusElementStack = [];\n\nlet lastFocus = null;\nlet ignoreFocusChanges = false;\nlet isLocked = false;\n\n/**\n * The lock handler.\n *\n * This is the item that does a majority of the work.\n * The overall logic from this comes from the examles in the WCAG guidelines.\n *\n * The general idea is that if the focus is not held within by an Element within the lock region, then we replace focus\n * on the first element in the lock region. If the first element is the element previously selected prior to the\n * user-initiated focus change, then instead jump to the last element in the lock region.\n *\n * This gives us a solution which supports focus locking of any kind, which loops in both directions, and which\n * prevents the lock from escaping the modal entirely.\n *\n * @method\n * @param {Event} event The event from the focus change\n */\nconst lockHandler = event => {\n    if (ignoreFocusChanges) {\n        // The focus change was made by an internal call to set focus.\n        return;\n    }\n\n    // Find the current lock region.\n    let lockRegion = getCurrentLockRegion();\n    while (lockRegion) {\n        if (document.contains(lockRegion)) {\n            break;\n        }\n\n        // The lock region does not exist.\n        // Perhaps it was removed without being untrapped.\n        untrapFocus();\n        lockRegion = getCurrentLockRegion();\n    }\n    if (!lockRegion) {\n        return;\n    }\n\n    if (lockRegion.contains(event.target)) {\n        lastFocus = event.target;\n    } else {\n        focusFirstDescendant();\n        if (lastFocus == document.activeElement) {\n            focusLastDescendant();\n        }\n        lastFocus = document.activeElement;\n    }\n};\n\n/**\n * Focus the first descendant of the current lock region.\n *\n * @method\n * @returns {Bool} Whether a node was focused\n */\nconst focusFirstDescendant = () => {\n    const lockRegion = getCurrentLockRegion();\n\n    // Grab all elements in the lock region and attempt to focus each element until one is focused.\n    // We can capture most of this in the query selector, but some cases may still reject focus.\n    // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n    // to capture this.\n    // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n    const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable));\n\n    // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.\n    // We must include it in the calculation of descendants to ensure that looping works correctly.\n    focusableElements.unshift(lockRegion);\n    return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Focus the last descendant of the current lock region.\n *\n * @method\n * @returns {Bool} Whether a node was focused\n */\nconst focusLastDescendant = () => {\n    const lockRegion = getCurrentLockRegion();\n\n    // Grab all elements in the lock region, reverse them, and attempt to focus each element until one is focused.\n    // We can capture most of this in the query selector, but some cases may still reject focus.\n    // For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector\n    // to capture this.\n    // The use of Array.some just ensures that we stop as soon as we have a successful focus.\n    const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable)).reverse();\n\n    // The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.\n    // We must include it in the calculation of descendants to ensure that looping works correctly.\n    focusableElements.push(lockRegion);\n    return focusableElements.some(focusableElement => attemptFocus(focusableElement));\n};\n\n/**\n * Check whether the supplied focusTarget is actually focusable.\n * There are cases where a normally focusable element can reject focus.\n *\n * Note: This example is a wholesale copy of the WCAG example.\n *\n * @method\n * @param {HTMLElement} focusTarget\n * @returns {Bool}\n */\nconst isFocusable = focusTarget => {\n    if (focusTarget.tabIndex > 0 || (focusTarget.tabIndex === 0 && focusTarget.getAttribute('tabIndex') !== null)) {\n        return true;\n    }\n\n    if (focusTarget.disabled) {\n        return false;\n    }\n\n    switch (focusTarget.nodeName) {\n        case 'A':\n            return !!focusTarget.href && focusTarget.rel != 'ignore';\n        case 'INPUT':\n            return focusTarget.type != 'hidden' && focusTarget.type != 'file';\n        case 'BUTTON':\n        case 'SELECT':\n        case 'TEXTAREA':\n            return true;\n        default:\n            return false;\n    }\n};\n\n/**\n * Attempt to focus the supplied focusTarget.\n *\n * Note: This example is a heavily inspired by the WCAG example.\n *\n * @method\n * @param {HTMLElement} focusTarget\n * @returns {Bool} Whether focus was successful o rnot.\n */\nconst attemptFocus = focusTarget => {\n    if (!isFocusable(focusTarget)) {\n        return false;\n    }\n\n    // The ignoreFocusChanges variable prevents the focus event handler from interfering and entering a fight with itself.\n    ignoreFocusChanges = true;\n\n    try {\n        focusTarget.focus();\n    } catch (e) {\n        // Ignore failures. We will just try to focus the next element in the list.\n    }\n\n    ignoreFocusChanges = false;\n\n    // If focus was successful the activeElement will be the one we focused.\n    return (document.activeElement === focusTarget);\n};\n\n/**\n * Get the current lock region from the top of the stack.\n *\n * @method\n * @returns {HTMLElement}\n */\nconst getCurrentLockRegion = () => {\n    return lockRegionStack[lockRegionStack.length - 1];\n};\n\n/**\n * Add a new lock region to the stack.\n *\n * @method\n * @param {HTMLElement} newLockRegion\n */\nconst addLockRegionToStack = newLockRegion => {\n    if (newLockRegion === getCurrentLockRegion()) {\n        return;\n    }\n\n    lockRegionStack.push(newLockRegion);\n    const currentLockRegion = getCurrentLockRegion();\n\n    // Append an empty div which can be focused just outside of the item locked.\n    // This locks tab focus to within the tab region, and does not allow it to extend back into the window by\n    // guaranteeing the existence of a tabable item after the lock region which can be focused but which will be caught\n    // by the handler.\n    const element = document.createElement('div');\n    element.tabIndex = 0;\n    element.style.position = 'fixed';\n    element.style.top = 0;\n    element.style.left = 0;\n\n    const initialNode = element.cloneNode();\n    currentLockRegion.parentNode.insertBefore(initialNode, currentLockRegion);\n    initialFocusElementStack.push(initialNode);\n\n    const finalNode = element.cloneNode();\n    currentLockRegion.parentNode.insertBefore(finalNode, currentLockRegion.nextSibling);\n    finalFocusElementStack.push(finalNode);\n};\n\n/**\n * Remove the top lock region from the stack.\n *\n * @method\n */\nconst removeLastLockRegionFromStack = () => {\n    // Take the top element off the stack, and replce the current lockRegion value.\n    lockRegionStack.pop();\n\n    const finalNode = finalFocusElementStack.pop();\n    if (finalNode) {\n        // The final focus element may have been removed if it was part of a parent item.\n        finalNode.remove();\n    }\n\n    const initialNode = initialFocusElementStack.pop();\n    if (initialNode) {\n        // The initial focus element may have been removed if it was part of a parent item.\n        initialNode.remove();\n    }\n};\n\n/**\n * Whether any region is left in the stack.\n *\n * @return {Bool}\n */\nconst hasTrappedRegionsInStack = () => {\n    return !!lockRegionStack.length;\n};\n\n/**\n * Start trapping the focus and lock it to the specified newLockRegion.\n *\n * @method\n * @param {HTMLElement} newLockRegion The container to lock focus to\n */\nexport const trapFocus = newLockRegion => {\n    // Update the lock region stack.\n    // This allows us to support nesting.\n    addLockRegionToStack(newLockRegion);\n\n    if (!isLocked) {\n        // Add the focus handler.\n        document.addEventListener('focus', lockHandler, true);\n    }\n\n    // Attempt to focus on the first item in the lock region.\n    if (!focusFirstDescendant()) {\n        const currentLockRegion = getCurrentLockRegion();\n\n        // No focusable descendants found in the region yet.\n        // This can happen when the region is locked before content is generated.\n        // Focus on the region itself for now.\n        const originalRegionTabIndex = currentLockRegion.tabIndex;\n        currentLockRegion.tabIndex = 0;\n        attemptFocus(currentLockRegion);\n        currentLockRegion.tabIndex = originalRegionTabIndex;\n    }\n\n    // Keep track of the last item focused.\n    lastFocus = document.activeElement;\n\n    isLocked = true;\n};\n\n/**\n * Stop trapping the focus.\n *\n * @method\n */\nexport const untrapFocus = () => {\n    // Remove the top region from the stack.\n    removeLastLockRegionFromStack();\n\n    if (hasTrappedRegionsInStack()) {\n        // The focus manager still has items in the stack.\n        return;\n    }\n\n    document.removeEventListener('focus', lockHandler, true);\n\n    lastFocus = null;\n    ignoreFocusChanges = false;\n    isLocked = false;\n};\n"],"names":["lockRegionStack","initialFocusElementStack","finalFocusElementStack","lastFocus","ignoreFocusChanges","isLocked","lockHandler","event","lockRegion","getCurrentLockRegion","document","contains","untrapFocus","target","focusFirstDescendant","activeElement","focusLastDescendant","focusableElements","Array","from","querySelectorAll","Selectors","elements","focusable","unshift","some","focusableElement","attemptFocus","reverse","push","focusTarget","tabIndex","getAttribute","disabled","nodeName","href","rel","type","isFocusable","focus","e","length","newLockRegion","currentLockRegion","element","createElement","style","position","top","left","initialNode","cloneNode","parentNode","insertBefore","finalNode","nextSibling","addLockRegionToStack","addEventListener","originalRegionTabIndex","pop","remove","removeLastLockRegionFromStack","removeEventListener"],"mappings":";;;;;;;;;;gLA2BMA,gBAAkB,GAClBC,yBAA2B,GAC3BC,uBAAyB,OAE3BC,UAAY,KACZC,oBAAqB,EACrBC,UAAW,QAkBTC,YAAcC,WACZH,8BAMAI,WAAaC,4BACVD,aACCE,SAASC,SAASH,aAMtBI,cACAJ,WAAaC,uBAEZD,aAIDA,WAAWG,SAASJ,MAAMM,QAC1BV,UAAYI,MAAMM,QAElBC,uBACIX,WAAaO,SAASK,eACtBC,sBAEJb,UAAYO,SAASK,iBAUvBD,qBAAuB,WACnBN,WAAaC,uBAObQ,kBAAoBC,MAAMC,KAAKX,WAAWY,iBAAiBC,mBAAUC,SAASC,mBAIpFN,kBAAkBO,QAAQhB,YACnBS,kBAAkBQ,MAAKC,kBAAoBC,aAAaD,qBAS7DV,oBAAsB,WAClBR,WAAaC,uBAObQ,kBAAoBC,MAAMC,KAAKX,WAAWY,iBAAiBC,mBAAUC,SAASC,YAAYK,iBAIhGX,kBAAkBY,KAAKrB,YAChBS,kBAAkBQ,MAAKC,kBAAoBC,aAAaD,qBA6C7DC,aAAeG,kBAhCDA,CAAAA,iBACZA,YAAYC,SAAW,GAA+B,IAAzBD,YAAYC,UAA2D,OAAzCD,YAAYE,aAAa,mBAC7E,KAGPF,YAAYG,gBACL,SAGHH,YAAYI,cACX,YACQJ,YAAYK,MAA2B,UAAnBL,YAAYM,QACxC,cAC0B,UAApBN,YAAYO,MAAwC,QAApBP,YAAYO,SAClD,aACA,aACA,kBACM,iBAEA,IAcVC,CAAYR,oBACN,EAIX1B,oBAAqB,MAGjB0B,YAAYS,QACd,MAAOC,WAITpC,oBAAqB,EAGbM,SAASK,gBAAkBe,aASjCrB,qBAAuB,IAClBT,gBAAgBA,gBAAgByC,OAAS,sBAyE3BC,mBAhEIA,CAAAA,mBACrBA,gBAAkBjC,8BAItBT,gBAAgB6B,KAAKa,qBACfC,kBAAoBlC,uBAMpBmC,QAAUlC,SAASmC,cAAc,OACvCD,QAAQb,SAAW,EACnBa,QAAQE,MAAMC,SAAW,QACzBH,QAAQE,MAAME,IAAM,EACpBJ,QAAQE,MAAMG,KAAO,QAEfC,YAAcN,QAAQO,YAC5BR,kBAAkBS,WAAWC,aAAaH,YAAaP,mBACvD1C,yBAAyB4B,KAAKqB,mBAExBI,UAAYV,QAAQO,YAC1BR,kBAAkBS,WAAWC,aAAaC,UAAWX,kBAAkBY,aACvErD,uBAAuB2B,KAAKyB,YA2C5BE,CAAqBd,eAEhBrC,UAEDK,SAAS+C,iBAAiB,QAASnD,aAAa,IAI/CQ,uBAAwB,OACnB6B,kBAAoBlC,uBAKpBiD,uBAAyBf,kBAAkBZ,SACjDY,kBAAkBZ,SAAW,EAC7BJ,aAAagB,mBACbA,kBAAkBZ,SAAW2B,uBAIjCvD,UAAYO,SAASK,cAErBV,UAAW,SAQFO,YAAc,KAlEW,MAElCZ,gBAAgB2D,YAEVL,UAAYpD,uBAAuByD,MACrCL,WAEAA,UAAUM,eAGRV,YAAcjD,yBAAyB0D,MACzCT,aAEAA,YAAYU,UAuDhBC,GA7CS7D,gBAAgByC,SAoDzB/B,SAASoD,oBAAoB,QAASxD,aAAa,GAEnDH,UAAY,KACZC,oBAAqB,EACrBC,UAAW"}