diff --git a/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz b/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz
new file mode 100644
index 000000000000..8d01a4a89b59
Binary files /dev/null and b/.yarn/offline-mirror/lodash.findlast-4.6.0.tgz differ
diff --git a/packages/react/.storybook/Container.js b/packages/react/.storybook/Container.js
index af9516cc7119..31ebc439da1e 100644
--- a/packages/react/.storybook/Container.js
+++ b/packages/react/.storybook/Container.js
@@ -37,11 +37,6 @@ function Container({ story }) {
}}>
{story()}
-
);
}
diff --git a/packages/react/package.json b/packages/react/package.json
index 66ca2f4be944..60b6c1e93226 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -42,9 +42,9 @@
"classnames": "2.2.6",
"downshift": "^1.31.14",
"flatpickr": "4.6.1",
- "focus-trap-react": "^6.0.0",
"invariant": "^2.2.3",
"lodash.debounce": "^4.0.8",
+ "lodash.findlast": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
"react-is": "^16.8.6",
diff --git a/packages/react/src/components/ComposedModal/ComposedModal.js b/packages/react/src/components/ComposedModal/ComposedModal.js
index bee7b6083a83..72d646c6428e 100644
--- a/packages/react/src/components/ComposedModal/ComposedModal.js
+++ b/packages/react/src/components/ComposedModal/ComposedModal.js
@@ -13,6 +13,7 @@ import { settings } from 'carbon-components';
import { Close20 } from '@carbon/icons-react';
import toggleClass from '../../tools/toggleClass';
import requiredIfGivenPropExists from '../../prop-types/requiredIfGivenPropExists';
+import wrapFocus from '../../internal/wrapFocus';
const { prefix } = settings;
@@ -27,6 +28,8 @@ export default class ComposedModal extends Component {
outerModal = React.createRef();
innerModal = React.createRef();
button = React.createRef();
+ startSentinel = React.createRef();
+ endSentinel = React.createRef();
static propTypes = {
/**
@@ -78,19 +81,6 @@ export default class ComposedModal extends Component {
};
}
- elementOrParentIsFloatingMenu = target => {
- const {
- selectorsFloatingMenus = [
- `.${prefix}--overflow-menu-options`,
- `.${prefix}--tooltip`,
- '.flatpickr-calendar',
- ],
- } = this.props;
- if (target && typeof target.closest === 'function') {
- return selectorsFloatingMenus.some(selector => target.closest(selector));
- }
- };
-
handleKeyDown = evt => {
// Esc key
if (evt.which === 27) {
@@ -109,22 +99,23 @@ export default class ComposedModal extends Component {
}
};
- focusModal = () => {
- if (this.outerModal.current) {
- this.outerModal.current.focus();
- }
- };
-
- handleBlur = evt => {
- // Keyboard trap
- if (
- this.innerModal.current &&
- this.props.open &&
- evt.relatedTarget &&
- !this.innerModal.current.contains(evt.relatedTarget) &&
- !this.elementOrParentIsFloatingMenu(evt.relatedTarget)
- ) {
- this.focusModal();
+ handleBlur = ({
+ target: oldActiveNode,
+ relatedTarget: currentActiveNode,
+ }) => {
+ const { open, selectorsFloatingMenus } = this.props;
+ if (open && currentActiveNode && oldActiveNode) {
+ const { current: modalNode } = this.innerModal;
+ const { current: startSentinelNode } = this.startSentinel;
+ const { current: endSentinelNode } = this.endSentinel;
+ wrapFocus({
+ modalNode,
+ startSentinelNode,
+ endSentinelNode,
+ currentActiveNode,
+ oldActiveNode,
+ selectorsFloatingMenus,
+ });
}
};
@@ -240,11 +231,26 @@ export default class ComposedModal extends Component {
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
onTransitionEnd={open ? this.handleTransitionEnd : undefined}
- className={modalClass}
- tabIndex={-1}>
-
+ className={modalClass}>
+ {/* Non-translatable: Focus-wrap code makes this `
` not actually read by screen readers */}
+
+ Focus sentinel
+
+
{childrenWithProps}
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */}
+
+ Focus sentinel
+
);
}
diff --git a/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap b/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap
index 3ea2f12e1c4b..0eaefdfd2816 100644
--- a/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap
+++ b/packages/react/src/components/ComposedModal/__snapshots__/ComposedModal-test.js.snap
@@ -14,11 +14,25 @@ exports[` renders 1`] = `
onTransitionEnd={[Function]}
open={true}
role="presentation"
- tabIndex={-1}
>
+
+ Focus sentinel
+
+
+ Focus sentinel
+
`;
diff --git a/packages/react/src/components/Modal/Modal-story.js b/packages/react/src/components/Modal/Modal-story.js
index eb2ffaee4133..ef34780ed8a5 100644
--- a/packages/react/src/components/Modal/Modal-story.js
+++ b/packages/react/src/components/Modal/Modal-story.js
@@ -32,7 +32,6 @@ const props = () => ({
'Enter key to submit (shouldSubmitOnEnter)',
false
),
- focusTrap: boolean('Trap focus (focusTrap)', false),
hasScrollingContent: boolean(
'Modal contains scrollable content (hasScrollingContent)',
false
diff --git a/packages/react/src/components/Modal/Modal-test.js b/packages/react/src/components/Modal/Modal-test.js
index f45be8aea838..4afcb7a8ffad 100644
--- a/packages/react/src/components/Modal/Modal-test.js
+++ b/packages/react/src/components/Modal/Modal-test.js
@@ -15,7 +15,7 @@ import { settings } from 'carbon-components';
const { prefix } = settings;
// The modal is the 0th child inside the wrapper on account of focus-trap-react
-const getModal = wrapper => wrapper.childAt(0);
+const getModal = wrapper => wrapper.find('.bx--modal');
describe('Modal', () => {
describe('Renders as expected', () => {
diff --git a/packages/react/src/components/Modal/Modal.js b/packages/react/src/components/Modal/Modal.js
index db00762727f6..e97aba523ead 100644
--- a/packages/react/src/components/Modal/Modal.js
+++ b/packages/react/src/components/Modal/Modal.js
@@ -10,10 +10,13 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { settings } from 'carbon-components';
import { Close20 } from '@carbon/icons-react';
-import FocusTrap from 'focus-trap-react';
import toggleClass from '../../tools/toggleClass';
import Button from '../Button';
+import deprecate from '../../prop-types/deprecate';
import requiredIfGivenPropExists from '../../prop-types/requiredIfGivenPropExists';
+import wrapFocus, {
+ elementOrParentIsFloatingMenu,
+} from '../../internal/wrapFocus';
import setupGetInstanceId from '../../tools/setupGetInstanceId';
const { prefix } = settings;
@@ -138,10 +141,13 @@ export default class Modal extends Component {
size: PropTypes.oneOf(['xs', 'sm', 'lg']),
/**
- * Specify whether the modal should use 3rd party `focus-trap-react` for the focus-wrap feature.
- * NOTE: by default this is true.
+ * Deprecated; Used for advanced focus-wrapping feature using 3rd party library,
+ * but it's now achieved without a 3rd party library.
*/
- focusTrap: PropTypes.bool,
+ focusTrap: deprecate(
+ PropTypes.bool,
+ `\nThe prop \`focusTrap\` for Modal has been deprecated, as the feature of \`focusTrap\` runs by default.`
+ ),
/**
* Specify whether the modal contains scrolling content
@@ -167,30 +173,18 @@ export default class Modal extends Component {
modalHeading: '',
modalLabel: '',
selectorPrimaryFocus: '[data-modal-primary-focus]',
- focusTrap: true,
hasScrollingContent: false,
};
button = React.createRef();
outerModal = React.createRef();
innerModal = React.createRef();
+ startTrap = React.createRef();
+ endTrap = React.createRef();
modalInstanceId = `modal-${getInstanceId()}`;
modalLabelId = `${prefix}--modal-header__label--${this.modalInstanceId}`;
modalHeadingId = `${prefix}--modal-header__heading--${this.modalInstanceId}`;
- elementOrParentIsFloatingMenu = target => {
- const {
- selectorsFloatingMenus = [
- `.${prefix}--overflow-menu-options`,
- `.${prefix}--tooltip`,
- '.flatpickr-calendar',
- ],
- } = this.props;
- if (target && typeof target.closest === 'function') {
- return selectorsFloatingMenus.some(selector => target.closest(selector));
- }
- };
-
handleKeyDown = evt => {
if (this.props.open) {
if (evt.which === 27) {
@@ -206,28 +200,32 @@ export default class Modal extends Component {
if (
this.innerModal.current &&
!this.innerModal.current.contains(evt.target) &&
- !this.elementOrParentIsFloatingMenu(evt.target)
+ !elementOrParentIsFloatingMenu(
+ evt.target,
+ this.props.selectorsFloatingMenus
+ )
) {
this.props.onRequestClose(evt);
}
};
- focusModal = () => {
- if (this.outerModal.current) {
- this.outerModal.current.focus();
- }
- };
-
- handleBlur = evt => {
- // Keyboard trap
- if (
- this.innerModal.current &&
- this.props.open &&
- evt.relatedTarget &&
- !this.innerModal.current.contains(evt.relatedTarget) &&
- !this.elementOrParentIsFloatingMenu(evt.relatedTarget)
- ) {
- this.focusModal();
+ handleBlur = ({
+ target: oldActiveNode,
+ relatedTarget: currentActiveNode,
+ }) => {
+ const { open, selectorsFloatingMenus } = this.props;
+ if (open && currentActiveNode && oldActiveNode) {
+ const { current: modalNode } = this.innerModal;
+ const { current: startTrapNode } = this.startTrap;
+ const { current: endTrapNode } = this.endTrap;
+ wrapFocus({
+ modalNode,
+ startTrapNode,
+ endTrapNode,
+ currentActiveNode,
+ oldActiveNode,
+ selectorsFloatingMenus,
+ });
}
};
@@ -277,9 +275,7 @@ export default class Modal extends Component {
if (!this.props.open) {
return;
}
- if (!this.props.focusTrap) {
- this.focusButton(this.innerModal.current);
- }
+ this.focusButton(this.innerModal.current);
}
handleTransitionEnd = evt => {
@@ -290,9 +286,7 @@ export default class Modal extends Component {
this.outerModal.current.offsetHeight &&
this.beingOpen
) {
- if (!this.props.focusTrap) {
- this.focusButton(evt.currentTarget);
- }
+ this.focusButton(evt.currentTarget);
this.beingOpen = false;
}
};
@@ -317,7 +311,6 @@ export default class Modal extends Component {
selectorsFloatingMenus, // eslint-disable-line
shouldSubmitOnEnter, // eslint-disable-line
size,
- focusTrap,
hasScrollingContent,
...other
} = this.props;
@@ -379,7 +372,8 @@ export default class Modal extends Component {
role="dialog"
className={containerClasses}
aria-label={ariaLabel}
- aria-modal="true">
+ aria-modal="true"
+ tabIndex="-1">
{passiveModal && modalButton}
{modalLabel && (
@@ -422,7 +416,7 @@ export default class Modal extends Component {
);
- const modal = (
+ return (
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */}
+
+ Focus sentinel
+
{modalBody}
+ {/* Non-translatable: Focus-wrap code makes this `` not actually read by screen readers */}
+
+ Focus sentinel
+
);
-
- return !focusTrap ? (
- modal
- ) : (
- // `` has `active: true` in its `defaultProps`
-
- {modal}
-
- );
}
}
diff --git a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
index 43e31cfd247e..26cbea0e21c7 100644
--- a/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
+++ b/packages/react/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
@@ -43,7 +43,6 @@ exports[`ModalWrapper should render 1`] = `
-
+
+ Focus sentinel
+
-
+
+
+
+
+
+
-
-
-
-
-
-
,
+ }
+ }
+ kind="primary"
+ onClick={[Function]}
+ tabIndex={0}
+ type="button"
>
-
-
-
-
,
}
}
- kind="primary"
onClick={[Function]}
tabIndex={0}
type="button"
>
- ,
- }
- }
- onClick={[Function]}
- tabIndex={0}
- type="button"
- >
- Save
-
-
-
+ Save
+
+
-
+
+ Focus sentinel
+
+
diff --git a/packages/react/src/internal/FloatingMenu.js b/packages/react/src/internal/FloatingMenu.js
index 2e71a9ab5be2..c5d5ab9776af 100644
--- a/packages/react/src/internal/FloatingMenu.js
+++ b/packages/react/src/internal/FloatingMenu.js
@@ -10,6 +10,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import window from 'window-or-global';
+import { settings } from 'carbon-components';
+
+const { prefix } = settings;
/**
* The structure for the position of floating menu.
@@ -333,7 +336,25 @@ class FloatingMenu extends React.Component {
if (typeof document !== 'undefined') {
const { target } = this.props;
return ReactDOM.createPortal(
- this._getChildrenWithProps(),
+ <>
+ {/* Non-translatable: Focus management code makes this `` not actually read by screen readers */}
+
+ Focus sentinel
+
+ {this._getChildrenWithProps()}
+ {/* Non-translatable: Focus management code makes this `` not actually read by screen readers */}
+
+ Focus sentinel
+
+ >,
!target ? document.body : target()
);
}
diff --git a/packages/react/src/internal/__tests__/wrapFocus-test.js b/packages/react/src/internal/__tests__/wrapFocus-test.js
new file mode 100644
index 000000000000..646f0e1297ed
--- /dev/null
+++ b/packages/react/src/internal/__tests__/wrapFocus-test.js
@@ -0,0 +1,150 @@
+import wrapFocus from '../wrapFocus';
+
+/**
+ * Copyright IBM Corp. 2020
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+describe('wrapFocus', () => {
+ let node;
+ let spyInnerModal;
+ let spyButton0;
+ let spyButton2;
+
+ beforeEach(() => {
+ node = document.createElement('div');
+ node.tabIndex = '-1';
+ node.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(node);
+ spyInnerModal = jest.spyOn(node.querySelector('#inner-modal'), 'focus');
+ spyButton0 = jest.spyOn(node.querySelector('#button-0'), 'focus');
+ spyButton2 = jest.spyOn(node.querySelector('#button-2'), 'focus');
+ });
+
+ it('runs forward focus-wrap when following outer node is focused on', () => {
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#outer-following'),
+ oldActiveNode: node.querySelector('#button-2'),
+ });
+ expect(spyButton0).toHaveBeenCalled();
+ });
+
+ it('runs forward focus-wrap when following focus sentinel is focused on', () => {
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#end-sentinel'),
+ oldActiveNode: node.querySelector('#button-2'),
+ });
+ expect(spyButton0).toHaveBeenCalled();
+ });
+
+ it('runs reverse focus-wrap when preceding outer node is focused on', () => {
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#outer-preceding'),
+ oldActiveNode: node.querySelector('#button-0'),
+ });
+ expect(spyButton2).toHaveBeenCalled();
+ });
+
+ it('runs reverse focus-wrap when preceding focus sentinel is focused on', () => {
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#start-sentinel'),
+ oldActiveNode: node.querySelector('#button-0'),
+ });
+ expect(spyButton2).toHaveBeenCalled();
+ });
+
+ it('does not run focus-wrap when a floating menu is focused on', () => {
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('.bx--tooltip'),
+ oldActiveNode: node.querySelector('#button-2'),
+ });
+ expect(spyInnerModal).not.toHaveBeenCalled();
+ expect(spyButton0).not.toHaveBeenCalled();
+ expect(spyButton2).not.toHaveBeenCalled();
+ });
+
+ it('uses inner modal node as a escape hatch for focusing for forward focus-wrap', () => {
+ node.querySelector(
+ '#inner-modal'
+ ).innerHTML = ``;
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#outer-following'),
+ oldActiveNode: node.querySelector('#dummy-old-active-node'),
+ });
+ expect(spyInnerModal).toHaveBeenCalled();
+ });
+
+ it('uses inner modal node as a escape hatch for focusing for reverse focus-wrap', () => {
+ node.querySelector(
+ '#inner-modal'
+ ).innerHTML = ``;
+ wrapFocus({
+ modalNode: node.querySelector('#inner-modal'),
+ startSentinelNode: node.querySelector('#start-sentinel'),
+ endSentinelNode: node.querySelector('#end-sentinel'),
+ currentActiveNode: node.querySelector('#outer-preceding'),
+ oldActiveNode: node.querySelector('#dummy-old-active-node'),
+ });
+ expect(spyInnerModal).toHaveBeenCalled();
+ });
+
+ afterEach(() => {
+ if (spyButton2) {
+ spyButton2.mockRestore();
+ spyButton2 = null;
+ }
+ if (spyButton0) {
+ spyButton0.mockRestore();
+ spyButton0 = null;
+ }
+ if (spyInnerModal) {
+ spyInnerModal.mockRestore();
+ spyInnerModal = null;
+ }
+ if (node) {
+ node.parentNode.removeChild(node);
+ node = null;
+ }
+ });
+});
diff --git a/packages/react/src/internal/keyboard/navigation.js b/packages/react/src/internal/keyboard/navigation.js
index 14d98c44969f..cdd85de640f4 100644
--- a/packages/react/src/internal/keyboard/navigation.js
+++ b/packages/react/src/internal/keyboard/navigation.js
@@ -24,7 +24,7 @@ import { match } from './match';
* getNextIndex(keyCodes.RIGHT, 0, 4)
*/
-const getNextIndex = (key, index, arrayLength) => {
+export const getNextIndex = (key, index, arrayLength) => {
if (match(key, ArrowRight)) {
return (index + 1) % arrayLength;
}
@@ -33,4 +33,30 @@ const getNextIndex = (key, index, arrayLength) => {
}
};
-export { getNextIndex };
+/**
+ * A flag `node.compareDocumentPosition(target)` returns,
+ * that indicates `target` is located earlier than `node` in the document or `target` contains `node`.
+ */
+export const DOCUMENT_POSITION_BROAD_PRECEDING =
+ // Checks `typeof Node` for `react-docgen`
+ typeof Node !== 'undefined' &&
+ Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS;
+
+/**
+ * A flag `node.compareDocumentPosition(target)` returns,
+ * that indicates `target` is located later than `node` in the document or `node` contains `target`.
+ */
+export const DOCUMENT_POSITION_BROAD_FOLLOWING =
+ // Checks `typeof Node` for `react-docgen`
+ typeof Node !== 'undefined' &&
+ Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY;
+
+/**
+ * CSS selector that selects major nodes that is sequential-focusable.
+ */
+export const selectorTabbable = `
+ a[href], area[href], input:not([disabled]):not([tabindex='-1']),
+ button:not([disabled]):not([tabindex='-1']),select:not([disabled]):not([tabindex='-1']),
+ textarea:not([disabled]):not([tabindex='-1']),
+ iframe, object, embed, *[tabindex]:not([tabindex='-1']), *[contenteditable=true]
+`;
diff --git a/packages/react/src/internal/wrapFocus.js b/packages/react/src/internal/wrapFocus.js
new file mode 100644
index 000000000000..27463d0b36ad
--- /dev/null
+++ b/packages/react/src/internal/wrapFocus.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright IBM Corp. 2020
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import findLast from 'lodash.findlast';
+import { settings } from 'carbon-components';
+import {
+ DOCUMENT_POSITION_BROAD_PRECEDING,
+ DOCUMENT_POSITION_BROAD_FOLLOWING,
+ selectorTabbable,
+} from './keyboard/navigation';
+
+const { prefix } = settings;
+
+/**
+ * @param {Node} node A DOM node.
+ * @param {string[]} selectorsFloatingMenus The CSS selectors that matches floating menus.
+ * @returns {boolean} `true` of the given `node` is in a floating menu.
+ */
+function elementOrParentIsFloatingMenu(
+ node,
+ selectorsFloatingMenus = [
+ `.${prefix}--overflow-menu-options`,
+ `.${prefix}--tooltip`,
+ '.flatpickr-calendar',
+ ]
+) {
+ if (node && typeof node.closest === 'function') {
+ return selectorsFloatingMenus.some(selector => node.closest(selector));
+ }
+}
+
+/**
+ * Ensures the focus is kept in the given `modalNode`, implementing "focus-wrap" behavior.
+ * @param {object} options The options.
+ * @param {Node} options.modalNode The DOM node of the inner modal.
+ * @param {Node} options.startTrapNode The DOM node of the focus sentinel the is placed earlier next to `modalNode`.
+ * @param {Node} options.endTrapNode The DOM node of the focus sentinel the is placed next to `modalNode`.
+ * @param {Node} options.currentActiveNode The DOM node that has focus.
+ * @param {Node} options.oldActiveNode The DOM node that previously had focus.
+ * @param {Node} [options.selectorsFloatingMenus] The CSS selectors that matches floating menus.
+ */
+function wrapFocus({
+ modalNode,
+ startTrapNode,
+ endTrapNode,
+ currentActiveNode,
+ oldActiveNode,
+ selectorsFloatingMenus,
+}) {
+ if (
+ modalNode &&
+ currentActiveNode &&
+ oldActiveNode &&
+ !modalNode.contains(currentActiveNode) &&
+ !elementOrParentIsFloatingMenu(currentActiveNode, selectorsFloatingMenus)
+ ) {
+ const comparisonResult = oldActiveNode.compareDocumentPosition(
+ currentActiveNode
+ );
+ if (
+ currentActiveNode === startTrapNode ||
+ comparisonResult & DOCUMENT_POSITION_BROAD_PRECEDING
+ ) {
+ const tabbable = findLast(
+ modalNode.querySelectorAll(selectorTabbable),
+ elem => Boolean(elem.offsetParent)
+ );
+ if (tabbable) {
+ tabbable.focus();
+ } else if (modalNode !== oldActiveNode) {
+ modalNode.focus();
+ }
+ } else if (
+ currentActiveNode === endTrapNode ||
+ comparisonResult & DOCUMENT_POSITION_BROAD_FOLLOWING
+ ) {
+ const tabbable = Array.prototype.find.call(
+ modalNode.querySelectorAll(selectorTabbable),
+ elem => Boolean(elem.offsetParent)
+ );
+ if (tabbable) {
+ tabbable.focus();
+ } else if (modalNode !== oldActiveNode) {
+ modalNode.focus();
+ }
+ }
+ }
+}
+
+export { elementOrParentIsFloatingMenu };
+export default wrapFocus;
diff --git a/yarn.lock b/yarn.lock
index 5dba2fc33c13..e560b5eee5cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14465,6 +14465,11 @@ lodash.escaperegexp@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
+lodash.findlast@^4.5.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.findlast/-/lodash.findlast-4.6.0.tgz#ea8bb78cf2e7e7804fc8aeb7d1953e07fe31fbc8"
+ integrity sha1-6ou3jPLn54BPyK630ZU+B/4x+8g=
+
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
@@ -19716,14 +19721,7 @@ rollup-pluginutils@2.0.1:
estree-walker "^0.3.0"
micromatch "^2.3.11"
-rollup-pluginutils@2.8.2:
- version "2.8.2"
- resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
- integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
- dependencies:
- estree-walker "^0.6.1"
-
-rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1:
+rollup-pluginutils@2.8.2, rollup-pluginutils@^2.6.0, rollup-pluginutils@^2.8.1:
version "2.8.2"
resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==