UNPKG

29.9 kBJavaScriptView Raw
1/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
2import React from 'react';
3import { render, screen } from '@testing-library/react';
4import user from '@testing-library/user-event';
5import '@testing-library/jest-dom';
6import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from '..';
7
8describe('Modal', () => {
9 const toggle = () => {};
10
11 beforeEach(() => {
12 jest.useFakeTimers();
13 });
14
15 afterEach(() => {
16 jest.clearAllTimers();
17 // rtl doesn't clear attributes added to body, so manually clearing them
18 document.body.removeAttribute('style');
19 document.body.removeAttribute('class');
20 });
21
22 it('should render modal portal into DOM', () => {
23 render(
24 <Modal isOpen toggle={toggle}>
25 Yo!
26 </Modal>,
27 );
28
29 expect(screen.getByText(/yo/i)).toBeInTheDocument();
30 });
31
32 it('should render with the class "modal-dialog"', () => {
33 render(
34 <Modal isOpen toggle={toggle}>
35 Yo!
36 </Modal>,
37 );
38
39 expect(screen.getByText(/yo/i).parentElement).toHaveClass('modal-dialog');
40 });
41
42 it('should render external content when present', () => {
43 render(
44 <Modal
45 isOpen
46 toggle={toggle}
47 external={<button className="cool-close-button">crazy button</button>}
48 >
49 Yo!
50 </Modal>,
51 );
52
53 expect(screen.getByText(/crazy button/i)).toBeInTheDocument();
54 expect(
55 screen
56 .getByText(/crazy button/i)
57 .nextElementSibling.className.split(' ')
58 .indexOf('modal-dialog') > -1,
59 ).toBe(true);
60 });
61
62 it('should render with the backdrop with the class "modal-backdrop" by default', () => {
63 render(
64 <Modal isOpen toggle={toggle}>
65 Yo!
66 </Modal>,
67 );
68
69 expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
70 });
71
72 it('should render with the backdrop with the class "modal-backdrop" when backdrop is "static"', () => {
73 render(
74 <Modal isOpen toggle={toggle} backdrop="static">
75 Yo!
76 </Modal>,
77 );
78
79 expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
80 });
81
82 it('should not render with the backdrop with the class "modal-backdrop" when backdrop is "false"', () => {
83 render(
84 <Modal isOpen toggle={toggle} backdrop={false}>
85 Yo!
86 </Modal>,
87 );
88
89 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
90 expect(document.getElementsByClassName('modal-backdrop').length).toBe(0);
91 });
92
93 it('should render with the class "modal-dialog-scrollable" when scrollable is "true"', () => {
94 render(
95 <Modal isOpen toggle={toggle} scrollable>
96 Yo!
97 </Modal>,
98 );
99
100 expect(
101 document.getElementsByClassName('modal-dialog-scrollable').length,
102 ).toBe(1);
103 });
104
105 it('should render with class "modal-dialog" and have custom class name if provided', () => {
106 render(
107 <Modal isOpen toggle={toggle} className="my-custom-modal">
108 Yo!
109 </Modal>,
110 );
111
112 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
113 expect(document.getElementsByClassName('my-custom-modal').length).toBe(1);
114 });
115
116 it('should render with class "modal-dialog" w/o centered class if not provided', () => {
117 render(
118 <Modal isOpen toggle={toggle}>
119 Yo!
120 </Modal>,
121 );
122
123 jest.advanceTimersByTime(300);
124 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
125 expect(
126 document.getElementsByClassName('modal-dialog-centered').length,
127 ).toBe(0);
128 });
129
130 it('should render with class "modal-dialog" and centered class if provided', () => {
131 render(
132 <Modal isOpen toggle={toggle} centered>
133 Yo!
134 </Modal>,
135 );
136
137 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
138 expect(
139 document.getElementsByClassName('modal-dialog-centered').length,
140 ).toBe(1);
141 });
142
143 describe('fullscreen', () => {
144 it('should render non fullscreen by default', () => {
145 render(
146 <Modal isOpen toggle={toggle}>
147 Yo!
148 </Modal>,
149 );
150
151 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
152 expect(document.getElementsByClassName('modal-fullscreen').length).toBe(
153 0,
154 );
155 });
156
157 it('should always render fullscreen if true', () => {
158 render(
159 <Modal isOpen toggle={toggle} fullscreen>
160 Yo!
161 </Modal>,
162 );
163
164 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
165 expect(document.getElementsByClassName('modal-fullscreen').length).toBe(
166 1,
167 );
168 });
169
170 it('should render fullscreen below breakpoint if breakpoint is provided', () => {
171 render(
172 <Modal isOpen toggle={toggle} fullscreen="lg">
173 Yo!
174 </Modal>,
175 );
176
177 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
178 expect(document.getElementsByClassName('modal-fullscreen').length).toBe(
179 0,
180 );
181 expect(
182 document.getElementsByClassName('modal-fullscreen-lg-down').length,
183 ).toBe(1);
184 });
185 });
186
187 it('should render with additional props if provided', () => {
188 render(
189 <Modal isOpen toggle={toggle} style={{ maxWidth: '95%' }}>
190 Yo!
191 </Modal>,
192 );
193
194 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
195 expect(
196 document.getElementsByClassName('modal-dialog')[0].style.maxWidth,
197 ).toBe('95%');
198 });
199
200 it('should render without fade transition if provided with fade={false}', () => {
201 render(
202 <Modal
203 isOpen
204 toggle={toggle}
205 fade={false}
206 modalClassName="fadeless-modal"
207 >
208 Howdy!
209 </Modal>,
210 );
211
212 const matchedModals = document.getElementsByClassName('fadeless-modal');
213 const matchedModal = matchedModals[0];
214
215 expect(matchedModals.length).toBe(1);
216 // Modal should not have the 'fade' class
217 expect(matchedModal.className.split(' ').indexOf('fade') < 0).toBe(true);
218 });
219
220 it('should render when expected when passed modalTransition and backdropTransition props', () => {
221 render(
222 <Modal
223 isOpen
224 toggle={toggle}
225 modalTransition={{ timeout: 2 }}
226 backdropTransition={{ timeout: 10 }}
227 modalClassName="custom-timeout-modal"
228 >
229 Hello, world!
230 </Modal>,
231 );
232
233 expect(document.getElementsByClassName('custom-timeout-modal').length).toBe(
234 1,
235 );
236 });
237
238 it('should render with class "modal" and have custom class name if provided with modalClassName', () => {
239 render(
240 <Modal isOpen toggle={toggle} modalClassName="my-custom-modal">
241 Yo!
242 </Modal>,
243 );
244
245 expect(document.querySelectorAll('.modal.my-custom-modal').length).toBe(1);
246 });
247
248 it('should render with custom class name if provided with wrapClassName', () => {
249 render(
250 <Modal isOpen toggle={toggle} wrapClassName="my-custom-modal">
251 Yo!
252 </Modal>,
253 );
254
255 expect(document.getElementsByClassName('my-custom-modal').length).toBe(1);
256 });
257
258 it('should render with class "modal-content" and have custom class name if provided with contentClassName', () => {
259 render(
260 <Modal isOpen toggle={toggle} contentClassName="my-custom-modal">
261 Yo!
262 </Modal>,
263 );
264
265 expect(
266 document.querySelectorAll('.modal-content.my-custom-modal').length,
267 ).toBe(1);
268 });
269
270 it('should render with class "modal-backdrop" and have custom class name if provided with backdropClassName', () => {
271 render(
272 <Modal isOpen toggle={toggle} backdropClassName="my-custom-modal">
273 Yo!
274 </Modal>,
275 );
276
277 expect(
278 document.querySelectorAll('.modal-backdrop.my-custom-modal').length,
279 ).toBe(1);
280 });
281
282 it('should render with the class "modal-${size}" when size is passed', () => {
283 render(
284 <Modal isOpen toggle={toggle} size="crazy">
285 Yo!
286 </Modal>,
287 );
288
289 expect(document.getElementsByClassName('modal-dialog').length).toBe(1);
290 expect(document.getElementsByClassName('modal-crazy').length).toBe(1);
291 });
292
293 it('should render modal when isOpen is true', () => {
294 render(
295 <Modal isOpen toggle={toggle}>
296 Yo!
297 </Modal>,
298 );
299
300 expect(document.getElementsByClassName('modal').length).toBe(1);
301 expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
302 });
303
304 it('should render modal with default role of "dialog"', () => {
305 render(
306 <Modal isOpen toggle={toggle}>
307 Yo!
308 </Modal>,
309 );
310
311 expect(
312 document.getElementsByClassName('modal')[0].getAttribute('role'),
313 ).toBe('dialog');
314 });
315
316 it('should render modal with provided role', () => {
317 render(
318 <Modal isOpen toggle={toggle} role="alert">
319 Yo!
320 </Modal>,
321 );
322
323 expect(
324 document.getElementsByClassName('modal')[0].getAttribute('role'),
325 ).toBe('alert');
326 });
327
328 it('should render modal with aria-labelledby provided labelledBy', () => {
329 render(
330 <Modal isOpen toggle={toggle} labelledBy="myModalTitle">
331 Yo!
332 </Modal>,
333 );
334
335 expect(
336 document
337 .getElementsByClassName('modal')[0]
338 .getAttribute('aria-labelledby'),
339 ).toBe('myModalTitle');
340 });
341
342 it('should not render modal when isOpen is false', () => {
343 render(
344 <Modal isOpen={false} toggle={toggle}>
345 Yo!
346 </Modal>,
347 );
348
349 expect(document.getElementsByClassName('modal').length).toBe(0);
350 expect(document.getElementsByClassName('modal-backdrop').length).toBe(0);
351 });
352
353 it('should toggle modal', () => {
354 const { rerender } = render(
355 <Modal isOpen={false} toggle={toggle}>
356 Yo!
357 </Modal>,
358 );
359
360 expect(document.getElementsByClassName('modal').length).toBe(0);
361 expect(document.getElementsByClassName('modal-backdrop').length).toBe(0);
362
363 rerender(
364 <Modal isOpen toggle={toggle}>
365 Yo!
366 </Modal>,
367 );
368
369 expect(document.getElementsByClassName('modal').length).toBe(1);
370 expect(document.getElementsByClassName('modal-backdrop').length).toBe(1);
371 });
372
373 it('should call onClosed & onOpened', () => {
374 const onOpened = jest.fn();
375 const onClosed = jest.fn();
376 const { rerender } = render(
377 <Modal
378 isOpen={false}
379 onOpened={onOpened}
380 onClosed={onClosed}
381 toggle={toggle}
382 >
383 Yo!
384 </Modal>,
385 );
386
387 expect(onOpened).not.toHaveBeenCalled();
388 expect(onClosed).not.toHaveBeenCalled();
389 rerender(
390 <Modal isOpen onOpened={onOpened} onClosed={onClosed} toggle={toggle}>
391 Yo!
392 </Modal>,
393 );
394 jest.advanceTimersByTime(300);
395 expect(onOpened).toHaveBeenCalledTimes(1);
396
397 rerender(
398 <Modal
399 isOpen={false}
400 onOpened={onOpened}
401 onClosed={onClosed}
402 toggle={toggle}
403 >
404 Yo!
405 </Modal>,
406 );
407
408 jest.advanceTimersByTime(300);
409 expect(onOpened).toHaveBeenCalledTimes(1);
410 expect(onClosed).toHaveBeenCalledTimes(1);
411 });
412
413 it('should call onClosed & onOpened when fade={false}', () => {
414 const onOpened = jest.fn();
415 const onClosed = jest.fn();
416 const { rerender } = render(
417 <Modal
418 isOpen={false}
419 onOpened={onOpened}
420 onClosed={onClosed}
421 toggle={toggle}
422 fade={false}
423 >
424 Yo!
425 </Modal>,
426 );
427
428 expect(onOpened).not.toHaveBeenCalled();
429 expect(onClosed).not.toHaveBeenCalled();
430
431 rerender(
432 <Modal
433 isOpen
434 onOpened={onOpened}
435 onClosed={onClosed}
436 toggle={toggle}
437 fade={false}
438 >
439 Yo!
440 </Modal>,
441 );
442 jest.advanceTimersByTime(1);
443
444 expect(onOpened).toHaveBeenCalledTimes(1);
445 expect(onClosed).not.toHaveBeenCalled();
446
447 rerender(
448 <Modal
449 isOpen={false}
450 onOpened={onOpened}
451 onClosed={onClosed}
452 toggle={toggle}
453 fade={false}
454 >
455 Yo!
456 </Modal>,
457 );
458 jest.advanceTimersByTime(1);
459
460 expect(onClosed).toHaveBeenCalledTimes(1);
461 expect(onOpened).toHaveBeenCalledTimes(1);
462 });
463
464 it('should call toggle when escape key pressed and not for enter key', () => {
465 const toggle = jest.fn();
466 render(
467 <Modal isOpen toggle={toggle}>
468 Yo!
469 </Modal>,
470 );
471
472 user.keyboard('{enter}');
473 expect(toggle).not.toHaveBeenCalled();
474
475 user.keyboard('{esc}');
476 expect(toggle).toHaveBeenCalled();
477 });
478
479 it('should not call toggle when escape key pressed when keyboard is false', () => {
480 const toggle = jest.fn();
481 render(
482 <Modal isOpen toggle={toggle} keyboard={false}>
483 Yo!
484 </Modal>,
485 );
486
487 user.keyboard('{esc}');
488 expect(toggle).not.toHaveBeenCalled();
489 });
490
491 it('should call toggle when clicking backdrop', () => {
492 const toggle = jest.fn();
493 render(
494 <Modal isOpen toggle={toggle}>
495 <button id="clicker">Does Nothing</button>
496 </Modal>,
497 );
498
499 user.click(screen.getByText(/does nothing/i));
500 expect(toggle).not.toHaveBeenCalled();
501
502 user.click(document.body.getElementsByClassName('modal')[0]);
503 expect(toggle).toHaveBeenCalled();
504 });
505
506 it('should not call toggle when clicking backdrop and backdrop is "static"', () => {
507 const toggle = jest.fn();
508 render(
509 <Modal isOpen toggle={toggle} backdrop="static">
510 <button id="clicker">Does Nothing</button>
511 </Modal>,
512 );
513
514 user.click(document.getElementsByClassName('modal-backdrop')[0]);
515 expect(toggle).not.toHaveBeenCalled();
516 });
517
518 it('should not call toggle when escape key pressed and backdrop is "static" and keyboard=false', () => {
519 const toggle = jest.fn();
520 render(
521 <Modal isOpen toggle={toggle} backdrop="static" keyboard={false}>
522 Yo!
523 </Modal>,
524 );
525
526 user.keyboard('{esc}');
527 expect(toggle).not.toHaveBeenCalled();
528 });
529
530 it('should call toggle when escape key pressed and backdrop is "static" and keyboard=true', () => {
531 const toggle = jest.fn();
532 render(
533 <Modal isOpen toggle={toggle} backdrop="static" keyboard>
534 <button id="clicker">Does Nothing</button>
535 </Modal>,
536 );
537
538 user.keyboard('{esc}');
539 expect(toggle).toHaveBeenCalled();
540 });
541
542 it('should animate when backdrop is "static" and escape key pressed and keyboard=false', () => {
543 render(
544 <Modal
545 isOpen
546 toggle={toggle}
547 backdrop="static"
548 keyboard={false}
549 data-testid="mandalorian"
550 >
551 <button id="clicker">Does Nothing</button>
552 </Modal>,
553 );
554
555 user.keyboard('{esc}');
556
557 expect(screen.getByTestId('mandalorian').parentElement).toHaveClass(
558 'modal-static',
559 );
560
561 jest.advanceTimersByTime(300);
562
563 expect(screen.getByTestId('mandalorian').parentElement).not.toHaveClass(
564 'modal-static',
565 );
566 });
567
568 it('should animate when backdrop is "static" and backdrop is clicked', () => {
569 render(
570 <Modal isOpen toggle={toggle} backdrop="static" data-testid="mandalorian">
571 <button id="clicker">Does Nothing</button>
572 </Modal>,
573 );
574
575 user.click(document.getElementsByClassName('modal')[0]);
576
577 expect(screen.getByTestId('mandalorian').parentElement).toHaveClass(
578 'modal-static',
579 );
580
581 jest.advanceTimersByTime(300);
582
583 expect(screen.getByTestId('mandalorian').parentElement).not.toHaveClass(
584 'modal-static',
585 );
586 });
587
588 it('should not animate when backdrop is "static" and modal is clicked', () => {
589 render(
590 <Modal isOpen toggle={toggle} backdrop="static" data-testid="mandalorian">
591 <button id="clicker">Does Nothing</button>
592 </Modal>,
593 );
594
595 user.click(document.getElementsByClassName('modal-dialog')[0]);
596
597 expect(screen.getByTestId('mandalorian').parentElement).not.toHaveClass(
598 'modal-static',
599 );
600 });
601
602 it('should destroy this._element', () => {
603 const { rerender } = render(
604 <Modal isOpen toggle={toggle} wrapClassName="weird-class">
605 <button id="clicker">Does Nothing</button>
606 </Modal>,
607 );
608
609 const element =
610 document.getElementsByClassName('weird-class')[0].parentElement;
611 expect(element).toBeInTheDocument();
612
613 rerender(
614 <Modal isOpen={false} toggle={toggle} wrapClassName="weird-class">
615 <button id="clicker">Does Nothing</button>
616 </Modal>,
617 );
618 jest.advanceTimersByTime(300);
619 expect(element).not.toBeInTheDocument();
620 });
621
622 it('should destroy this._element when unmountOnClose prop set to true', () => {
623 const { rerender } = render(
624 <Modal isOpen toggle={toggle} unmountOnClose wrapClassName="weird-class">
625 <button id="clicker">Does Nothing</button>
626 </Modal>,
627 );
628
629 const element =
630 document.getElementsByClassName('weird-class')[0].parentElement;
631 expect(element).toBeInTheDocument();
632
633 rerender(
634 <Modal
635 isOpen={false}
636 toggle={toggle}
637 unmountOnClose
638 wrapClassName="weird-class"
639 >
640 <button id="clicker">Does Nothing</button>
641 </Modal>,
642 );
643 jest.advanceTimersByTime(300);
644 expect(element).not.toBeInTheDocument();
645 });
646
647 it('should not destroy this._element when unmountOnClose prop set to false', () => {
648 const { rerender } = render(
649 <Modal
650 isOpen
651 toggle={toggle}
652 unmountOnClose={false}
653 wrapClassName="weird-class"
654 >
655 <button id="clicker">Does Nothing</button>
656 </Modal>,
657 );
658
659 const element =
660 document.getElementsByClassName('weird-class')[0].parentElement;
661 expect(element).toBeInTheDocument();
662
663 rerender(
664 <Modal
665 isOpen={false}
666 toggle={toggle}
667 unmountOnClose={false}
668 wrapClassName="weird-class"
669 >
670 <button id="clicker">Does Nothing</button>
671 </Modal>,
672 );
673 expect(element).toBeInTheDocument();
674 });
675
676 it('should destroy this._element on unmount', () => {
677 const { unmount } = render(
678 <Modal isOpen toggle={toggle} wrapClassName="weird-class">
679 <button id="clicker">Does Nothing</button>
680 </Modal>,
681 );
682 unmount();
683 jest.advanceTimersByTime(300);
684 expect(document.getElementsByClassName('modal').length).toBe(0);
685 });
686
687 it('should render nested modals', () => {
688 const { unmount } = render(
689 <Modal isOpen toggle={toggle}>
690 <ModalBody>
691 <Modal isOpen toggle={() => {}}>
692 Yo!
693 </Modal>
694 </ModalBody>
695 </Modal>,
696 );
697
698 expect(document.getElementsByClassName('modal-dialog').length).toBe(2);
699 expect(document.body.className).toBe('modal-open');
700
701 unmount();
702
703 expect(document.getElementsByClassName('modal-dialog').length).toBe(0);
704 });
705
706 it('should remove exactly modal-open class from body', () => {
707 // set a body class which includes modal-open
708 document.body.className = 'my-modal-opened';
709
710 const { rerender } = render(
711 <Modal isOpen={false} toggle={toggle}>
712 Yo!
713 </Modal>,
714 );
715
716 expect(document.body.className).toBe('my-modal-opened');
717
718 rerender(
719 <Modal isOpen toggle={toggle}>
720 Yo!
721 </Modal>,
722 );
723
724 expect(document.body.className).toBe('my-modal-opened modal-open');
725
726 // using this to test if replace will leave a space when removing modal-open
727 document.body.className += ' modal-opened';
728 expect(document.body.className).toBe(
729 'my-modal-opened modal-open modal-opened',
730 );
731
732 rerender(
733 <Modal isOpen={false} toggle={toggle}>
734 Yo!
735 </Modal>,
736 );
737
738 jest.advanceTimersByTime(300);
739 expect(document.body.className).toBe('my-modal-opened modal-opened');
740 });
741
742 it('should call onEnter & onExit props if provided', () => {
743 const onEnter = jest.fn();
744 const onExit = jest.fn();
745 const { rerender, unmount } = render(
746 <Modal isOpen={false} onEnter={onEnter} onExit={onExit} toggle={toggle}>
747 Yo!
748 </Modal>,
749 );
750
751 expect(onEnter).toHaveBeenCalled();
752 expect(onExit).not.toHaveBeenCalled();
753
754 onEnter.mockReset();
755 onExit.mockReset();
756
757 rerender(
758 <Modal isOpen onEnter={onEnter} onExit={onExit} toggle={toggle}>
759 Yo!
760 </Modal>,
761 );
762 expect(onEnter).not.toHaveBeenCalled();
763 expect(onExit).not.toHaveBeenCalled();
764
765 onEnter.mockReset();
766 onExit.mockReset();
767
768 rerender(
769 <Modal isOpen={false} onEnter={onEnter} onExit={onExit} toggle={toggle}>
770 Yo!
771 </Modal>,
772 );
773 unmount();
774 expect(onEnter).not.toHaveBeenCalled();
775 expect(onExit).toHaveBeenCalled();
776 });
777
778 it('should update element z index when prop changes', () => {
779 const { rerender } = render(
780 <Modal isOpen zIndex={0} wrapClassName="sandman">
781 Yo!
782 </Modal>,
783 );
784
785 expect(
786 document.getElementsByClassName('sandman')[0].parentElement.style.zIndex,
787 ).toBe('0');
788
789 rerender(
790 <Modal isOpen zIndex={1} wrapClassName="sandman">
791 Yo!
792 </Modal>,
793 );
794
795 expect(
796 document.getElementsByClassName('sandman')[0].parentElement.style.zIndex,
797 ).toBe('1');
798 });
799
800 it('should allow focus on only focusable elements and tab through them', () => {
801 render(
802 <Modal isOpen toggle={toggle}>
803 <ModalHeader toggle={toggle}>Modal title</ModalHeader>
804 <ModalBody>
805 <a alt="test" href="/">
806 First Test
807 </a>
808 <map name="test">
809 <area alt="test" href="/" coords="200,5,200,30" />
810 </map>
811 <input type="text" aria-label="test text input" />
812 <input type="hidden" />
813 <input type="text" disabled value="Test" />
814 <select name="test" id="select_test">
815 <option>Second item</option>
816 </select>
817 <select name="test" id="select_test_disabled" disabled>
818 <option>Third item</option>
819 </select>
820 <textarea
821 name="textarea_test"
822 id="textarea_test"
823 cols="30"
824 rows="10"
825 aria-label="test text area"
826 />
827 <textarea
828 name="textarea_test_disabled"
829 id="textarea_test_disabled"
830 cols="30"
831 rows="10"
832 disabled
833 />
834 <object>Test</object>
835 <span tabIndex="0">test tab index</span>
836 </ModalBody>
837 <ModalFooter>
838 <Button disabled color="primary" onClick={toggle}>
839 Do Something
840 </Button>{' '}
841 <Button color="secondary" onClick={toggle}>
842 Cancel
843 </Button>
844 </ModalFooter>
845 </Modal>,
846 );
847
848 user.tab();
849 expect(screen.getByLabelText(/close/i)).toHaveFocus();
850 user.tab();
851 expect(screen.getByText(/first test/i)).toHaveFocus();
852 user.tab();
853 expect(screen.getByLabelText(/test text input/i)).toHaveFocus();
854 user.tab();
855 expect(screen.getByText(/second item/i).parentElement).toHaveFocus();
856 user.tab();
857 expect(screen.getByLabelText(/test text area/i)).toHaveFocus();
858 user.tab();
859 expect(screen.getByText(/test tab index/i)).toHaveFocus();
860 user.tab();
861 expect(screen.getByText(/cancel/i)).toHaveFocus();
862 user.tab();
863 expect(screen.getByLabelText(/close/i)).toHaveFocus();
864 });
865
866 it('should return the focus to the last focused element before the modal has opened', () => {
867 const { rerender } = render(
868 <>
869 <button className="focus">Focused</button>
870 <Modal isOpen={false}>
871 <ModalBody>Whatever</ModalBody>
872 </Modal>
873 </>,
874 );
875
876 user.tab();
877 expect(screen.getByText(/focused/i)).toHaveFocus();
878
879 rerender(
880 <>
881 <button className="focus">Focused</button>
882 <Modal isOpen>
883 <ModalBody>Whatever</ModalBody>
884 </Modal>
885 </>,
886 );
887
888 rerender(
889 <>
890 <button className="focus">Focused</button>
891 <Modal isOpen={false}>
892 <ModalBody>Whatever</ModalBody>
893 </Modal>
894 </>,
895 );
896
897 jest.runAllTimers();
898 expect(screen.getByText(/focused/i)).toHaveFocus();
899 });
900
901 it('should not return the focus to the last focused element before the modal has opened when "returnFocusAfterClose" is false', () => {
902 const { rerender } = render(
903 <>
904 <button className="focus">Focused</button>
905 <Modal returnFocusAfterClose={false} isOpen={false}>
906 <ModalBody>Whatever</ModalBody>
907 </Modal>
908 </>,
909 );
910
911 user.tab();
912 expect(screen.getByText(/focused/i)).toHaveFocus();
913
914 rerender(
915 <>
916 <button className="focus">Focused</button>
917 <Modal returnFocusAfterClose={false} isOpen>
918 <ModalBody>Whatever</ModalBody>
919 </Modal>
920 </>,
921 );
922
923 rerender(
924 <>
925 <button className="focus">Focused</button>
926 <Modal returnFocusAfterClose={false} isOpen={false}>
927 <ModalBody>Whatever</ModalBody>
928 </Modal>
929 </>,
930 );
931
932 jest.runAllTimers();
933 expect(screen.getByText(/focused/i)).not.toHaveFocus();
934 });
935
936 it('should return the focus to the last focused element before the modal has opened when "unmountOnClose" is false', () => {
937 const { rerender } = render(
938 <>
939 <button className="focus">Focused</button>
940 <Modal unmountOnClose={false} isOpen={false}>
941 <ModalBody>Whatever</ModalBody>
942 </Modal>
943 </>,
944 );
945
946 user.tab();
947 expect(screen.getByText(/focused/i)).toHaveFocus();
948
949 rerender(
950 <>
951 <button className="focus">Focused</button>
952 <Modal unmountOnClose={false} isOpen>
953 <ModalBody>Whatever</ModalBody>
954 </Modal>
955 </>,
956 );
957
958 rerender(
959 <>
960 <button className="focus">Focused</button>
961 <Modal unmountOnClose={false} isOpen={false}>
962 <ModalBody>Whatever</ModalBody>
963 </Modal>
964 </>,
965 );
966
967 jest.runAllTimers();
968 expect(screen.getByText(/focused/i)).toHaveFocus();
969 });
970
971 it('should not return the focus to the last focused element before the modal has opened when "returnFocusAfterClose" is false and "unmountOnClose" is false', () => {
972 const { rerender } = render(
973 <>
974 <button className="focus">Focused</button>
975 <Modal
976 unmountOnClose={false}
977 returnFocusAfterClose={false}
978 isOpen={false}
979 >
980 <ModalBody>Whatever</ModalBody>
981 </Modal>
982 </>,
983 );
984
985 user.tab();
986 expect(screen.getByText(/focused/i)).toHaveFocus();
987
988 rerender(
989 <>
990 <button className="focus">Focused</button>
991 <Modal unmountOnClose={false} returnFocusAfterClose={false} isOpen>
992 <ModalBody>Whatever</ModalBody>
993 </Modal>
994 </>,
995 );
996
997 rerender(
998 <>
999 <button className="focus">Focused</button>
1000 <Modal
1001 unmountOnClose={false}
1002 returnFocusAfterClose={false}
1003 isOpen={false}
1004 >
1005 <ModalBody>Whatever</ModalBody>
1006 </Modal>
1007 </>,
1008 );
1009
1010 jest.runAllTimers();
1011 expect(screen.getByText(/focused/i)).not.toHaveFocus();
1012 });
1013
1014 it('should attach/detach trapFocus for dialogs', () => {
1015 const addEventListener = jest.spyOn(document, 'addEventListener');
1016 const removeEventListener = jest.spyOn(document, 'removeEventListener');
1017
1018 const { unmount } = render(
1019 <Modal isOpen>
1020 <ModalBody>
1021 <Button className="focus">focusable element</Button>
1022 </ModalBody>
1023 </Modal>,
1024 );
1025
1026 expect(addEventListener).toHaveBeenCalledTimes(1);
1027 expect(addEventListener).toHaveBeenCalledWith(
1028 'focus',
1029 expect.any(Function),
1030 true,
1031 );
1032
1033 unmount();
1034
1035 expect(removeEventListener).toHaveBeenCalledTimes(1);
1036 expect(removeEventListener).toHaveBeenCalledWith(
1037 'focus',
1038 expect.any(Function),
1039 true,
1040 );
1041
1042 addEventListener.mockRestore();
1043 removeEventListener.mockRestore();
1044 });
1045
1046 it('should trap focus inside the open dialog', () => {
1047 const { rerender } = render(
1048 <>
1049 <Button className="first">Focused</Button>
1050 <Modal isOpen={false} trapFocus>
1051 <ModalBody>
1052 Something else to see
1053 <Button className="focus">focusable element</Button>
1054 </ModalBody>
1055 </Modal>
1056 </>,
1057 );
1058
1059 screen.getByText(/focused/i).focus();
1060
1061 expect(screen.getByText(/focused/i)).toHaveFocus();
1062
1063 rerender(
1064 <>
1065 <Button className="first">Focused</Button>
1066 <Modal isOpen trapFocus data-testid="modal">
1067 <ModalBody>
1068 Something else to see
1069 <Button className="focus">focusable element</Button>
1070 </ModalBody>
1071 </Modal>
1072 </>,
1073 );
1074
1075 jest.runAllTimers();
1076 expect(screen.getByText(/focused/i)).not.toHaveFocus();
1077
1078 expect(screen.getByTestId('modal').parentElement).toHaveFocus();
1079 // pressing tab shouldn't move focus outside the modal
1080 user.tab();
1081 expect(screen.getByText(/focusable element/i)).toHaveFocus();
1082 user.tab();
1083 expect(screen.getByText(/focusable element/i)).toHaveFocus();
1084 });
1085
1086 it('tab should focus on inside modal children for nested modal', () => {
1087 render(
1088 <Modal isOpen toggle={toggle}>
1089 <ModalBody>
1090 <Button className="b0" onClick={toggle}>
1091 Cancel
1092 </Button>
1093 <Modal isOpen>
1094 <ModalBody>
1095 <Button className="b1">Click 1</Button>
1096 </ModalBody>
1097 </Modal>
1098 </ModalBody>
1099 </Modal>,
1100 );
1101
1102 user.tab();
1103 expect(screen.getByText(/click 1/i)).toHaveFocus();
1104 // pressing tab doesn't take focus out of inside modal
1105 user.tab();
1106 expect(screen.getByText(/click 1/i)).toHaveFocus();
1107 });
1108
1109 it('works with strict mode', () => {
1110 const spy = jest.spyOn(console, 'error');
1111 render(
1112 <React.StrictMode>
1113 <Modal isOpen>Hello</Modal>
1114 </React.StrictMode>,
1115 );
1116 expect(spy).not.toHaveBeenCalled();
1117 });
1118});