diff --git a/.size-limit.json b/.size-limit.json index e3b6b05..2307021 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -10,7 +10,7 @@ }, { "path": "dist/es2015/sidecar.js", - "limit": "4.8 KB", + "limit": "4.85 KB", "ignore": [ "prop-types", "@babel/runtime", @@ -19,7 +19,7 @@ }, { "path": "dist/es2015/index.js", - "limit": "7.1 KB", + "limit": "7.2 KB", "ignore": [ "prop-types", "@babel/runtime", diff --git a/src/Lock.js b/src/Lock.js index e513b24..46a7d09 100644 --- a/src/Lock.js +++ b/src/Lock.js @@ -173,6 +173,7 @@ const FocusLock = React.forwardRef(function FocusLockUI(props, parentRef) { onDeactivation={onDeactivation} returnFocus={returnFocus} focusOptions={focusOptions} + noFocusGuards={noFocusGuards} /> )} null; let lastPortaledElement = null; let focusWasOutsideWindow = false; +let windowFocused = false; const defaultWhitelist = () => true; @@ -89,15 +90,16 @@ const withinHost = (activeElement, workingArea) => ( workingArea.some(area => checkInHost(activeElement, area, area)) ); +const getNodeFocusables = nodes => getFocusableNodes(nodes, new Map()); const isNotFocusable = node => ( - !getFocusableNodes([node.parentNode], new Map()).some(el => el.node === node) + !getNodeFocusables([node.parentNode]).some(el => el.node === node) ); const activateTrap = () => { let result = false; if (lastActiveTrap) { const { - observed, persistentFocus, autoFocus, shards, crossFrame, focusOptions, + observed, persistentFocus, autoFocus, shards, crossFrame, focusOptions, noFocusGuards, } = lastActiveTrap; const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement); @@ -125,9 +127,24 @@ const activateTrap = () => { ...shards.map(extractRef).filter(Boolean), ]; + const shouldForceRestoreFocus = () => { + // force restoration happens when + // - focus is not inside now + // - focusWasOutside + // - there are go guards + // - the last active element was the first or the last focusable one + if (!focusWasOutside(crossFrame) || !noFocusGuards || !lastActiveFocus || windowFocused) { + return false; + } + const nodes = getNodeFocusables(workingArea); + const lastIndex = nodes.findIndex(({ node }) => node === lastActiveFocus); + + return lastIndex === 0 || lastIndex === nodes.length - 1; + }; + if (!activeElement || focusWhitelisted(activeElement)) { if ( - (persistentFocus || focusWasOutside(crossFrame)) + (persistentFocus || shouldForceRestoreFocus()) || !isFreeFocus() || (!lastActiveFocus && autoFocus) ) { @@ -215,7 +232,11 @@ FocusTrap.propTypes = { children: PropTypes.node.isRequired, }; +const onWindowFocus = () => { + windowFocused = true; +}; const onWindowBlur = () => { + windowFocused = false; focusWasOutsideWindow = 'just'; // using setTimeout to set this variable after React/sidecar reaction deferAction(() => { @@ -226,12 +247,14 @@ const onWindowBlur = () => { const attachHandler = () => { document.addEventListener('focusin', onTrap); document.addEventListener('focusout', onBlur); + window.addEventListener('focus', onWindowFocus); window.addEventListener('blur', onWindowBlur); }; const detachHandler = () => { document.removeEventListener('focusin', onTrap); document.removeEventListener('focusout', onBlur); + window.removeEventListener('focus', onWindowFocus); window.removeEventListener('blur', onWindowBlur); }; diff --git a/stories/Iframe.js b/stories/Iframe.js index 93bf29d..2b6d8c5 100644 --- a/stories/Iframe.js +++ b/stories/Iframe.js @@ -74,9 +74,23 @@ export const IFrame = props => ( {' '} outside +
+ +
+ -