UNPKG

36.8 kBJavaScriptView Raw
1import React from 'react';
2import { createEvent, fireEvent, render, screen } from '@testing-library/react';
3import user from '@testing-library/user-event';
4import '@testing-library/jest-dom';
5import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from '..';
6import { keyCodes } from '../utils';
7import { testForChildrenInComponent } from '../testUtils';
8
9describe('Dropdown', () => {
10 beforeEach(() => {
11 jest.useFakeTimers();
12 });
13
14 afterEach(() => {
15 jest.restoreAllMocks();
16 jest.runOnlyPendingTimers();
17 jest.useRealTimers();
18 });
19
20 function imitateDropdownFocus(toggle) {
21 // this is needed to make the focus on the correct element
22 // by following the default user behaviour
23 // needed in particular for keyboard based tests
24 // setting focus on the toggle element with code
25 // is causing tab to cycle through elements.
26 const { rerender } = render(
27 <Dropdown isOpen={false} toggle={toggle} data-testid="drpdwn">
28 <DropdownToggle>Toggle</DropdownToggle>
29 <DropdownMenu>
30 <DropdownItem>First item</DropdownItem>
31 <DropdownItem>Second item</DropdownItem>
32 <DropdownItem id="divider" divider />
33 </DropdownMenu>
34 </Dropdown>,
35 );
36
37 user.click(screen.getByText('Toggle'));
38 toggle.mockClear();
39
40 rerender(
41 <Dropdown isOpen toggle={toggle} data-testid="drpdwn">
42 <DropdownToggle>Toggle</DropdownToggle>
43 <DropdownMenu>
44 <DropdownItem>First item</DropdownItem>
45 <DropdownItem>Second item</DropdownItem>
46 <DropdownItem id="divider" divider />
47 </DropdownMenu>
48 </Dropdown>,
49 );
50
51 return { rerender };
52 }
53
54 it('should render a single child', () => {
55 render(<Dropdown isOpen>Ello world</Dropdown>);
56
57 expect(screen.getByText(/ello world/i)).toHaveClass('dropdown');
58 });
59
60 it('should render menu when isOpen is true', () => {
61 render(
62 <Dropdown isOpen>
63 <DropdownToggle>Toggle</DropdownToggle>
64 <DropdownMenu>
65 <DropdownItem>Test</DropdownItem>
66 </DropdownMenu>
67 </Dropdown>,
68 );
69
70 expect(screen.getByText(/toggle/i)).toHaveClass('btn');
71 expect(screen.getByText(/test/i)).toHaveClass('dropdown-item');
72 });
73
74 it('should not call props.toggle when disabled ', () => {
75 const toggle = jest.fn();
76 render(
77 <Dropdown isOpen toggle={toggle} disabled>
78 <DropdownToggle>Toggle</DropdownToggle>
79 <DropdownMenu>
80 <DropdownItem>Test</DropdownItem>
81 </DropdownMenu>
82 </Dropdown>,
83 );
84
85 user.click(screen.getByText(/toggle/i));
86 expect(toggle).not.toHaveBeenCalled();
87 });
88
89 it('should call toggle when DropdownToggle is clicked ', () => {
90 const toggle = jest.fn();
91 render(
92 <Dropdown isOpen toggle={toggle}>
93 <DropdownToggle>Toggle</DropdownToggle>
94 <DropdownMenu>
95 <DropdownItem>Test</DropdownItem>
96 </DropdownMenu>
97 </Dropdown>,
98 );
99
100 user.click(screen.getByText(/toggle/i));
101 expect(toggle).toHaveBeenCalledTimes(1);
102 });
103
104 it('should call toggle when DropdownToggle with non string children is clicked ', () => {
105 const toggle = jest.fn();
106 render(
107 <Dropdown isOpen toggle={toggle}>
108 <DropdownToggle>
109 <div>Toggle</div>
110 </DropdownToggle>
111 <DropdownMenu>
112 <DropdownItem>Test</DropdownItem>
113 </DropdownMenu>
114 </Dropdown>,
115 );
116
117 user.click(screen.getByText(/toggle/i));
118 expect(toggle).toHaveBeenCalledTimes(1);
119 });
120
121 describe('handleProps', () => {
122 it('should not pass custom props to html attrs', () => {
123 const toggle = jest.fn();
124 render(
125 <Dropdown a11y isOpen toggle={toggle}>
126 <DropdownToggle>Toggle</DropdownToggle>
127 <DropdownMenu>
128 <DropdownItem>Test</DropdownItem>
129 </DropdownMenu>
130 </Dropdown>,
131 );
132
133 const dropdown = document.getElementsByClassName('dropdown')[0];
134 expect(dropdown).not.toHaveAttribute('inNavbar');
135 expect(dropdown).not.toHaveAttribute('toggle');
136 expect(dropdown).not.toHaveAttribute('a11y');
137 expect(dropdown).not.toHaveAttribute('isOpen');
138 });
139
140 it('should add event listeners when isOpen changed to true', () => {
141 const addEventListener = jest.spyOn(document, 'addEventListener');
142 const { rerender } = render(
143 <Dropdown isOpen={false}>
144 <DropdownToggle>Toggle</DropdownToggle>
145 <DropdownMenu>
146 <DropdownItem>Test</DropdownItem>
147 </DropdownMenu>
148 </Dropdown>,
149 );
150
151 expect(addEventListener).not.toHaveBeenCalled();
152
153 rerender(
154 <Dropdown isOpen>
155 <DropdownToggle>Toggle</DropdownToggle>
156 <DropdownMenu>
157 <DropdownItem>Test</DropdownItem>
158 </DropdownMenu>
159 </Dropdown>,
160 );
161
162 // called three times because we have click, touchstart and keyup
163 expect(addEventListener).toHaveBeenCalledTimes(3);
164 });
165
166 it('should not be called on componentDidUpdate when isOpen did not change', () => {
167 const addEventListener = jest.spyOn(document, 'addEventListener');
168 const { rerender } = render(
169 <Dropdown isOpen>
170 <DropdownToggle>Toggle</DropdownToggle>
171 <DropdownMenu>
172 <DropdownItem>Test</DropdownItem>
173 </DropdownMenu>
174 </Dropdown>,
175 );
176
177 expect(addEventListener).toHaveBeenCalled();
178 addEventListener.mockClear();
179
180 rerender(
181 <Dropdown isOpen size="lg">
182 <DropdownToggle>Toggle</DropdownToggle>
183 <DropdownMenu>
184 <DropdownItem>Test</DropdownItem>
185 </DropdownMenu>
186 </Dropdown>,
187 );
188
189 expect(addEventListener).not.toHaveBeenCalled();
190 });
191 });
192
193 describe('removeEvents', () => {
194 it('should remove event listeners on componentWillUnmount', () => {
195 const removeEventListener = jest.spyOn(document, 'removeEventListener');
196 const { unmount } = render(
197 <Dropdown isOpen>
198 <DropdownToggle>Toggle</DropdownToggle>
199 <DropdownMenu>
200 <DropdownItem>Test</DropdownItem>
201 </DropdownMenu>
202 </Dropdown>,
203 );
204
205 unmount();
206
207 expect(removeEventListener).toHaveBeenCalled();
208 });
209 });
210
211 describe('handleDocumentClick', () => {
212 it('should call toggle on document click', () => {
213 const toggle = jest.fn(() => {});
214
215 render(
216 <Dropdown isOpen toggle={toggle}>
217 <DropdownToggle>Toggle</DropdownToggle>
218 <DropdownMenu>
219 <DropdownItem>Test</DropdownItem>
220 </DropdownMenu>
221 </Dropdown>,
222 );
223
224 user.click(document.body);
225
226 expect(toggle).toHaveBeenCalled();
227 });
228
229 it('should call toggle on container click', () => {
230 const toggle = jest.fn();
231
232 render(
233 <Dropdown isOpen toggle={toggle} data-testid="dropdown">
234 <DropdownToggle>Toggle</DropdownToggle>
235 <DropdownMenu>
236 <DropdownItem>Test</DropdownItem>
237 </DropdownMenu>
238 </Dropdown>,
239 );
240
241 user.click(screen.getByTestId('dropdown'));
242
243 expect(toggle).toHaveBeenCalled();
244 });
245
246 it('should call toggle on container click', () => {
247 const toggle = jest.fn();
248
249 render(
250 <Dropdown isOpen toggle={toggle} data-testid="dropdown">
251 <DropdownToggle>
252 <div>Toggle</div>
253 </DropdownToggle>
254 <DropdownMenu>
255 <DropdownItem>Test</DropdownItem>
256 </DropdownMenu>
257 </Dropdown>,
258 );
259
260 user.click(screen.getByTestId('dropdown'));
261
262 expect(toggle).toHaveBeenCalled();
263 });
264
265 it('should not call toggle on inner container click', () => {
266 const toggle = jest.fn();
267 render(
268 <Dropdown isOpen toggle={toggle}>
269 <DropdownToggle>Toggle</DropdownToggle>
270 <DropdownMenu>
271 <DropdownItem>Test</DropdownItem>
272 <DropdownItem id="divider" divider />
273 </DropdownMenu>
274 </Dropdown>,
275 );
276
277 user.click(document.getElementById('divider'));
278
279 expect(toggle).not.toHaveBeenCalled();
280 });
281
282 it('should not call toggle when right-clicked', () => {
283 const toggle = jest.fn();
284
285 render(
286 <Dropdown isOpen toggle={toggle} data-testid="dropdown">
287 <DropdownToggle>Toggle</DropdownToggle>
288 <DropdownMenu>
289 <DropdownItem>Test</DropdownItem>
290 <DropdownItem id="divider" divider />
291 </DropdownMenu>
292 </Dropdown>,
293 );
294
295 user.click(screen.getByTestId('dropdown'), { button: 2 });
296
297 expect(toggle).not.toHaveBeenCalled();
298 });
299
300 it('should go through first dropdown item and close when tab is pressed multiple times', async () => {
301 const toggle = jest.fn();
302
303 imitateDropdownFocus(toggle);
304
305 user.tab();
306 expect(screen.getByText(/first item/i)).toHaveFocus();
307
308 user.tab();
309 expect(toggle).toHaveBeenCalledTimes(1);
310 });
311 });
312
313 describe('keyboard events', () => {
314 it('should call toggle on ESC keydown when it isOpen is true', () => {
315 const toggle = jest.fn();
316
317 imitateDropdownFocus(toggle);
318
319 user.keyboard('{esc}');
320
321 expect(toggle).toHaveBeenCalledTimes(1);
322 });
323
324 it('should call toggle on down arrow keydown when it isOpen is false', () => {
325 const toggle = jest.fn();
326 render(
327 <Dropdown isOpen={false} toggle={toggle}>
328 <DropdownToggle>Toggle</DropdownToggle>
329 <DropdownMenu>
330 <DropdownItem>Test</DropdownItem>
331 <DropdownItem id="divider" divider />
332 </DropdownMenu>
333 </Dropdown>,
334 );
335
336 user.tab();
337 expect(screen.getByText('Toggle')).toHaveFocus();
338
339 user.keyboard('{arrowdown}');
340 expect(toggle).toHaveBeenCalledTimes(1);
341 });
342
343 it('should call toggle on up arrow keydown when it isOpen is false', () => {
344 const toggle = jest.fn();
345 render(
346 <Dropdown isOpen={false} toggle={toggle}>
347 <DropdownToggle>Toggle</DropdownToggle>
348 <DropdownMenu>
349 <DropdownItem>Test</DropdownItem>
350 <DropdownItem id="divider" divider />
351 </DropdownMenu>
352 </Dropdown>,
353 );
354
355 user.tab();
356 expect(screen.getByText('Toggle')).toHaveFocus();
357
358 user.keyboard('{arrowup}');
359 expect(toggle).toHaveBeenCalledTimes(1);
360 });
361
362 it('should focus the first menuitem when toggle is triggered by enter keydown', () => {
363 const toggle = jest.fn();
364 const focus = jest.fn();
365 render(
366 <Dropdown isOpen={false} toggle={toggle}>
367 <DropdownToggle>Toggle</DropdownToggle>
368 <DropdownMenu>
369 <DropdownItem header>Header</DropdownItem>
370 <DropdownItem disabled>Disabled</DropdownItem>
371 <DropdownItem onFocus={focus}>Test</DropdownItem>
372 <DropdownItem divider />
373 <DropdownItem>Another Test</DropdownItem>
374 </DropdownMenu>
375 </Dropdown>,
376 );
377
378 user.tab();
379 expect(screen.getByText('Toggle')).toHaveFocus();
380
381 expect(focus).not.toHaveBeenCalled();
382
383 user.keyboard('{enter}');
384 expect(toggle).toHaveBeenCalled();
385
386 jest.runAllTimers();
387 expect(focus).toHaveBeenCalled();
388 });
389
390 it('should focus the first menuitem when toggle is triggered by up arrow keydown', () => {
391 const toggle = jest.fn();
392 const focus = jest.fn();
393 render(
394 <Dropdown isOpen={false} toggle={toggle}>
395 <DropdownToggle>Toggle</DropdownToggle>
396 <DropdownMenu>
397 <DropdownItem header>Header</DropdownItem>
398 <DropdownItem disabled>Disabled</DropdownItem>
399 <DropdownItem onFocus={focus}>Test</DropdownItem>
400 <DropdownItem divider />
401 <DropdownItem>Another Test</DropdownItem>
402 </DropdownMenu>
403 </Dropdown>,
404 );
405
406 user.tab();
407 expect(screen.getByText('Toggle')).toHaveFocus();
408
409 expect(focus).not.toHaveBeenCalled();
410
411 user.keyboard('{arrowdown}');
412 expect(toggle).toHaveBeenCalled();
413
414 jest.runAllTimers();
415 expect(focus).toHaveBeenCalled();
416 });
417
418 it('should focus the first menuitem when toggle is triggered by down arrow keydown', () => {
419 const toggle = jest.fn();
420 const focus = jest.fn();
421 render(
422 <Dropdown isOpen={false} toggle={toggle}>
423 <DropdownToggle>Toggle</DropdownToggle>
424 <DropdownMenu>
425 <DropdownItem header>Header</DropdownItem>
426 <DropdownItem disabled>Disabled</DropdownItem>
427 <DropdownItem onFocus={focus}>Test</DropdownItem>
428 <DropdownItem divider />
429 <DropdownItem>Another Test</DropdownItem>
430 </DropdownMenu>
431 </Dropdown>,
432 );
433
434 user.tab();
435 expect(screen.getByText('Toggle')).toHaveFocus();
436
437 expect(focus).not.toHaveBeenCalled();
438
439 user.keyboard('{arrowup}');
440 expect(toggle).toHaveBeenCalled();
441
442 jest.runAllTimers();
443 expect(focus).toHaveBeenCalled();
444 expect(screen.getByText('Test')).toHaveFocus();
445 });
446
447 it('should focus the next menuitem on down arrow keydown when isOpen is true', () => {
448 const toggle = jest.fn();
449 const focus = jest.fn();
450 const focus2 = jest.fn();
451 const { rerender } = render(
452 <Dropdown isOpen={false} toggle={toggle}>
453 <DropdownToggle>Toggle</DropdownToggle>
454 <DropdownMenu>
455 <DropdownItem header>Header</DropdownItem>
456 <DropdownItem disabled>Disabled</DropdownItem>
457 <DropdownItem onFocus={focus}>Test</DropdownItem>
458 <DropdownItem>i am focused</DropdownItem>
459 <DropdownItem divider />
460 <DropdownItem>Another Test</DropdownItem>
461 </DropdownMenu>
462 </Dropdown>,
463 );
464
465 user.tab();
466 expect(screen.getByText('Toggle')).toHaveFocus();
467 expect(focus).not.toHaveBeenCalled();
468
469 user.keyboard('{arrowup}');
470 expect(toggle).toHaveBeenCalled();
471
472 rerender(
473 <Dropdown isOpen toggle={toggle}>
474 <DropdownToggle>Toggle</DropdownToggle>
475 <DropdownMenu>
476 <DropdownItem header>Header</DropdownItem>
477 <DropdownItem disabled>Disabled</DropdownItem>
478 <DropdownItem onFocus={focus}>Test</DropdownItem>
479 <DropdownItem onFocus={focus2}>i am focused</DropdownItem>
480 <DropdownItem divider />
481 <DropdownItem>Another Test</DropdownItem>
482 </DropdownMenu>
483 </Dropdown>,
484 );
485
486 jest.runAllTimers();
487 expect(focus).toHaveBeenCalled();
488 expect(screen.getByText('Test')).toHaveFocus();
489
490 user.keyboard('{arrowdown}');
491 expect(focus2).toHaveBeenCalled();
492 expect(screen.getByText('i am focused')).toHaveFocus();
493 });
494
495 it('should focus the next menuitem on ctrl + n keydown when isOpen is true', () => {
496 const focus1 = jest.fn();
497 const focus2 = jest.fn();
498 const toggle = jest.fn();
499
500 render(
501 <Dropdown isOpen toggle={toggle}>
502 <DropdownToggle>Toggle</DropdownToggle>
503 <DropdownMenu>
504 <DropdownItem id="first" onFocus={focus1}>
505 Test1
506 </DropdownItem>
507 <DropdownItem id="divider" divider />
508 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
509 </DropdownMenu>
510 </Dropdown>,
511 );
512
513 screen.getByText('Test1').focus();
514 expect(screen.getByText('Test1')).toHaveFocus();
515 user.keyboard('{ctrl>}N');
516 expect(screen.getByText('Test2')).toHaveFocus();
517 });
518
519 it('should focus the first menu item matching the character pressed when isOpen is true', () => {
520 const focus1 = jest.fn();
521 const focus2 = jest.fn();
522 const focus3 = jest.fn();
523
524 render(
525 <Dropdown isOpen>
526 <DropdownToggle>Toggle</DropdownToggle>
527 <DropdownMenu end>
528 <DropdownItem id="first" onFocus={focus1}>
529 Reactstrap
530 </DropdownItem>
531 <DropdownItem onFocus={focus2}>4</DropdownItem>
532 <DropdownItem id="divider" divider />
533 <DropdownItem onFocus={focus3}> Lyfe</DropdownItem>
534 </DropdownMenu>
535 </Dropdown>,
536 );
537
538 user.tab();
539 user.tab();
540 expect(screen.getByText('Reactstrap')).toHaveFocus();
541
542 focus1.mockClear();
543
544 user.keyboard('4');
545 expect(screen.getByText('4')).toHaveFocus();
546
547 expect(focus1.mock.calls.length).toBe(0);
548 expect(focus2.mock.calls.length).toBe(1);
549 expect(focus3.mock.calls.length).toBe(0);
550 });
551
552 it('should skip non-menu items focus the next menu item on down arrow keydown when it isOpen is true and anther item is focused', () => {
553 const focus1 = jest.fn();
554 const focus2 = jest.fn();
555 const toggle = jest.fn();
556
557 render(
558 <Dropdown isOpen toggle={toggle}>
559 <DropdownToggle>Toggle</DropdownToggle>
560 <DropdownMenu end>
561 <DropdownItem id="first" onFocus={focus1}>
562 Test1
563 </DropdownItem>
564 <DropdownItem id="divider" divider />
565 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
566 </DropdownMenu>
567 </Dropdown>,
568 );
569
570 user.tab();
571 user.tab();
572 expect(screen.getByText('Test1')).toHaveFocus();
573 user.keyboard('{arrowdown}');
574 expect(screen.getByText('Test2')).toHaveFocus();
575 expect(toggle).not.toHaveBeenCalled();
576 expect(focus1).toBeCalledTimes(1);
577 expect(focus2).toBeCalledTimes(1);
578 });
579
580 it('should focus the previous menu item on up arrow keydown when isOpen is true and another item is focused', () => {
581 const focus1 = jest.fn();
582 const focus2 = jest.fn();
583 const toggle = jest.fn();
584
585 render(
586 <Dropdown isOpen toggle={toggle}>
587 <DropdownToggle>Toggle</DropdownToggle>
588 <DropdownMenu end>
589 <DropdownItem id="first" onFocus={focus1}>
590 Test1
591 </DropdownItem>
592 <DropdownItem id="divider" divider />
593 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
594 </DropdownMenu>
595 </Dropdown>,
596 );
597
598 user.tab();
599 user.tab();
600 expect(screen.getByText('Test1')).toHaveFocus();
601 user.keyboard('{arrowdown}');
602 expect(screen.getByText('Test2')).toHaveFocus();
603 expect(toggle).not.toHaveBeenCalled();
604 expect(focus1).toBeCalledTimes(1);
605 expect(focus2).toBeCalledTimes(1);
606 user.keyboard('{arrowup}');
607 expect(screen.getByText('Test1')).toHaveFocus();
608 expect(toggle).not.toHaveBeenCalled();
609 expect(focus1).toBeCalledTimes(2);
610 });
611
612 it('should focus the previous menuitem on ctrl + p keydown when isOpen is true and another item is focused', () => {
613 const focus1 = jest.fn();
614 const focus2 = jest.fn();
615 const toggle = jest.fn();
616
617 render(
618 <Dropdown isOpen toggle={toggle}>
619 <DropdownToggle>Toggle</DropdownToggle>
620 <DropdownMenu>
621 <DropdownItem id="first" onFocus={focus1}>
622 Test1
623 </DropdownItem>
624 <DropdownItem id="divider" divider />
625 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
626 </DropdownMenu>
627 </Dropdown>,
628 );
629
630 screen.getByText('Test1').focus();
631 expect(screen.getByText('Test1')).toHaveFocus();
632 user.keyboard('{arrowdown}');
633 expect(toggle).not.toHaveBeenCalled();
634 expect(focus1).toBeCalledTimes(1);
635 expect(focus2).toBeCalledTimes(1);
636 expect(screen.getByText('Test2')).toHaveFocus();
637 user.keyboard('{ctrl>}P');
638 expect(screen.getByText('Test1')).toHaveFocus();
639 });
640
641 it('should wrap focus with down arrow keydown', () => {
642 const focus1 = jest.fn();
643 const focus2 = jest.fn();
644 const toggle = jest.fn();
645
646 render(
647 <Dropdown isOpen toggle={toggle}>
648 <DropdownToggle>Toggle</DropdownToggle>
649 <DropdownMenu end>
650 <DropdownItem id="first" onFocus={focus1}>
651 Test1
652 </DropdownItem>
653 <DropdownItem id="divider" divider />
654 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
655 </DropdownMenu>
656 </Dropdown>,
657 );
658
659 user.tab();
660 user.tab();
661 expect(screen.getByText('Test1')).toHaveFocus();
662 user.keyboard('{arrowdown}');
663 expect(screen.getByText('Test2')).toHaveFocus();
664 expect(toggle).not.toHaveBeenCalled();
665 expect(focus1).toBeCalledTimes(1);
666 expect(focus2).toBeCalledTimes(1);
667 user.keyboard('{arrowdown}');
668 expect(screen.getByText('Test1')).toHaveFocus();
669 expect(toggle).not.toHaveBeenCalled();
670 expect(focus1).toBeCalledTimes(2);
671 });
672
673 it('should wrap focus with up arrow keydown', () => {
674 const focus1 = jest.fn();
675 const focus2 = jest.fn();
676 const toggle = jest.fn();
677
678 render(
679 <Dropdown isOpen toggle={toggle}>
680 <DropdownToggle>Toggle</DropdownToggle>
681 <DropdownMenu end>
682 <DropdownItem id="first" onFocus={focus1}>
683 Test1
684 </DropdownItem>
685 <DropdownItem id="divider" divider />
686 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
687 </DropdownMenu>
688 </Dropdown>,
689 );
690
691 user.tab();
692 user.tab();
693 expect(screen.getByText('Test1')).toHaveFocus();
694 user.keyboard('{arrowup}');
695 expect(screen.getByText('Test2')).toHaveFocus();
696 expect(toggle).not.toHaveBeenCalled();
697 expect(focus1).toBeCalledTimes(1);
698 expect(focus2).toBeCalledTimes(1);
699 });
700
701 it('should focus the 1st item on home key keyDown', () => {
702 const focus1 = jest.fn();
703 const focus2 = jest.fn();
704 const focus3 = jest.fn();
705 const toggle = jest.fn();
706
707 render(
708 <Dropdown isOpen toggle={toggle}>
709 <DropdownToggle>Toggle</DropdownToggle>
710 <DropdownMenu end>
711 <DropdownItem id="first" onFocus={focus1}>
712 Test1
713 </DropdownItem>
714 <DropdownItem id="divider" divider />
715 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
716 <DropdownItem onFocus={focus3}>Test3</DropdownItem>
717 </DropdownMenu>
718 </Dropdown>,
719 );
720
721 user.tab();
722 user.tab();
723 expect(screen.getByText('Test1')).toHaveFocus();
724 user.keyboard('{arrowdown}');
725 user.keyboard('{arrowdown}');
726 expect(screen.getByText('Test3')).toHaveFocus();
727 expect(toggle).not.toHaveBeenCalled();
728 expect(focus1).toBeCalledTimes(1);
729 expect(focus2).toBeCalledTimes(1);
730 expect(focus3).toBeCalledTimes(1);
731 user.keyboard('{home}');
732 expect(screen.getByText('Test1')).toHaveFocus();
733 expect(toggle).not.toHaveBeenCalled();
734 expect(focus1).toBeCalledTimes(2);
735 });
736
737 it('should focus the last item on end key keyDown', () => {
738 const focus1 = jest.fn();
739 const focus2 = jest.fn();
740 const focus3 = jest.fn();
741 const toggle = jest.fn();
742
743 render(
744 <Dropdown isOpen toggle={toggle}>
745 <DropdownToggle>Toggle</DropdownToggle>
746 <DropdownMenu end>
747 <DropdownItem id="first" onFocus={focus1}>
748 Test1
749 </DropdownItem>
750 <DropdownItem id="divider" divider />
751 <DropdownItem onFocus={focus2}>Test2</DropdownItem>
752 <DropdownItem onFocus={focus3}>Test3</DropdownItem>
753 </DropdownMenu>
754 </Dropdown>,
755 );
756
757 user.tab();
758 user.tab();
759 expect(screen.getByText('Test1')).toHaveFocus();
760 user.keyboard('{end}');
761 expect(screen.getByText('Test3')).toHaveFocus();
762 expect(toggle).not.toHaveBeenCalled();
763 expect(focus1).toBeCalledTimes(1);
764 expect(focus2).toBeCalledTimes(0);
765 expect(focus3).toBeCalledTimes(1);
766 });
767
768 it('should trigger a click on links when an item is focused and space[bar] it pressed', () => {
769 const click = jest.fn();
770 const toggle = jest.fn();
771
772 render(
773 <Dropdown isOpen toggle={toggle}>
774 <DropdownToggle>Toggle</DropdownToggle>
775 <DropdownMenu end>
776 <DropdownItem href="#" id="first" onClick={click}>
777 Test1
778 </DropdownItem>
779 <DropdownItem id="second">Test</DropdownItem>
780 <DropdownItem id="divider" divider />
781 <DropdownItem id="third">Test</DropdownItem>
782 </DropdownMenu>
783 </Dropdown>,
784 );
785
786 user.tab();
787 user.tab();
788 expect(screen.getByText('Test1')).toHaveFocus();
789
790 user.keyboard('{space}');
791
792 expect(click).toHaveBeenCalled();
793 });
794
795 it('should trigger a click on buttons when an item is focused and space[bar] it pressed (override browser defaults for focus management)', () => {
796 const toggle = jest.fn();
797 const click = jest.fn();
798
799 render(
800 <Dropdown isOpen toggle={toggle}>
801 <DropdownToggle>Toggle</DropdownToggle>
802 <DropdownMenu>
803 <DropdownItem id="first" onClick={click}>
804 Test1
805 </DropdownItem>
806 <DropdownItem id="second">Test</DropdownItem>
807 <DropdownItem id="divider" divider />
808 <DropdownItem id="third">Test</DropdownItem>
809 </DropdownMenu>
810 </Dropdown>,
811 );
812
813 screen.getByText('Test1').focus();
814 expect(toggle).not.toHaveBeenCalled();
815 expect(screen.getByText('Test1')).toHaveFocus();
816
817 user.keyboard('{space}');
818
819 expect(toggle).toHaveBeenCalledTimes(1);
820 expect(click).toHaveBeenCalledTimes(1);
821 });
822
823 it('should not trigger anything when within an input', () => {
824 const click = jest.fn();
825 const focus = jest.fn();
826 const toggle = jest.fn();
827
828 render(
829 <Dropdown isOpen toggle={toggle}>
830 <DropdownToggle>Toggle</DropdownToggle>
831 <DropdownMenu>
832 <DropdownItem tag="div" id="first" onClick={click} onFocus={focus}>
833 <input id="input" placeholder="name" />
834 </DropdownItem>
835 <DropdownItem id="second">Test</DropdownItem>
836 <DropdownItem id="divider" divider />
837 <DropdownItem id="third">Test</DropdownItem>
838 </DropdownMenu>
839 </Dropdown>,
840 );
841 screen.getByPlaceholderText('name').focus();
842 expect(screen.getByPlaceholderText(/name/i)).toHaveFocus();
843
844 focus.mockClear();
845 click.mockClear();
846
847 user.keyboard('{arrowdown}');
848 user.keyboard('{arrowup}');
849 user.keyboard('{space}');
850
851 expect(toggle).not.toHaveBeenCalled();
852
853 expect(screen.getByPlaceholderText(/name/i)).toHaveFocus();
854
855 expect(focus).not.toHaveBeenCalled();
856 expect(click).not.toHaveBeenCalled();
857 });
858
859 it('should not trigger anything when within a textarea', () => {
860 const click = jest.fn();
861 const focus = jest.fn();
862 const toggle = jest.fn();
863
864 render(
865 <Dropdown isOpen toggle={toggle}>
866 <DropdownToggle>Toggle</DropdownToggle>
867 <DropdownMenu>
868 <DropdownItem tag="div" id="first" onClick={click} onFocus={focus}>
869 <textarea id="input" placeholder="placeholder" />
870 </DropdownItem>
871 <DropdownItem id="second">Test</DropdownItem>
872 <DropdownItem id="divider" divider />
873 <DropdownItem id="third">Test</DropdownItem>
874 </DropdownMenu>
875 </Dropdown>,
876 );
877
878 screen.getByPlaceholderText(/placeholder/i).focus();
879 expect(screen.getByPlaceholderText(/placeholder/i)).toHaveFocus();
880
881 focus.mockClear();
882 click.mockClear();
883
884 user.keyboard('{arrowdown}');
885 user.keyboard('{arrowup}');
886 user.keyboard('{space}');
887
888 expect(toggle).not.toHaveBeenCalled();
889
890 expect(screen.getByPlaceholderText(/placeholder/i)).toHaveFocus();
891
892 expect(focus).not.toHaveBeenCalled();
893 expect(click).not.toHaveBeenCalled();
894 });
895
896 it('should toggle when isOpen is true and tab keyDown on menuitem', () => {
897 const toggle = jest.fn();
898 const focus = jest.fn();
899
900 render(
901 <Dropdown isOpen toggle={toggle}>
902 <DropdownToggle>Toggle</DropdownToggle>
903 <DropdownMenu>
904 <DropdownItem id="first">First</DropdownItem>
905 <DropdownItem id="second" onFocus={focus}>
906 Second
907 </DropdownItem>
908 </DropdownMenu>
909 </Dropdown>,
910 );
911
912 screen.getByText(/first/i).focus();
913
914 user.tab();
915
916 expect(toggle).toHaveBeenCalledTimes(1);
917 });
918
919 it('should not trigger anything when disabled', () => {
920 const toggle = jest.fn();
921 const click = jest.fn();
922 const focus = jest.fn();
923
924 render(
925 <Dropdown isOpen toggle={toggle} disabled>
926 <DropdownToggle>Toggle</DropdownToggle>
927 <DropdownMenu>
928 <DropdownItem tag="div" id="first" onClick={click} onFocus={focus}>
929 Test1
930 </DropdownItem>
931 <DropdownItem id="second">Test</DropdownItem>
932 <DropdownItem id="divider" divider />
933 <DropdownItem id="third">Test</DropdownItem>
934 </DropdownMenu>
935 </Dropdown>,
936 );
937
938 screen.getByText(/test1/i).focus();
939
940 focus.mockClear();
941
942 user.keyboard('{arrowdown}');
943 user.keyboard('{arrowup}');
944 user.keyboard('{space}');
945
946 expect(toggle).not.toHaveBeenCalled();
947 expect(click).not.toHaveBeenCalled();
948 expect(focus).not.toHaveBeenCalled();
949 });
950
951 it('should not focus anything when all items disabled', () => {
952 const toggle = jest.fn();
953 const click = jest.fn();
954 const focus = jest.fn();
955
956 render(
957 <Dropdown isOpen toggle={toggle}>
958 <DropdownToggle>Toggle</DropdownToggle>
959 <DropdownMenu>
960 <DropdownItem
961 disabled
962 tag="div"
963 id="first"
964 onClick={click}
965 onFocus={focus}
966 >
967 Test
968 </DropdownItem>
969 <DropdownItem disabled id="second">
970 Test
971 </DropdownItem>
972 <DropdownItem id="divider" divider />
973 <DropdownItem disabled id="third">
974 Test
975 </DropdownItem>
976 </DropdownMenu>
977 </Dropdown>,
978 );
979
980 screen.getByText(/toggle/i).focus();
981
982 user.keyboard('{arrowdown}');
983 user.keyboard('{arrowup}');
984 user.keyboard('{space}');
985
986 expect(toggle).not.toHaveBeenCalled();
987 expect(click).not.toHaveBeenCalled();
988 expect(focus).not.toHaveBeenCalled();
989 });
990
991 it('should not call preventDefault when dropdown has focus and f5 key is pressed', () => {
992 const toggle = jest.fn();
993
994 render(
995 <Dropdown isOpen={false} toggle={toggle}>
996 <DropdownToggle>Toggle</DropdownToggle>
997 <DropdownMenu>
998 <DropdownItem>Test</DropdownItem>
999 <DropdownItem id="divider" divider />
1000 </DropdownMenu>
1001 </Dropdown>,
1002 );
1003
1004 expect(toggle).not.toHaveBeenCalled();
1005
1006 const button = screen.getByText(/toggle/i);
1007
1008 const keyEvent1 = createEvent.keyDown(button, {
1009 keyCode: 116,
1010 });
1011 fireEvent(button, keyEvent1);
1012 expect(keyEvent1.defaultPrevented).toBe(false);
1013
1014 const keyEvent2 = createEvent.keyDown(button, {
1015 keyCode: 16,
1016 });
1017 fireEvent(button, keyEvent2);
1018 expect(keyEvent2.defaultPrevented).toBe(false);
1019 });
1020
1021 it('should call preventDefault when dropdown has focus and any key(up, down, esc, enter, home, end or any alphanumeric key) is pressed', () => {
1022 const toggle = jest.fn();
1023
1024 render(
1025 <Dropdown isOpen={false} toggle={toggle}>
1026 <DropdownToggle>Toggle</DropdownToggle>
1027 <DropdownMenu>
1028 <DropdownItem>Test</DropdownItem>
1029 <DropdownItem id="divider" divider />
1030 </DropdownMenu>
1031 </Dropdown>,
1032 );
1033
1034 expect(toggle).not.toHaveBeenCalled();
1035
1036 const button = screen.getByText(/toggle/i);
1037 [
1038 keyCodes.down,
1039 keyCodes.up,
1040 keyCodes.end,
1041 keyCodes.home,
1042 keyCodes.enter,
1043 90, // for 'a'
1044 65, // for 'A'
1045 ].forEach((keyCode) => {
1046 const keyEvent = createEvent.keyDown(button, {
1047 keyCode,
1048 });
1049 fireEvent(button, keyEvent);
1050 expect(keyEvent.defaultPrevented).toBe(true);
1051 });
1052 });
1053 });
1054
1055 it('should render different size classes', () => {
1056 const { rerender } = render(
1057 <Dropdown group isOpen size="sm">
1058 <DropdownToggle>Toggle</DropdownToggle>
1059 <DropdownMenu>
1060 <DropdownItem>Test</DropdownItem>
1061 </DropdownMenu>
1062 </Dropdown>,
1063 );
1064
1065 expect(screen.getByText(/toggle/i).parentElement).toHaveClass(
1066 'btn-group-sm',
1067 );
1068
1069 rerender(
1070 <Dropdown group isOpen size="lg">
1071 <DropdownToggle>Toggle</DropdownToggle>
1072 <DropdownMenu>
1073 <DropdownItem>Test</DropdownItem>
1074 </DropdownMenu>
1075 </Dropdown>,
1076 );
1077
1078 expect(screen.getByText(/toggle/i).parentElement).toHaveClass(
1079 'btn-group-lg',
1080 );
1081 });
1082
1083 describe('Dropdown with nav', () => {
1084 it('should render a single child', () => {
1085 testForChildrenInComponent(Dropdown);
1086 });
1087
1088 it('should render multiple children when isOpen', () => {
1089 render(
1090 <Dropdown nav isOpen>
1091 <DropdownToggle>Toggle</DropdownToggle>
1092 <DropdownMenu>
1093 <DropdownItem>Test</DropdownItem>
1094 </DropdownMenu>
1095 </Dropdown>,
1096 );
1097
1098 expect(screen.getByText(/test/i)).toBeInTheDocument();
1099 expect(screen.getByText(/toggle/i)).toBeInTheDocument();
1100 });
1101 });
1102
1103 describe('Dropdown in navbar', () => {
1104 it('should open without popper with inNavbar prop', () => {
1105 render(
1106 <Dropdown nav inNavbar>
1107 <DropdownToggle caret nav>
1108 Toggle
1109 </DropdownToggle>
1110 <DropdownMenu>
1111 <DropdownItem>Test</DropdownItem>
1112 </DropdownMenu>
1113 </Dropdown>,
1114 );
1115
1116 expect(screen.getByText(/toggle/i).tagName).toBe('A');
1117 expect(screen.getByText(/test/i).parentElement.tagName).toBe('DIV');
1118 });
1119 });
1120
1121 describe('active', () => {
1122 it('should render an active class', () => {
1123 render(<Dropdown active nav />);
1124
1125 expect(screen.getByRole('listitem')).toHaveClass('active');
1126 });
1127
1128 it('should render an active class when a child DropdownItem is active IF setActiveFromChild is true', () => {
1129 render(
1130 <Dropdown nav inNavbar setActiveFromChild>
1131 <DropdownToggle nav caret>
1132 Options
1133 </DropdownToggle>
1134 <DropdownMenu>
1135 <DropdownItem active>Test</DropdownItem>
1136 </DropdownMenu>
1137 </Dropdown>,
1138 );
1139
1140 expect(screen.getByRole('listitem')).toHaveClass('active');
1141 });
1142 });
1143
1144 it('should render with correct class when direction is set', () => {
1145 const { rerender } = render(<Dropdown direction="up" nav />);
1146 expect(screen.getByRole('listitem')).toHaveClass('dropup');
1147 rerender(<Dropdown direction="start" nav />);
1148 expect(screen.getByRole('listitem')).toHaveClass('dropstart');
1149 rerender(<Dropdown direction="end" nav />);
1150 expect(screen.getByRole('listitem')).toHaveClass('dropend');
1151 });
1152
1153 describe('menuRole prop', () => {
1154 it('should set correct roles for children when menuRole is menu', () => {
1155 render(
1156 <Dropdown menuRole="menu" isOpen>
1157 <DropdownToggle nav caret>
1158 Options
1159 </DropdownToggle>
1160 <DropdownMenu>
1161 <DropdownItem active>Test</DropdownItem>
1162 </DropdownMenu>
1163 </Dropdown>,
1164 );
1165
1166 expect(screen.getByText(/options/i)).toHaveAttribute(
1167 'aria-haspopup',
1168 'menu',
1169 );
1170 expect(screen.getByRole('menu')).toBeInTheDocument();
1171 expect(screen.getByRole('menuitem')).toBeInTheDocument();
1172 });
1173
1174 it('should set correct roles for children when menuRole is menu', () => {
1175 render(
1176 <Dropdown menuRole="listbox" isOpen>
1177 <DropdownToggle nav caret>
1178 Options
1179 </DropdownToggle>
1180 <DropdownMenu>
1181 <DropdownItem active>Test</DropdownItem>
1182 </DropdownMenu>
1183 </Dropdown>,
1184 );
1185
1186 expect(screen.getByText(/options/i)).toHaveAttribute(
1187 'aria-haspopup',
1188 'listbox',
1189 );
1190 expect(screen.getByRole('option')).toBeInTheDocument();
1191 expect(screen.getByRole('listbox')).toBeInTheDocument();
1192 });
1193 });
1194});