{"version":3,"file":"aria.min.js","sources":["../src/aria.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 * Enhancements to Bootstrap components for accessibility.\n *\n * @module     theme_boost/aria\n * @copyright  2018 Damyon Wiese <damyon@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {end, arrowUp, arrowDown, arrowLeft, arrowRight, home, enter, space} from 'core/key_codes';\nimport $ from 'jquery';\nimport Pending from 'core/pending';\n\n/**\n * Drop downs from bootstrap don't support keyboard accessibility by default.\n */\nconst dropdownFix = () => {\n    let focusEnd = false;\n    const setFocusEnd = () => {\n        focusEnd = true;\n    };\n    const getFocusEnd = () => {\n        const result = focusEnd;\n        focusEnd = false;\n        return result;\n    };\n\n    // Special handling for \"up\" keyboard control.\n    document.addEventListener('keydown', e => {\n        if (e.target.matches('[data-toggle=\"dropdown\"]')) {\n            const trigger = e.which;\n\n            // Up key opens the menu at the end.\n            if (trigger == arrowUp) {\n                // Focus the end of the menu, not the beginning.\n                setFocusEnd();\n            }\n\n            // Space key or Enter key opens the menu.\n            if (trigger == space || trigger == enter) {\n                // Cancel random scroll.\n                e.preventDefault();\n                // Open the menu instead.\n                e.target.click();\n            }\n        }\n    });\n\n    // Special handling for navigation keys when menu is open.\n    const shiftFocus = element => {\n        const delayedFocus = pendingPromise => {\n            element.focus();\n            pendingPromise.resolve();\n        };\n        setTimeout(delayedFocus, 50, new Pending('core/aria:delayed-focus'));\n    };\n\n    $('.dropdown').on('shown.bs.dropdown', e => {\n        // We need to focus on the first menuitem.\n        const menu = e.target.querySelector('[role=\"menu\"]');\n        let menuItems = false;\n        let foundMenuItem = false;\n\n        if (menu) {\n            menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n        }\n        if (menuItems && menuItems.length > 0) {\n            if (getFocusEnd()) {\n                foundMenuItem = menuItems[menuItems.length - 1];\n            } else {\n                // The first menu entry, pretty reasonable.\n                foundMenuItem = menuItems[0];\n            }\n        }\n        if (foundMenuItem) {\n            shiftFocus(foundMenuItem);\n        }\n    });\n    // Search for menu items by finding the first item that has\n    // text starting with the typed character (case insensitive).\n    document.addEventListener('keypress', e => {\n        if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n            const trigger = String.fromCharCode(e.which).toLowerCase();\n            const menu = e.target.closest('[role=\"menu\"]');\n\n            if (!menu) {\n                return;\n            }\n            const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n            if (!menuItems) {\n                return;\n            }\n\n            for (let i = 0; i < menuItems.length; i++) {\n                const item = menuItems[i];\n                const itemText = item.text.trim().toLowerCase();\n                if (itemText.indexOf(trigger) == 0) {\n                    shiftFocus(item);\n                    break;\n                }\n            }\n        }\n    });\n\n    // Keyboard navigation for arrow keys, home and end keys.\n    document.addEventListener('keydown', e => {\n        if (e.target.matches('.dropdown [role=\"menu\"] [role=\"menuitem\"]')) {\n            const trigger = e.which;\n            let next = false;\n            const menu = e.target.closest('[role=\"menu\"]');\n\n            if (!menu) {\n                return;\n            }\n            const menuItems = menu.querySelectorAll('[role=\"menuitem\"]');\n            if (!menuItems) {\n                return;\n            }\n            // Down key.\n            if (trigger == arrowDown) {\n                for (let i = 0; i < menuItems.length - 1; i++) {\n                    if (menuItems[i] == e.target) {\n                        next = menuItems[i + 1];\n                        break;\n                    }\n                }\n                if (!next) {\n                    // Wrap to first item.\n                    next = menuItems[0];\n                }\n\n            } else if (trigger == arrowUp) {\n                // Up key.\n                for (let i = 1; i < menuItems.length; i++) {\n                    if (menuItems[i] == e.target) {\n                        next = menuItems[i - 1];\n                        break;\n                    }\n                }\n                if (!next) {\n                    // Wrap to last item.\n                    next = menuItems[menuItems.length - 1];\n                }\n\n            } else if (trigger == home) {\n                // Home key.\n                next = menuItems[0];\n\n            } else if (trigger == end) {\n                // End key.\n                next = menuItems[menuItems.length - 1];\n            }\n            // Variable next is set if we do want to act on the keypress.\n            if (next) {\n                e.preventDefault();\n                shiftFocus(next);\n            }\n            return;\n        }\n    });\n\n    $('.dropdown').on('hidden.bs.dropdown', e => {\n        // We need to focus on the menu trigger.\n        const trigger = e.target.querySelector('[data-toggle=\"dropdown\"]');\n        if (trigger) {\n            shiftFocus(trigger);\n        }\n    });\n};\n\n/**\n * After page load, focus on any element with special autofocus attribute.\n */\nconst autoFocus = () => {\n    window.addEventListener(\"load\", () => {\n        const alerts = document.querySelectorAll('[data-aria-autofocus=\"true\"][role=\"alert\"]');\n        Array.prototype.forEach.call(alerts, autofocusElement => {\n            // According to the specification an role=\"alert\" region is only read out on change to the content\n            // of that region.\n            autofocusElement.innerHTML += ' ';\n            autofocusElement.removeAttribute('data-aria-autofocus');\n        });\n    });\n};\n\n/**\n * Changes the focus to the correct tab based on the key that is pressed.\n * @param {KeyboardEvent} e\n */\nconst updateTabFocus = e => {\n    const tabList = e.target.closest('[role=\"tablist\"]');\n    const vertical = tabList.getAttribute('aria-orientation') == 'vertical';\n    const rtl = window.right_to_left();\n    const arrowNext = vertical ? arrowDown : (rtl ? arrowLeft : arrowRight);\n    const arrowPrevious = vertical ? arrowUp : (rtl ? arrowRight : arrowLeft);\n    const tabs = Array.prototype.filter.call(\n        tabList.querySelectorAll('[role=\"tab\"]'),\n        tab => getComputedStyle(tab).display !== 'none'); // We only work with the visible tabs.\n\n    for (let i = 0; i < tabs.length; i++) {\n        tabs[i].index = i;\n    }\n\n    switch (e.keyCode) {\n        case arrowNext:\n            e.preventDefault();\n            if (e.target.index !== undefined && tabs[e.target.index + 1]) {\n                tabs[e.target.index + 1].focus();\n            } else {\n                tabs[0].focus();\n            }\n            break;\n        case arrowPrevious:\n            e.preventDefault();\n            if (e.target.index !== undefined && tabs[e.target.index - 1]) {\n                tabs[e.target.index - 1].focus();\n            } else {\n                tabs[tabs.length - 1].focus();\n            }\n            break;\n        case home:\n            e.preventDefault();\n            tabs[0].focus();\n            break;\n        case end:\n            e.preventDefault();\n            tabs[tabs.length - 1].focus();\n            break;\n        case enter:\n        case space:\n            e.preventDefault();\n            $(e.target).tab('show');\n            tabs.forEach(tab => {\n                tab.tabIndex = -1;\n            });\n            e.target.tabIndex = 0;\n    }\n};\n\n/**\n * Fix accessibility issues regarding tab elements focus and their tab order in Bootstrap navs.\n */\nconst tabElementFix = () => {\n    document.addEventListener('keydown', e => {\n        if ([arrowUp, arrowDown, arrowLeft, arrowRight, home, end, enter, space].includes(e.keyCode)) {\n            if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n                updateTabFocus(e);\n            }\n        }\n    });\n\n    document.addEventListener('click', e => {\n        if (e.target.matches('[role=\"tablist\"] [role=\"tab\"]')) {\n            const tabs = e.target.closest('[role=\"tablist\"]').querySelectorAll('[role=\"tab\"]');\n            e.preventDefault();\n            $(e.target).tab('show');\n            tabs.forEach(tab => {\n                tab.tabIndex = -1;\n            });\n            e.target.tabIndex = 0;\n        }\n    });\n};\n\nexport const init = () => {\n    dropdownFix();\n    autoFocus();\n    tabElementFix();\n};\n"],"names":["dropdownFix","focusEnd","document","addEventListener","e","target","matches","trigger","which","arrowUp","space","enter","preventDefault","click","shiftFocus","element","setTimeout","pendingPromise","focus","resolve","Pending","on","result","menu","querySelector","menuItems","foundMenuItem","querySelectorAll","length","String","fromCharCode","toLowerCase","closest","i","item","text","trim","indexOf","next","arrowDown","home","end","tabElementFix","arrowLeft","arrowRight","includes","keyCode","tabList","vertical","getAttribute","rtl","window","right_to_left","arrowNext","arrowPrevious","tabs","Array","prototype","filter","call","tab","getComputedStyle","display","index","undefined","forEach","tabIndex","updateTabFocus","alerts","autofocusElement","innerHTML","removeAttribute"],"mappings":"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"}