(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.focusLock = factory()); }(this, function () { 'use strict'; var toArray = function toArray(a) { var ret = Array(a.length); for (var i = 0; i < a.length; ++i) { ret[i] = a[i]; } return ret; }; var arrayFind = function arrayFind(array, search) { return array.filter(function (a) { return a === search; })[0]; }; var asArray = function asArray(a) { return Array.isArray(a) ? a : [a]; }; var tabSort = function tabSort(a, b) { var tabDiff = a.tabIndex - b.tabIndex; var indexDiff = a.index - b.index; if (tabDiff) { if (!a.tabIndex) return 1; if (!b.tabIndex) return -1; } return tabDiff || indexDiff; }; var orderByTabIndex = function orderByTabIndex(nodes, filterNegative, keepGuards) { return toArray(nodes).map(function (node, index) { return { node: node, index: index, tabIndex: keepGuards && node.tabIndex === -1 ? (node.dataset || {}).focusGuard ? 0 : -1 : node.tabIndex }; }).filter(function (data) { return !filterNegative || data.tabIndex >= 0; }).sort(tabSort); }; var tabbables = ['button:enabled:not([readonly])', 'select:enabled:not([readonly])', 'textarea:enabled:not([readonly])', 'input:enabled:not([readonly])', 'a[href]', 'area[href]', 'iframe', 'object', 'embed', '[tabindex]', '[contenteditable]', '[autofocus]']; var FOCUS_GROUP = 'data-focus-lock'; var FOCUS_DISABLED = 'data-focus-lock-disabled'; var FOCUS_ALLOW = 'data-no-focus-lock'; var FOCUS_AUTO = 'data-autofocus-inside'; var queryTabbables = tabbables.join(','); var queryGuardTabbables = queryTabbables + ', [data-focus-guard]'; var getFocusables = function getFocusables(parents, withGuards) { return parents.reduce(function (acc, parent) { return acc.concat( // add all tabbables inside toArray(parent.querySelectorAll(withGuards ? queryGuardTabbables : queryTabbables)), // add if node is tabble itself parent.parentNode ? toArray(parent.parentNode.querySelectorAll(tabbables.join(','))).filter(function (node) { return node === parent; }) : []); }, []); }; var getParentAutofocusables = function getParentAutofocusables(parent) { var parentFocus = parent.querySelectorAll('[' + FOCUS_AUTO + ']'); return toArray(parentFocus).map(function (node) { return getFocusables([node]); }).reduce(function (acc, nodes) { return acc.concat(nodes); }, []); }; var isElementHidden = function isElementHidden(computedStyle) { if (!computedStyle || !computedStyle.getPropertyValue) { return false; } return computedStyle.getPropertyValue('display') === 'none' || computedStyle.getPropertyValue('visibility') === 'hidden'; }; var isVisible = function isVisible(node) { return !node || node === document || !isElementHidden(window.getComputedStyle(node, null)) && isVisible(node.parentNode); }; var notHiddenInput = function notHiddenInput(node) { return !((node.tagName === 'INPUT' || node.tagName === 'BUTTON') && (node.type === 'hidden' || node.disabled)); }; var getParents = function getParents(node) { var parents = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; parents.push(node); if (node.parentNode) { getParents(node.parentNode, parents); } return parents; }; var getCommonParent = function getCommonParent(nodea, nodeb) { var parentsA = getParents(nodea); var parentsB = getParents(nodeb); for (var i = 0; i < parentsA.length; i += 1) { var currentParent = parentsA[i]; if (parentsB.indexOf(currentParent) >= 0) { return currentParent; } } return false; }; var filterFocusable = function filterFocusable(nodes) { return toArray(nodes).filter(function (node) { return isVisible(node); }).filter(function (node) { return notHiddenInput(node); }); }; var getTabbableNodes = function getTabbableNodes(topNodes, withGuards) { return orderByTabIndex(filterFocusable(getFocusables(topNodes, withGuards)), true, withGuards); }; var getAllTabbableNodes = function getAllTabbableNodes(topNodes) { return orderByTabIndex(filterFocusable(getFocusables(topNodes)), false); }; var parentAutofocusables = function parentAutofocusables(topNode) { return filterFocusable(getParentAutofocusables(topNode)); }; var isRadio = function isRadio(node) { return node.tagName === 'INPUT' && node.type === 'radio'; }; var findSelectedRadio = function findSelectedRadio(node, nodes) { return nodes.filter(isRadio).filter(function (el) { return el.name === node.name; }).filter(function (el) { return el.checked; })[0] || node; }; var pickFirstFocus = function pickFirstFocus(nodes) { if (nodes[0] && nodes.length > 1) { if (isRadio(nodes[0]) && nodes[0].name) { return findSelectedRadio(nodes[0], nodes); } } return nodes[0]; }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var filterNested = function filterNested(nodes) { var l = nodes.length; for (var i = 0; i < l; i += 1) { var _loop = function _loop(j) { if (i !== j) { if (nodes[i].contains(nodes[j])) { return { v: filterNested(nodes.filter(function (x) { return x !== nodes[j]; })) }; } } }; for (var j = 0; j < l; j += 1) { var _ret = _loop(j); if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; } } return nodes; }; var getTopParent = function getTopParent(node) { return node.parentNode ? getTopParent(node.parentNode) : node; }; var getAllAffectedNodes = function getAllAffectedNodes(node) { var nodes = asArray(node); return nodes.filter(Boolean).reduce(function (acc, currentNode) { var group = currentNode.getAttribute(FOCUS_GROUP); acc.push.apply(acc, group ? filterNested(toArray(getTopParent(currentNode).querySelectorAll('[' + FOCUS_GROUP + '="' + group + '"]:not([' + FOCUS_DISABLED + '="disabled"])'))) : [currentNode]); return acc; }, []); }; var findAutoFocused = function findAutoFocused(autoFocusables) { return function (node) { return !!node.autofocus || node.dataset && !!node.dataset.autofocus || autoFocusables.indexOf(node) >= 0; }; }; var newFocus = function newFocus(innerNodes, outerNodes, activeElement, lastNode, autoFocused) { var cnt = innerNodes.length; var firstFocus = innerNodes[0]; var lastFocus = innerNodes[cnt - 1]; // focus is inside if (innerNodes.indexOf(activeElement) >= 0) { return undefined; } var activeIndex = outerNodes.indexOf(activeElement); var lastIndex = outerNodes.indexOf(lastNode || activeIndex); var lastNodeInside = innerNodes.indexOf(lastNode); var indexDiff = activeIndex - lastIndex; var firstNodeIndex = outerNodes.indexOf(firstFocus); var lastNodeIndex = outerNodes.indexOf(lastFocus); // new focus if (activeIndex === -1 || lastNodeInside === -1) { return innerNodes.indexOf(autoFocused.length ? pickFirstFocus(autoFocused) : pickFirstFocus(innerNodes)); } // old focus if (!indexDiff && lastNodeInside >= 0) { return lastNodeInside; } // jump out if (indexDiff && Math.abs(indexDiff) > 1) { return lastNodeInside; } // focus above lock if (activeIndex <= firstNodeIndex) { return cnt - 1; } // focus below lock if (activeIndex > lastNodeIndex) { return 0; } // index is inside tab order, but outside Lock if (indexDiff) { if (Math.abs(indexDiff) > 1) { return lastNodeInside; } return (cnt + lastNodeInside + indexDiff) % cnt; } // do nothing return undefined; }; var getTopCommonParent = function getTopCommonParent(baseActiveElement, leftEntry, rightEntries) { var activeElements = asArray(baseActiveElement); var leftEntries = asArray(leftEntry); var activeElement = activeElements[0]; var topCommon = null; leftEntries.filter(Boolean).forEach(function (entry) { topCommon = getCommonParent(topCommon || entry, entry) || topCommon; rightEntries.filter(Boolean).forEach(function (subEntry) { var common = getCommonParent(activeElement, subEntry); if (common) { if (!topCommon || common.contains(topCommon)) { topCommon = common; } else { topCommon = getCommonParent(common, topCommon); } } }); }); return topCommon; }; var allParentAutofocusables = function allParentAutofocusables(entries) { return entries.reduce(function (acc, node) { return acc.concat(parentAutofocusables(node)); }, []); }; var notAGuard = function notAGuard(node) { return !(node.dataset && node.dataset.focusGuard); }; var reorderNodes = function reorderNodes(srcNodes, dstNodes) { return srcNodes.map(function (dnode) { return dstNodes.find(function (_ref) { var node = _ref.node; return dnode === node; }); }).filter(Boolean); }; var getFocusMerge = function getFocusMerge(topNode, lastNode) { var activeElement = document && document.activeElement; var entries = getAllAffectedNodes(topNode).filter(notAGuard); var commonParent = getTopCommonParent(activeElement || topNode, topNode, entries); var innerElements = getTabbableNodes(entries).filter(function (_ref5) { var node = _ref5.node; return notAGuard(node); }); if (!innerElements[0]) { innerElements = getAllTabbableNodes(entries).filter(function (_ref6) { var node = _ref6.node; return notAGuard(node); }); if (!innerElements[0]) { return undefined; } } var outerNodes = getTabbableNodes([commonParent]).map(function (_ref7) { var node = _ref7.node; return node; }); var orderedInnerElements = reorderNodes(outerNodes, innerElements); var innerNodes = orderedInnerElements.map(function (_ref8) { var node = _ref8.node; return node; }); var newId = newFocus(innerNodes, outerNodes, activeElement, lastNode, innerNodes.filter(findAutoFocused(allParentAutofocusables(entries)))); if (newId === undefined) { return newId; } return orderedInnerElements[newId]; }; var focusInFrame = function focusInFrame(frame) { return frame === document.activeElement; }; var focusInsideIframe = function focusInsideIframe(topNode) { return !!arrayFind(toArray(topNode.querySelectorAll('iframe')), focusInFrame); }; var focusInside = function focusInside(topNode) { var activeElement = document && document.activeElement; if (!activeElement || activeElement.dataset && activeElement.dataset.focusGuard) { return false; } return getAllAffectedNodes(topNode).reduce(function (result, node) { return result || node.contains(activeElement) || focusInsideIframe(node); }, false); }; var focusIsHidden = function focusIsHidden() { return document && toArray(document.querySelectorAll('[' + FOCUS_ALLOW + ']')).some(function (node) { return node.contains(document.activeElement); }); }; var focusOn = function focusOn(target) { target.focus(); if (target.contentWindow) { target.contentWindow.focus(); } }; var guardCount = 0; var lockDisabled = false; var setFocus = (function (topNode, lastNode) { var focusable = getFocusMerge(topNode, lastNode); if (lockDisabled) { return; } if (focusable) { if (guardCount > 2) { // eslint-disable-next-line no-console console.error('FocusLock: focus-fighting detected. Only one focus management system could be active. ' + 'See https://github.com/theKashey/focus-lock/#focus-fighting'); lockDisabled = true; setTimeout(function () { lockDisabled = false; }, 1); return; } guardCount++; focusOn(focusable.node); guardCount--; } }); var lastActiveTrap = 0; var lastActiveFocus = null; var focusOnBody = function focusOnBody() { return document && document.activeElement === document.body; }; var isFreeFocus = function isFreeFocus() { return focusOnBody() || focusIsHidden(); }; var activateTrap = function activateTrap() { var result = false; if (lastActiveTrap) { var observed = lastActiveTrap; if (!isFreeFocus()) { if (observed && !focusInside(observed)) { result = setFocus(observed, lastActiveFocus); } lastActiveFocus = document.activeElement; } } return result; }; var reducePropsToState = function reducePropsToState(propsList) { return propsList.filter(function (node) { return node; }).slice(-1)[0]; }; var handleStateChangeOnClient = function handleStateChangeOnClient(trap) { lastActiveTrap = trap; if (trap) { activateTrap(); } }; var instances = []; var emitChange = function emitChange(event) { if (handleStateChangeOnClient(reducePropsToState(instances))) { event && event.preventDefault(); return true; } return false; }; var attachHandler = function attachHandler() { document.addEventListener('focusin', emitChange); }; var detachHandler = function detachHandler() { document.removeEventListener('focusin', emitChange); }; var focusLock = { on: function on(domNode) { if (instances.length === 0) { attachHandler(); } if (instances.indexOf(domNode) < 0) { instances.push(domNode); emitChange(); } }, off: function off(domNode) { instances = instances.filter(function (node) { return node !== domNode; }); emitChange(); if (instances.length === 0) { detachHandler(); } } }; return focusLock; }));