/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ import React from 'react'; import { mount, shallow } from 'enzyme'; import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from '../'; import { keyCodes } from '../utils'; const didMount = (component) => { const wrapper = mount(component); wrapper.setProps({ fakefield: 'fakeToUpdate' }); return wrapper; } describe('Modal', () => { let isOpen; let toggle; let isOpenNested; let toggleNested; beforeEach(() => { isOpen = false; toggle = () => { isOpen = !isOpen; }; isOpenNested = false; toggleNested = () => { isOpenNested = !isOpenNested; }; jest.useFakeTimers(); }); afterEach(() => { // fast forward time for modal to fade out jest.runTimersToTime(300); jest.clearAllTimers(); }); it('should render modal portal into DOM', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(wrapper.childAt(0).children().length).not.toBe(0); wrapper.unmount(); }); it('should render with the class "modal-dialog"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); wrapper.unmount(); }); it('should render external content when present', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} external={<button className="cool-close-button">×</button>}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('cool-close-button').length).toBe(1); expect(document.getElementsByClassName('cool-close-button')[0].nextElementSibling.className.split(' ').indexOf('modal-dialog') > -1).toBe(true); wrapper.unmount(); }); it('should render with the backdrop with the class "modal-backdrop" by default', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-backdrop').length).toBe(1); wrapper.unmount(); }); it('should render with the backdrop with the class "modal-backdrop" when backdrop is "static"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-backdrop').length).toBe(1); wrapper.unmount(); }); it('should not render with the backdrop with the class "modal-backdrop" when backdrop is "false"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop={false}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('modal-backdrop').length).toBe(0); wrapper.unmount(); }); it('should render with the class "modal-dialog-scrollable" when scrollable is "true"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} scrollable={true}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog-scrollable').length).toBe(1); wrapper.unmount(); }); it('should render with class "modal-dialog" and have custom class name if provided', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} className="my-custom-modal"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('my-custom-modal').length).toBe(1); wrapper.unmount(); }); it('should render with class "modal-dialog" w/o centered class if not provided', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('modal-dialog-centered').length).toBe(0); wrapper.unmount(); }); it('should render with class "modal-dialog" and centered class if provided', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} centered> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('modal-dialog-centered').length).toBe(1); wrapper.unmount(); }); it('should render with additional props if provided', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} style={{ maxWidth: '95%' }}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('modal-dialog')[0].style.maxWidth).toBe('95%'); wrapper.unmount(); }); it('should render without fade transition if provided with fade={false}', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} fade={false} modalClassName="fadeless-modal"> Howdy! </Modal> ); // Modal should appear instantaneously jest.runTimersToTime(1); const matchedModals = document.getElementsByClassName('fadeless-modal'); const matchedModal = matchedModals[0]; expect(matchedModals.length).toBe(1); // Modal should not have the 'fade' class expect(matchedModal.className.split(' ').indexOf('fade') < 0).toBe(true); wrapper.unmount(); }); it('should render when expected when passed modalTransition and backdropTransition props', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} modalTransition={{ timeout: 2 }} backdropTransition={{ timeout: 10 }} modalClassName="custom-timeout-modal" > Hello, world! </Modal> ); jest.runTimersToTime(20); const matchedModals = document.getElementsByClassName('custom-timeout-modal'); expect(matchedModals.length).toBe(1); wrapper.unmount(); }); it('should render with class "modal" and have custom class name if provided with modalClassName', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} modalClassName="my-custom-modal"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.querySelectorAll('.modal.my-custom-modal').length).toBe(1); wrapper.unmount(); }); it('should render with custom class name if provided with wrapClassName', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} wrapClassName="my-custom-modal"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('my-custom-modal').length).toBe(1); wrapper.unmount(); }); it('should render with class "modal-content" and have custom class name if provided with contentClassName', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} contentClassName="my-custom-modal"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.querySelectorAll('.modal-content.my-custom-modal').length).toBe(1); wrapper.unmount(); }); it('should render with class "modal-backdrop" and have custom class name if provided with backdropClassName', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdropClassName="my-custom-modal"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.querySelectorAll('.modal-backdrop.my-custom-modal').length).toBe(1); wrapper.unmount(); }); it('should render with the class "modal-${size}" when size is passed', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} size="crazy"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(1); expect(document.getElementsByClassName('modal-crazy').length).toBe(1); wrapper.unmount(); }); it('should render modal when isOpen is true', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal').length).toBe(1); expect(document.getElementsByClassName('modal-backdrop').length).toBe(1); wrapper.unmount(); }); it('should render modal with default role of "dialog"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal')[0].getAttribute('role')).toBe('dialog'); wrapper.unmount(); }); it('should render modal with provided role', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} role="alert"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal')[0].getAttribute('role')).toBe('alert'); wrapper.unmount(); }); it('should render modal with aria-labelledby provided labelledBy', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} labelledBy="myModalTitle"> Yo! </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal')[0].getAttribute('aria-labelledby')).toBe('myModalTitle'); wrapper.unmount(); }); it('should not render modal when isOpen is false', () => { const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(wrapper.childAt(0).children().length).toBe(0); expect(document.getElementsByClassName('modal').length).toBe(0); expect(document.getElementsByClassName('modal-backdrop').length).toBe(0); wrapper.unmount(); }); it('should toggle modal', () => { const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(wrapper.childAt(0).children().length).toBe(0); expect(document.getElementsByClassName('modal').length).toBe(0); expect(document.getElementsByClassName('modal-backdrop').length).toBe(0); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); expect(document.getElementsByClassName('modal-backdrop').length).toBe(1); wrapper.unmount(); }); it('should call onClosed & onOpened', () => { jest.spyOn(Modal.prototype, 'onOpened'); jest.spyOn(Modal.prototype, 'onClosed'); const onOpened = jest.fn(); const onClosed = jest.fn(); const wrapper = didMount( <Modal isOpen={isOpen} onOpened={onOpened} onClosed={onClosed} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(onOpened).not.toHaveBeenCalled(); expect(Modal.prototype.onOpened).not.toHaveBeenCalled(); expect(onClosed).not.toHaveBeenCalled(); expect(Modal.prototype.onClosed).not.toHaveBeenCalled(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(onOpened).toHaveBeenCalled(); expect(Modal.prototype.onOpened).toHaveBeenCalled(); expect(onClosed).not.toHaveBeenCalled(); expect(Modal.prototype.onClosed).not.toHaveBeenCalled(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(onClosed).toHaveBeenCalled(); expect(Modal.prototype.onClosed).toHaveBeenCalled(); wrapper.unmount(); }); it('should call onClosed & onOpened when fade={false}', () => { const onOpened = jest.fn(); const onClosed = jest.fn(); const wrapper = didMount( <Modal isOpen={isOpen} onOpened={onOpened} onClosed={onClosed} toggle={toggle} fade={false}> Yo! </Modal> ); jest.runTimersToTime(1); expect(isOpen).toBe(false); expect(onOpened).not.toHaveBeenCalled(); expect(onClosed).not.toHaveBeenCalled(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(1); expect(isOpen).toBe(true); expect(onOpened).toHaveBeenCalled(); expect(onClosed).not.toHaveBeenCalled(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(1); expect(isOpen).toBe(false); expect(onClosed).toHaveBeenCalled(); wrapper.unmount(); }); it('should not call init when isOpen does not change', () => { jest.spyOn(Modal.prototype, 'init'); jest.spyOn(Modal.prototype, 'componentDidUpdate'); const wrapper = mount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(Modal.prototype.init).not.toHaveBeenCalled(); expect(Modal.prototype.componentDidUpdate).not.toHaveBeenCalled(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(Modal.prototype.init).not.toHaveBeenCalled(); expect(Modal.prototype.componentDidUpdate).toHaveBeenCalled(); wrapper.unmount(); }); it('should close modal when escape key pressed', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); instance.handleEscape({ keyCode: 13 }); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const escapeKeyUpEvent = { keyCode: keyCodes.esc, preventDefault: jest.fn(() => {}), stopPropagation: jest.fn(() => {}), }; instance.handleEscape(escapeKeyUpEvent); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(escapeKeyUpEvent.preventDefault.mock.calls.length).toBe(1); expect(escapeKeyUpEvent.stopPropagation.mock.calls.length).toBe(1); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal').length).toBe(0); wrapper.unmount(); }); it('should not close modal when escape key pressed when keyboard is false', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} keyboard={false}> Yo! </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); instance.handleEscape({ keyCode: 13 }); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); instance.handleEscape({ keyCode: 27 }); jest.runTimersToTime(300); expect(isOpen).toBe(true); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal').length).toBe(1); wrapper.unmount(); }); it('should close modal when clicking backdrop', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <button id="clicker">Does Nothing</button> </Modal> ); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); // document.getElementById('clicker').click(); jest.runTimersToTime(300); expect(isOpen).toBe(true); const modal = document.getElementsByClassName('modal')[0]; const mouseDownEvent = document.createEvent('MouseEvents'); mouseDownEvent.initEvent('mousedown', true, true); modal.dispatchEvent(mouseDownEvent); const clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent('click', true, true); modal.dispatchEvent(clickEvent); jest.runTimersToTime(300); expect(isOpen).toBe(false); wrapper.unmount(); }); it('should not close modal when clicking backdrop and backdrop is "static"', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static"> <button id="clicker">Does Nothing</button> </Modal> ); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); document.getElementById('clicker').click(); jest.runTimersToTime(300); expect(isOpen).toBe(true); document.getElementsByClassName('modal-backdrop')[0].click(); jest.runTimersToTime(300); expect(isOpen).toBe(true); wrapper.unmount(); }); it('should not close modal when escape key pressed and backdrop is "static" and keyboard=false', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static" keyboard={false}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const escapeKeyUpEvent = { keyCode: keyCodes.esc, preventDefault: jest.fn(() => {}), stopPropagation: jest.fn(() => {}), }; instance.handleEscape(escapeKeyUpEvent); jest.runTimersToTime(300); expect(isOpen).toBe(true); wrapper.unmount(); }); it('should close modal when escape key pressed and backdrop is "static" and keyboard=true', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static" keyboard={true}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const escapeKeyUpEvent = { keyCode: keyCodes.esc, preventDefault: jest.fn(() => {}), stopPropagation: jest.fn(() => {}), }; instance.handleEscape(escapeKeyUpEvent); jest.runTimersToTime(300); expect(isOpen).toBe(false); wrapper.unmount(); }); it('should animate when backdrop is "static" and escape key pressed and keyboard=false', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static" keyboard={false}> <button id="clicker">Does Nothing</button> </Modal> ); const spy = jest.spyOn(wrapper.instance(), 'handleStaticBackdropAnimation'); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const escapeKeyUpEvent = { keyCode: keyCodes.esc, preventDefault: jest.fn(() => {}), stopPropagation: jest.fn(() => {}), }; wrapper.instance().handleEscape(escapeKeyUpEvent); expect(spy).toHaveBeenCalled(); wrapper.unmount(); }); it('should animate when backdrop is "static" and backdrop is clicked', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static"> <button id="clicker">Does Nothing</button> </Modal> ); const spy = jest.spyOn(wrapper.instance(), 'handleStaticBackdropAnimation'); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const modal = document.getElementsByClassName('modal')[0]; const mouseDownEvent = document.createEvent('MouseEvents'); mouseDownEvent.initEvent('mousedown', true, true); modal.dispatchEvent(mouseDownEvent); const clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent('click', true, true); modal.dispatchEvent(clickEvent); jest.runTimersToTime(300); expect(spy).toHaveBeenCalled(); wrapper.unmount(); }); it('should not animate when backdrop is "static" and modal is clicked', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} backdrop="static"> <button id="clicker">Does Nothing</button> </Modal> ); const spy = jest.spyOn(wrapper.instance(), 'handleStaticBackdropAnimation'); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.getElementsByClassName('modal').length).toBe(1); const modalDialog = document.getElementsByClassName('modal-dialog')[0]; const mouseDownEvent = document.createEvent('MouseEvents'); mouseDownEvent.initEvent('mousedown', true, true); modalDialog.dispatchEvent(mouseDownEvent); const clickEvent = document.createEvent('MouseEvents'); clickEvent.initEvent('click', true, true); modalDialog.dispatchEvent(clickEvent); jest.runTimersToTime(300); expect(spy).not.toHaveBeenCalled(); wrapper.unmount(); }); it('should destroy this._element', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(instance._element).toBeTruthy(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(instance._element).toBe(null); wrapper.unmount(); }); it('should destroy this._element when unmountOnClose prop set to true', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} unmountOnClose={true}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(instance._element).toBeTruthy(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(instance._element).toBe(null); wrapper.unmount(); }); it('should not destroy this._element when unmountOnClose prop set to false', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle} unmountOnClose={false}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(instance._element).toBeTruthy(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(instance._element).toBeTruthy(); wrapper.unmount(); }); it('should destroy this._element on unmount', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <button id="clicker">Does Nothing</button> </Modal> ); const instance = wrapper.instance(); jest.runTimersToTime(300); expect(instance._element).toBeTruthy(); wrapper.unmount(); jest.runTimersToTime(300); expect(instance._element).toBe(null); }); it('should render nested modals', () => { isOpen = true; isOpenNested = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <ModalBody> <Modal isOpen={isOpenNested} toggle={toggleNested}> Yo! </Modal> </ModalBody> </Modal> ); jest.runTimersToTime(300); expect(document.getElementsByClassName('modal-dialog').length).toBe(2); expect(document.body.className).toBe('modal-open'); toggleNested(); jest.runTimersToTime(300); expect(isOpenNested).toBe(false); expect(document.getElementsByClassName('modal-dialog').length).toBe(2); expect(document.body.className).toBe('modal-open'); wrapper.unmount(); expect(document.getElementsByClassName('modal-dialog').length).toBe(0); expect(document.body.className).toBe(''); }); it('should remove exactly modal-open class from body', () => { // set a body class which includes modal-open document.body.className = 'my-modal-opened'; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> Yo! </Modal> ); // assert that the modal is closed and the body class is what was set initially jest.runTimersToTime(300); expect(isOpen).toBe(false); expect(document.body.className).toBe('my-modal-opened'); toggle(); wrapper.setProps({ isOpen: isOpen }); // assert that the modal is open and the body class is what was set initially + modal-open jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(document.body.className).toBe('my-modal-opened modal-open'); // append another body class which includes modal-open // using this to test if replace will leave a space when removing modal-open document.body.className += ' modal-opened'; expect(document.body.className).toBe('my-modal-opened modal-open modal-opened'); toggle(); wrapper.setProps({ isOpen: isOpen }); // assert that the modal is closed and the body class is what was set initially jest.runTimersToTime(301); expect(isOpen).toBe(false); expect(document.body.className).toBe('my-modal-opened modal-opened'); wrapper.unmount(); }); it('should call onEnter & onExit props if provided', () => { const onEnter = jest.fn(); const onExit = jest.fn(); const wrapper = didMount( <Modal isOpen={isOpen} onEnter={onEnter} onExit={onExit} toggle={toggle}> Yo! </Modal> ); expect(isOpen).toBe(false); expect(onEnter).toHaveBeenCalled(); expect(onExit).not.toHaveBeenCalled(); onEnter.mockReset(); onExit.mockReset(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); expect(isOpen).toBe(true); expect(onEnter).not.toHaveBeenCalled(); expect(onExit).not.toHaveBeenCalled(); onEnter.mockReset(); onExit.mockReset(); toggle(); wrapper.setProps({ isOpen: isOpen }); jest.runTimersToTime(300); wrapper.unmount(); expect(onEnter).not.toHaveBeenCalled(); expect(onExit).toHaveBeenCalled(); }); it('should update element z index when prop changes', () => { const wrapper = shallow( <Modal isOpen zIndex={0}> Yo! </Modal> ); expect(wrapper.instance()._element.style.zIndex).toBe('0'); wrapper.setProps({ zIndex: 1 }); expect(wrapper.instance()._element.style.zIndex).toBe('1'); }); it('should allow focus on only focusable elements', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <ModalHeader toggle={toggle}>Modal title</ModalHeader> <ModalBody> <a alt="test" href="/">Test</a> <map name="test"> <area alt="test" href="/" coords="200,5,200,30" /> </map> <input type="text" /> <input type="hidden" /> <input type="text" disabled value="Test" /> <select name="test" id="select_test"> <option>Test item</option> </select> <select name="test" id="select_test_disabled" disabled> <option>Test item</option> </select> <textarea name="textarea_test" id="textarea_test" cols="30" rows="10" /> <textarea name="textarea_test_disabled" id="textarea_test_disabled" cols="30" rows="10" disabled /> <object>Test</object> <span tabIndex="0">test tab index</span> </ModalBody> <ModalFooter> <Button disabled color="primary" onClick={toggle}>Do Something</Button>{' '} <Button color="secondary" onClick={toggle}>Cancel</Button> </ModalFooter> </Modal> ); const instance = wrapper.instance(); expect(instance.getFocusableChildren().length).toBe(9); wrapper.unmount(); }); it('should tab through focusable elements', () => { isOpen = true; const mock = jest.fn(); const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <ModalBody> <Button onFocus={mock} color="secondary" onClick={toggle}>Cancel</Button> </ModalBody> </Modal> ); const instance = wrapper.instance(); expect(instance.getFocusedChild()).not.toBe(instance.getFocusableChildren()[0]); wrapper.find('.modal').hostNodes().simulate('keyDown', { which: 9, key: 'Tab', keyCode: 9 }); expect(instance.getFocusedChild()).toBe(instance.getFocusableChildren()[0]); expect(mock).toHaveBeenCalled(); wrapper.unmount(); }); it('should return the focus to the last focused element before the modal has opened', () => { const MockComponent = ({ isOpen = false }) => ( <> <button className={'focus'}>Focused</button> <Modal isOpen={isOpen}> <ModalBody>Whatever</ModalBody> </Modal> </> ); const wrapper = didMount(<MockComponent />); const button = wrapper .find('.focus') .hostNodes() .getDOMNode(); button.focus(); wrapper.setProps({ isOpen: true }); wrapper.setProps({ isOpen: false }); jest.runAllTimers(); expect(document.activeElement === button).toEqual(true); wrapper.unmount(); }) it('should not return the focus to the last focused element before the modal has opened when "returnFocusAfterClose" is false', () => { const MockComponent = ({ isOpen = false }) => ( <> <button className={'focus'}>Focused</button> <Modal isOpen={isOpen} returnFocusAfterClose={false} > <ModalBody>Whatever</ModalBody> </Modal> </> ); const wrapper = didMount(<MockComponent />); const button = wrapper .find('.focus') .hostNodes() .getDOMNode(); button.focus(); wrapper.setProps({ isOpen: true }); wrapper.setProps({ isOpen: false }); jest.runAllTimers(); expect(document.activeElement === button).toEqual(false); wrapper.unmount(); }) it('should return the focus to the last focused element before the modal has opened when "unmountOnClose" is false', () => { const MockComponent = ({ isOpen = false }) => ( <> <button className={'focus'}>Focused</button> <Modal isOpen={isOpen} unmountOnClose={false}> <ModalBody>Whatever</ModalBody> </Modal> </> ); const wrapper = didMount(<MockComponent />); const button = wrapper .find('.focus') .hostNodes() .getDOMNode(); button.focus(); wrapper.setProps({ isOpen: true }); wrapper.setProps({ isOpen: false }); jest.runAllTimers(); expect(document.activeElement === button).toEqual(true); wrapper.unmount(); }) it('should not return the focus to the last focused element before the modal has opened when "returnFocusAfterClose" is false and "unmountOnClose" is false', () => { const MockComponent = ({ isOpen = false }) => ( <> <button className={'focus'}/> <Modal isOpen={isOpen} returnFocusAfterClose={false} unmountOnClose={false}> <ModalBody>Whatever</ModalBody> </Modal> </> ); const wrapper = didMount(<MockComponent />); const button = wrapper .find('.focus') .hostNodes() .getDOMNode(); button.focus(); wrapper.setProps({ isOpen: true }); wrapper.setProps({ isOpen: false }); jest.runAllTimers(); expect(document.activeElement === button).toEqual(false); wrapper.unmount(); }); it('should attach/detach trapFocus for dialogs', () => { const addEventListenerFn = document.addEventListener, removeEventListenerFn = document.removeEventListener; document.addEventListener = jest.fn(); document.removeEventListener = jest.fn(); const MockComponent = () => ( <> <Modal isOpen={true}> <ModalBody> <Button className={'focus'}>focusable element</Button> </ModalBody> </Modal> </>), wrapper = didMount(<MockComponent />), modal_instance = wrapper.find(Modal).instance(); expect(document.addEventListener.mock.calls.length).toBe(1); expect(document.addEventListener.mock.calls[0]).toEqual(['focus', modal_instance.trapFocus, true]); wrapper.unmount(); expect(document.removeEventListener.mock.calls.length).toBe(1); expect(document.removeEventListener.mock.calls[0]).toEqual(['focus', modal_instance.trapFocus, true]); // restore global document mock document.addEventListener = addEventListenerFn; document.removeEventListener = removeEventListenerFn; }); it('should trap focus inside the open dialog', () => { const MockComponent = () => ( <> <Button className={'first'}>Focused</Button> <Modal isOpen={true} trapFocus> <ModalBody> Something else to see <Button className={'focus'}>focusable element</Button> </ModalBody> </Modal> </>), wrapper = didMount(<MockComponent />); const button = wrapper.find('.first').hostNodes().getDOMNode(), button2 = wrapper.find('.focus').hostNodes().getDOMNode(), modal_instance = wrapper.find(Modal).instance(), ev_mock = { target: button, preventDefault: jest.fn(), stopPropagation: jest.fn() }; button.focus(); modal_instance.trapFocus(ev_mock); jest.runAllTimers(); expect(document.activeElement).not.toBe(button); expect(document.activeElement).toBe(button2); expect(ev_mock.preventDefault.mock.calls.length).toBe(1); expect(ev_mock.stopPropagation.mock.calls.length).toBe(1); wrapper.unmount(); }); it('should not trap focus when there is a nested modal', () => { isOpen = true; const wrapper = didMount( <Modal isOpen={isOpen} toggle={toggle}> <ModalBody> <Button className={'b0'} onClick={toggle}>Cancel</Button> <Modal isOpen={true}> <ModalBody> <Button className={'b1'}>Click 1</Button> <Button className={'b2'}>Click 2</Button> </ModalBody> </Modal> </ModalBody> </Modal> ); const instance = wrapper.instance(), nested = wrapper.find(Modal).at(1).instance(), button = wrapper.find('.b0').hostNodes().getDOMNode(), button1 = wrapper.find('.b1').hostNodes().getDOMNode(), button2 = wrapper.find('.b2').hostNodes().getDOMNode(), ev_mock = { target: button, preventDefault: jest.fn(), stopPropagation: jest.fn() }; button2.focus(); instance.trapFocus(ev_mock); jest.runAllTimers(); expect(document.activeElement).not.toBe(button); expect(document.activeElement).toBe(button2); wrapper.unmount(); }); it('should not handle tab if there is a nested Modal', () => { const wrapper = didMount( <Modal isOpen={true} toggle={toggle}> <ModalBody> <Button className={'b0'} onClick={toggle}>Cancel</Button> <Modal isOpen={true}> <ModalBody> <Button className={'b1'}>Click 1</Button> </ModalBody> </Modal> </ModalBody> </Modal> ); const instance = wrapper.instance(), nested = wrapper.find(Modal).at(1).instance(), button = wrapper.find('.b0').hostNodes().getDOMNode(), button1 = wrapper.find('.b1').hostNodes().getDOMNode(), ev_mock = { target: button1, which: 9, shiftKey: true, preventDefault: jest.fn(), stopPropagation: jest.fn() }; button1.focus(); instance.getFocusableChildren = jest.fn(); instance.getFocusableChildren.mockReturnValue([]); instance.handleTab(ev_mock); jest.runAllTimers(); expect(instance.getFocusableChildren.mock.calls.length).toBe(0); wrapper.unmount(); }); });