/* eslint-disable no-mixed-operators */ import * as React from 'react'; import PropTypes from 'prop-types'; import withSideEffect from 'react-clientside-effect'; import moveFocusInside, { focusInside, focusIsHidden, getFocusabledIn } from 'focus-lock'; import { deferAction } from './util'; import { mediumFocus, mediumBlur, mediumEffect } from './medium'; var focusOnBody = function focusOnBody() { return document && document.activeElement === document.body; }; var isFreeFocus = function isFreeFocus() { return focusOnBody() || focusIsHidden(); }; var lastActiveTrap = null; var lastActiveFocus = null; var lastPortaledElement = null; var focusWasOutsideWindow = false; var defaultWhitelist = function defaultWhitelist() { return true; }; var focusWhitelisted = function focusWhitelisted(activeElement) { return (lastActiveTrap.whiteList || defaultWhitelist)(activeElement); }; var recordPortal = function recordPortal(observerNode, portaledElement) { lastPortaledElement = { observerNode: observerNode, portaledElement: portaledElement }; }; var focusIsPortaledPair = function focusIsPortaledPair(element) { return lastPortaledElement && lastPortaledElement.portaledElement === element; }; function autoGuard(startIndex, end, step, allNodes) { var lastGuard = null; var i = startIndex; do { var item = allNodes[i]; if (item.guard) { if (item.node.dataset.focusAutoGuard) { lastGuard = item; } } else if (item.lockItem) { if (i !== startIndex) { // we will tab to the next element return; } lastGuard = null; } else { break; } } while ((i += step) !== end); if (lastGuard) { lastGuard.node.tabIndex = 0; } } var extractRef = function extractRef(ref) { return ref && 'current' in ref ? ref.current : ref; }; var focusWasOutside = function focusWasOutside(crossFrameOption) { if (crossFrameOption) { // with cross frame return true for any value return Boolean(focusWasOutsideWindow); } // in other case return only of focus went a while aho return focusWasOutsideWindow === 'meanwhile'; }; var checkInHost = function checkInHost(check, el, boundary) { return el && ( // find host equal to active element and check nested active element el.host === check && (!el.activeElement || boundary.contains(el.activeElement)) // dive up || el.parentNode && checkInHost(check, el.parentNode, boundary)); }; var withinHost = function withinHost(activeElement, workingArea) { return workingArea.some(function (area) { return checkInHost(activeElement, area, area); }); }; var activateTrap = function activateTrap() { var result = false; if (lastActiveTrap) { var _lastActiveTrap = lastActiveTrap, observed = _lastActiveTrap.observed, persistentFocus = _lastActiveTrap.persistentFocus, autoFocus = _lastActiveTrap.autoFocus, shards = _lastActiveTrap.shards, crossFrame = _lastActiveTrap.crossFrame, focusOptions = _lastActiveTrap.focusOptions; var workingNode = observed || lastPortaledElement && lastPortaledElement.portaledElement; var activeElement = document && document.activeElement; if (workingNode) { var workingArea = [workingNode].concat(shards.map(extractRef).filter(Boolean)); if (!activeElement || focusWhitelisted(activeElement)) { if (persistentFocus || focusWasOutside(crossFrame) || !isFreeFocus() || !lastActiveFocus && autoFocus) { if (workingNode && !( // active element is "inside" working area focusInside(workingArea) || // check for shadow-dom contained elements activeElement && withinHost(activeElement, workingArea) || focusIsPortaledPair(activeElement, workingNode))) { if (document && !lastActiveFocus && activeElement && !autoFocus) { // Check if blur() exists, which is missing on certain elements on IE if (activeElement.blur) { activeElement.blur(); } document.body.focus(); } else { result = moveFocusInside(workingArea, lastActiveFocus, { focusOptions: focusOptions }); lastPortaledElement = {}; } } focusWasOutsideWindow = false; lastActiveFocus = document && document.activeElement; } } if (document) { var newActiveElement = document && document.activeElement; var allNodes = getFocusabledIn(workingArea); var focusedIndex = allNodes.map(function (_ref) { var node = _ref.node; return node; }).indexOf(newActiveElement); if (focusedIndex > -1) { // remove old focus allNodes.filter(function (_ref2) { var guard = _ref2.guard, node = _ref2.node; return guard && node.dataset.focusAutoGuard; }).forEach(function (_ref3) { var node = _ref3.node; return node.removeAttribute('tabIndex'); }); autoGuard(focusedIndex, allNodes.length, +1, allNodes); autoGuard(focusedIndex, -1, -1, allNodes); } } } } return result; }; var onTrap = function onTrap(event) { if (activateTrap() && event) { // prevent scroll jump event.stopPropagation(); event.preventDefault(); } }; var onBlur = function onBlur() { return deferAction(activateTrap); }; var onFocus = function onFocus(event) { // detect portal var source = event.target; var currentNode = event.currentTarget; if (!currentNode.contains(source)) { recordPortal(currentNode, source); } }; var FocusWatcher = function FocusWatcher() { return null; }; var FocusTrap = function FocusTrap(_ref4) { var children = _ref4.children; return /*#__PURE__*/React.createElement("div", { onBlur: onBlur, onFocus: onFocus }, children); }; FocusTrap.propTypes = process.env.NODE_ENV !== "production" ? { children: PropTypes.node.isRequired } : {}; var onWindowBlur = function onWindowBlur() { focusWasOutsideWindow = 'just'; // using setTimeout to set this variable after React/sidecar reaction deferAction(function () { focusWasOutsideWindow = 'meanwhile'; }); }; var attachHandler = function attachHandler() { document.addEventListener('focusin', onTrap); document.addEventListener('focusout', onBlur); window.addEventListener('blur', onWindowBlur); }; var detachHandler = function detachHandler() { document.removeEventListener('focusin', onTrap); document.removeEventListener('focusout', onBlur); window.removeEventListener('blur', onWindowBlur); }; function reducePropsToState(propsList) { return propsList.filter(function (_ref5) { var disabled = _ref5.disabled; return !disabled; }); } function handleStateChangeOnClient(traps) { var trap = traps.slice(-1)[0]; if (trap && !lastActiveTrap) { attachHandler(); } var lastTrap = lastActiveTrap; var sameTrap = lastTrap && trap && trap.id === lastTrap.id; lastActiveTrap = trap; if (lastTrap && !sameTrap) { lastTrap.onDeactivation(); // return focus only of last trap was removed if (!traps.filter(function (_ref6) { var id = _ref6.id; return id === lastTrap.id; }).length) { // allow defer is no other trap is awaiting restore lastTrap.returnFocus(!trap); } } if (trap) { lastActiveFocus = null; if (!sameTrap || lastTrap.observed !== trap.observed) { trap.onActivation(); } activateTrap(true); deferAction(activateTrap); } else { detachHandler(); lastActiveFocus = null; } } // bind medium mediumFocus.assignSyncMedium(onFocus); mediumBlur.assignMedium(onBlur); mediumEffect.assignMedium(function (cb) { return cb({ moveFocusInside: moveFocusInside, focusInside: focusInside }); }); export default withSideEffect(reducePropsToState, handleStateChangeOnClient)(FocusWatcher);