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`] = ` - - + 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"