diff --git a/.yarn-offline-mirror/focus-trap-4.0.2.tgz b/.yarn-offline-mirror/focus-trap-4.0.2.tgz
new file mode 100644
index 0000000000..a35ebac14e
Binary files /dev/null and b/.yarn-offline-mirror/focus-trap-4.0.2.tgz differ
diff --git a/.yarn-offline-mirror/focus-trap-react-6.0.0.tgz b/.yarn-offline-mirror/focus-trap-react-6.0.0.tgz
new file mode 100644
index 0000000000..935608d637
Binary files /dev/null and b/.yarn-offline-mirror/focus-trap-react-6.0.0.tgz differ
diff --git a/.yarn-offline-mirror/tabbable-3.1.2.tgz b/.yarn-offline-mirror/tabbable-3.1.2.tgz
new file mode 100644
index 0000000000..d46c107e2c
Binary files /dev/null and b/.yarn-offline-mirror/tabbable-3.1.2.tgz differ
diff --git a/package.json b/package.json
index 5e61644d81..cd3b3f08c3 100644
--- a/package.json
+++ b/package.json
@@ -134,6 +134,7 @@
"classnames": "2.2.6",
"downshift": "^1.31.14",
"flatpickr": "4.5.5",
+ "focus-trap-react": "^6.0.0",
"invariant": "^2.2.3",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
diff --git a/src/components/Modal/Modal-test.js b/src/components/Modal/Modal-test.js
index 080b98b47b..a1f5231e3e 100644
--- a/src/components/Modal/Modal-test.js
+++ b/src/components/Modal/Modal-test.js
@@ -13,31 +13,34 @@ import ModalWrapper from '../ModalWrapper';
import { shallow, mount } from 'enzyme';
import { componentsX } from '../../internal/FeatureFlags';
+// The modal is the 0th child inside the wrapper on account of focus-trap-react
+const getModal = wrapper => wrapper.childAt(0);
+
describe('Modal', () => {
describe('Renders as expected', () => {
const wrapper = shallow();
const mounted = mount();
it('has the expected classes', () => {
- expect(wrapper.hasClass('bx--modal')).toEqual(true);
+ expect(getModal(wrapper).hasClass('bx--modal')).toEqual(true);
});
it('should add extra classes that are passed via className', () => {
- expect(wrapper.hasClass('extra-class')).toEqual(true);
+ expect(getModal(wrapper).hasClass('extra-class')).toEqual(true);
});
it('should not be a passive modal by default', () => {
- expect(wrapper.hasClass('bx--modal-tall')).toEqual(true);
+ expect(getModal(wrapper).hasClass('bx--modal-tall')).toEqual(true);
});
it('should be a passive modal when passiveModal is passed', () => {
wrapper.setProps({ passiveModal: true });
- expect(wrapper.hasClass('bx--modal-tall')).toEqual(false);
+ expect(getModal(wrapper).hasClass('bx--modal-tall')).toEqual(false);
});
it('should set id if one is passed via props', () => {
const modal = shallow();
- expect(modal.props().id).toEqual('modal-1');
+ expect(getModal(modal).props().id).toEqual('modal-1');
});
it('has the expected default iconDescription', () => {
@@ -215,7 +218,7 @@ describe('Danger Modal', () => {
const wrapper = shallow();
it('has the expected classes', () => {
- expect(wrapper.hasClass('bx--modal--danger')).toEqual(true);
+ expect(getModal(wrapper).hasClass('bx--modal--danger')).toEqual(true);
});
it('has correct button combination', () => {
diff --git a/src/components/Modal/Modal.js b/src/components/Modal/Modal.js
index a470d994c6..f3c42e3f98 100644
--- a/src/components/Modal/Modal.js
+++ b/src/components/Modal/Modal.js
@@ -14,6 +14,7 @@ import Button from '../Button';
import { settings } from 'carbon-components';
import Close20 from '@carbon/icons-react/lib/close/20';
import { breakingChangesX, componentsX } from '../../internal/FeatureFlags';
+import FocusTrap from 'focus-trap-react';
const { prefix } = settings;
@@ -125,6 +126,12 @@ export default class Modal extends Component {
* be focused when the Modal opens
*/
selectorPrimaryFocus: PropTypes.string,
+
+ /**
+ * Specify whether the modal should be a focus trap. NOTE: by default
+ * this is true
+ */
+ focusTrap: PropTypes.bool,
};
static defaultProps = {
@@ -137,6 +144,7 @@ export default class Modal extends Component {
modalHeading: '',
modalLabel: '',
selectorPrimaryFocus: '[data-modal-primary-focus]',
+ focusTrap: true,
};
button = React.createRef();
@@ -266,6 +274,7 @@ export default class Modal extends Component {
selectorPrimaryFocus, // eslint-disable-line
selectorsFloatingMenus, // eslint-disable-line
shouldSubmitOnEnter, // eslint-disable-line
+ focusTrap,
...other
} = this.props;
@@ -339,18 +348,22 @@ export default class Modal extends Component {
);
return (
-
- {modalBody}
-
+
+
+ {modalBody}
+
+
);
}
}
diff --git a/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap b/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
index d34b3a3c29..b15870fa2e 100644
--- a/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
+++ b/src/components/ModalWrapper/__snapshots__/ModalWrapper-test.js.snap
@@ -42,6 +42,7 @@ exports[`ModalWrapper should render 1`] = `
-
-
- Test Modal Label
-
-
- Transactional Modal
-
-
-
-
-
+
+ close the modal
+
+
+
+
+
+
+
- Text
-
-
-
-
+ Text
+
+
+
-
-
-
- Save
- ,
- }
- }
- kind="primary"
- onClick={[Function]}
- small={false}
- tabIndex={0}
- type="button"
- >
-
+
+
+ Save
+ ,
+ }
+ }
+ kind="primary"
onClick={[Function]}
+ small={false}
tabIndex={0}
type="button"
>
- Save
-
-
+
+
+
-
+
diff --git a/yarn.lock b/yarn.lock
index 48be742efa..6481751819 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5463,6 +5463,21 @@ flush-write-stream@^1.0.0:
inherits "^2.0.1"
readable-stream "^2.0.4"
+focus-trap-react@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-6.0.0.tgz#3f5a9f68447dd374d22388fb4c50018be83e74a5"
+ integrity sha512-mvEYxmP75PMx0vOqoIAmJHO/qUEvdTAdz6gLlEZyxxODnuKQdnKea2RWTYxghAPrV+ibiIq2o/GTSgQycnAjcw==
+ dependencies:
+ focus-trap "^4.0.2"
+
+focus-trap@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-4.0.2.tgz#4ee2b96547c9ea0e4252a2d4b2cca68944194663"
+ integrity sha512-HtLjfAK7Hp2qbBtLS6wEznID1mPT+48ZnP2nkHzgjpL4kroYHg0CdqJ5cTXk+UO5znAxF5fRUkhdyfgrhh8Lzw==
+ dependencies:
+ tabbable "^3.1.2"
+ xtend "^4.0.1"
+
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
@@ -12304,6 +12319,11 @@ symbol.prototype.description@^1.0.0:
dependencies:
has-symbols "^1.0.0"
+tabbable@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-3.1.2.tgz#f2d16cccd01f400e38635c7181adfe0ad965a4a2"
+ integrity sha512-wjB6puVXTYO0BSFtCmWQubA/KIn7Xvajw0x0l6eJUudMG/EAiJvIUnyNX6xO4NpGrJ16lbD0eUseB9WxW0vlpQ==
+
table@^5.0.2:
version "5.1.1"
resolved "https://registry.yarnpkg.com/table/-/table-5.1.1.tgz#92030192f1b7b51b6eeab23ed416862e47b70837"