@khanacademy/wonder-blocks-dropdown 2.7.3 → 2.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/es/index.js +92 -162
  3. package/dist/index.js +285 -374
  4. package/package.json +6 -6
  5. package/src/components/__docs__/action-menu.argtypes.js +44 -0
  6. package/src/components/__docs__/action-menu.stories.js +435 -0
  7. package/src/components/__docs__/base-select.argtypes.js +54 -0
  8. package/src/components/__docs__/multi-select.stories.js +509 -0
  9. package/src/components/__docs__/single-select.accessibility.stories.mdx +59 -0
  10. package/src/components/__docs__/single-select.argtypes.js +54 -0
  11. package/src/components/__docs__/single-select.stories.js +464 -0
  12. package/src/components/__tests__/dropdown-core-virtualized.test.js +0 -15
  13. package/src/components/__tests__/dropdown-core.test.js +114 -208
  14. package/src/components/__tests__/multi-select.test.js +1 -3
  15. package/src/components/__tests__/single-select.test.js +15 -47
  16. package/src/components/action-menu.js +11 -0
  17. package/src/components/dropdown-core-virtualized.js +0 -5
  18. package/src/components/dropdown-core.js +140 -126
  19. package/src/components/multi-select.js +17 -33
  20. package/src/components/single-select.js +15 -30
  21. package/src/util/__tests__/dropdown-menu-styles.test.js +0 -26
  22. package/src/util/constants.js +0 -11
  23. package/src/util/dropdown-menu-styles.js +0 -5
  24. package/src/util/types.js +2 -5
  25. package/src/components/__tests__/search-text-input.test.js +0 -212
  26. package/src/components/action-menu.stories.js +0 -48
  27. package/src/components/multi-select.stories.js +0 -124
  28. package/src/components/search-text-input.js +0 -115
  29. package/src/components/single-select.stories.js +0 -247
@@ -4,46 +4,26 @@ import {fireEvent, render, screen, waitFor} from "@testing-library/react";
4
4
  import userEvent from "@testing-library/user-event";
5
5
 
6
6
  import OptionItem from "../option-item.js";
7
- import SearchTextInput from "../search-text-input.js";
8
7
  import DropdownCore from "../dropdown-core.js";
9
8
 
10
9
  const items = [
11
10
  {
12
- component: (
13
- <OptionItem testId="item-0" label="item 0" value="0" key="0" />
14
- ),
11
+ component: <OptionItem label="item 0" value="0" key="0" />,
15
12
  focusable: true,
16
13
  populatedProps: {},
17
14
  },
18
15
  {
19
- component: (
20
- <OptionItem testId="item-1" label="item 1" value="1" key="1" />
21
- ),
16
+ component: <OptionItem label="item 1" value="1" key="1" />,
22
17
  focusable: true,
23
18
  populatedProps: {},
24
19
  },
25
20
  {
26
- component: (
27
- <OptionItem testId="item-2" label="item 2" value="2" key="2" />
28
- ),
21
+ component: <OptionItem label="item 2" value="2" key="2" />,
29
22
  focusable: true,
30
23
  populatedProps: {},
31
24
  },
32
25
  ];
33
26
 
34
- const searchFieldItem = {
35
- component: (
36
- <SearchTextInput
37
- testId="search-text-input"
38
- key="search-text-input"
39
- onChange={jest.fn()}
40
- searchText={""}
41
- />
42
- ),
43
- focusable: true,
44
- populatedProps: {},
45
- };
46
-
47
27
  describe("DropdownCore", () => {
48
28
  it("should throw for invalid role", () => {
49
29
  // Arrange
@@ -90,7 +70,7 @@ describe("DropdownCore", () => {
90
70
  );
91
71
 
92
72
  // Act
93
- const item = screen.getByTestId("item-0");
73
+ const item = screen.getByRole("option", {name: "item 0"});
94
74
 
95
75
  // Assert
96
76
  expect(item).toHaveFocus();
@@ -121,7 +101,7 @@ describe("DropdownCore", () => {
121
101
  userEvent.keyboard("{arrowdown}"); // 1 -> 2
122
102
 
123
103
  // Assert
124
- expect(screen.queryByTestId("item-2")).toHaveFocus();
104
+ expect(screen.getByRole("option", {name: "item 2"})).toHaveFocus();
125
105
  });
126
106
 
127
107
  it("keyboard works backwards as expected", () => {
@@ -137,6 +117,7 @@ describe("DropdownCore", () => {
137
117
  opener={<button />}
138
118
  openerElement={null}
139
119
  onOpenChanged={jest.fn()}
120
+ isFilterable={false}
140
121
  />,
141
122
  );
142
123
 
@@ -151,7 +132,42 @@ describe("DropdownCore", () => {
151
132
  userEvent.keyboard("{arrowup}"); // 2 -> 1
152
133
 
153
134
  // Assert
154
- expect(screen.getByTestId("item-1")).toHaveFocus();
135
+ expect(screen.getByRole("option", {name: "item 1"})).toHaveFocus();
136
+ });
137
+
138
+ it("keyboard works backwards with the search field included", () => {
139
+ // Arrange
140
+ render(
141
+ <DropdownCore
142
+ initialFocusedIndex={0}
143
+ onSearchTextChanged={jest.fn()}
144
+ searchText=""
145
+ isFilterable={true}
146
+ // mock the items
147
+ items={items}
148
+ role="listbox"
149
+ open={true}
150
+ // mock the opener elements
151
+ opener={<button />}
152
+ openerElement={null}
153
+ onOpenChanged={jest.fn()}
154
+ />,
155
+ );
156
+
157
+ // Act
158
+ // navigate down four times
159
+ userEvent.keyboard("{arrowdown}"); // 0 -> 1
160
+ userEvent.keyboard("{arrowdown}"); // 1 -> 2
161
+ userEvent.keyboard("{arrowdown}"); // 2 -> search field
162
+ userEvent.keyboard("{arrowdown}"); // search field -> 0
163
+
164
+ // navigate up back three times
165
+ userEvent.keyboard("{arrowup}"); // 0 -> search field
166
+ userEvent.keyboard("{arrowup}"); // search field -> 2
167
+ userEvent.keyboard("{arrowup}"); // 2 -> 1
168
+
169
+ // Assert
170
+ expect(screen.getByRole("option", {name: "item 1"})).toHaveFocus();
155
171
  });
156
172
 
157
173
  it("closes on tab as expected", () => {
@@ -280,7 +296,7 @@ describe("DropdownCore", () => {
280
296
  />,
281
297
  );
282
298
 
283
- const openerElement = screen.getByTestId("opener");
299
+ const openerElement = screen.getByRole("button");
284
300
  openerElement.focus();
285
301
 
286
302
  // Act
@@ -307,44 +323,7 @@ describe("DropdownCore", () => {
307
323
  );
308
324
 
309
325
  // Assert
310
- expect(screen.queryByTestId("item-0")).toHaveFocus();
311
- });
312
-
313
- it("selects correct item when starting off at an undefined index and a searchbox", () => {
314
- // Arrange
315
- render(
316
- <DropdownCore
317
- initialFocusedIndex={undefined}
318
- searchText=""
319
- // mock the items
320
- items={[
321
- {
322
- component: (
323
- <SearchTextInput
324
- testId="item-0"
325
- key="search-text-input"
326
- onChange={jest.fn()}
327
- searchText={""}
328
- />
329
- ),
330
- focusable: true,
331
- populatedProps: {},
332
- },
333
- ]}
334
- role="listbox"
335
- open={true}
336
- // mock the opener elements
337
- opener={<button />}
338
- openerElement={null}
339
- onOpenChanged={jest.fn()}
340
- />,
341
- );
342
-
343
- // Act
344
- const firstItem = screen.queryByTestId("item-0");
345
-
346
- // Assert
347
- expect(firstItem).toHaveFocus();
326
+ expect(screen.getByRole("option", {name: "item 0"})).toHaveFocus();
348
327
  });
349
328
 
350
329
  it("selects correct item when starting off at a different index and a searchbox", () => {
@@ -353,29 +332,19 @@ describe("DropdownCore", () => {
353
332
  <DropdownCore
354
333
  initialFocusedIndex={1}
355
334
  searchText=""
335
+ isFilterable={true}
356
336
  // mock the items
357
337
  items={[
358
- searchFieldItem,
359
338
  {
360
339
  component: (
361
- <OptionItem
362
- testId="item-0"
363
- label="item 1"
364
- value="1"
365
- key="1"
366
- />
340
+ <OptionItem label="item 1" value="1" key="1" />
367
341
  ),
368
342
  focusable: true,
369
343
  populatedProps: {},
370
344
  },
371
345
  {
372
346
  component: (
373
- <OptionItem
374
- testId="item-1"
375
- label="item 2"
376
- value="2"
377
- key="2"
378
- />
347
+ <OptionItem label="item 2" value="2" key="2" />
379
348
  ),
380
349
  focusable: true,
381
350
  populatedProps: {},
@@ -391,7 +360,7 @@ describe("DropdownCore", () => {
391
360
  );
392
361
 
393
362
  // Act
394
- const firstItem = screen.queryByTestId("item-0");
363
+ const firstItem = screen.getByRole("option", {name: "item 2"});
395
364
 
396
365
  // Assert
397
366
  expect(firstItem).toHaveFocus();
@@ -418,7 +387,7 @@ describe("DropdownCore", () => {
418
387
  userEvent.keyboard("{arrowdown}"); // 2 -> 0
419
388
 
420
389
  // Assert
421
- expect(screen.getByTestId("item-0")).toHaveFocus();
390
+ expect(screen.getByRole("option", {name: "item 0"})).toHaveFocus();
422
391
  });
423
392
 
424
393
  it("focuses correct item with clicking/pressing with initial focused of not 0", () => {
@@ -438,12 +407,12 @@ describe("DropdownCore", () => {
438
407
  );
439
408
 
440
409
  // Act
441
- userEvent.click(screen.getByTestId("item-1"));
410
+ userEvent.click(screen.getByRole("option", {name: "item 1"}));
442
411
  // navigate down
443
412
  userEvent.keyboard("{arrowdown}"); // 1 -> 2
444
413
 
445
414
  // Assert
446
- expect(screen.getByTestId("item-2")).toHaveFocus();
415
+ expect(screen.getByRole("option", {name: "item 2"})).toHaveFocus();
447
416
  });
448
417
 
449
418
  it("focuses correct item with a disabled item", () => {
@@ -455,12 +424,7 @@ describe("DropdownCore", () => {
455
424
  items={[
456
425
  {
457
426
  component: (
458
- <OptionItem
459
- testId="item-0"
460
- label="item 0"
461
- value="0"
462
- key="0"
463
- />
427
+ <OptionItem label="item 0" value="0" key="0" />
464
428
  ),
465
429
  focusable: true,
466
430
  populatedProps: {},
@@ -468,7 +432,6 @@ describe("DropdownCore", () => {
468
432
  {
469
433
  component: (
470
434
  <OptionItem
471
- testId="item-1"
472
435
  label="item 1"
473
436
  value="1"
474
437
  key="1"
@@ -480,12 +443,7 @@ describe("DropdownCore", () => {
480
443
  },
481
444
  {
482
445
  component: (
483
- <OptionItem
484
- testId="item-2"
485
- label="item 2"
486
- value="2"
487
- key="2"
488
- />
446
+ <OptionItem label="item 2" value="2" key="2" />
489
447
  ),
490
448
  focusable: true,
491
449
  populatedProps: {},
@@ -505,7 +463,7 @@ describe("DropdownCore", () => {
505
463
  userEvent.keyboard("{arrowdown}"); // 0 -> 2 (1 is disabled)
506
464
 
507
465
  // Assert
508
- expect(screen.getByTestId("item-2")).toHaveFocus();
466
+ expect(screen.getByRole("option", {name: "item 2"})).toHaveFocus();
509
467
  });
510
468
 
511
469
  it("calls correct onclick for an option item", () => {
@@ -518,12 +476,7 @@ describe("DropdownCore", () => {
518
476
  items={[
519
477
  {
520
478
  component: (
521
- <OptionItem
522
- label="item 0"
523
- value="0"
524
- key="0"
525
- testId="item-0"
526
- />
479
+ <OptionItem label="item 0" value="0" key="0" />
527
480
  ),
528
481
  focusable: true,
529
482
  populatedProps: {},
@@ -534,7 +487,6 @@ describe("DropdownCore", () => {
534
487
  label="item 1"
535
488
  value="1"
536
489
  key="1"
537
- testId="item-1"
538
490
  onClick={onClick1}
539
491
  />
540
492
  ),
@@ -543,12 +495,7 @@ describe("DropdownCore", () => {
543
495
  },
544
496
  {
545
497
  component: (
546
- <OptionItem
547
- label="item 2"
548
- testId="item-2"
549
- value="2"
550
- key="2"
551
- />
498
+ <OptionItem label="item 2" value="2" key="2" />
552
499
  ),
553
500
  focusable: true,
554
501
  populatedProps: {},
@@ -564,7 +511,7 @@ describe("DropdownCore", () => {
564
511
  );
565
512
 
566
513
  // Act
567
- userEvent.click(screen.getByTestId("item-1"));
514
+ userEvent.click(screen.getByRole("option", {name: "item 1"}));
568
515
 
569
516
  // Assert
570
517
  expect(onClick1).toHaveBeenCalledTimes(1);
@@ -579,19 +526,8 @@ describe("DropdownCore", () => {
579
526
  <DropdownCore
580
527
  onSearchTextChanged={handleSearchTextChanged}
581
528
  searchText="ab"
582
- items={[
583
- {
584
- component: (
585
- <SearchTextInput
586
- key="search-text-input"
587
- onChange={handleSearchTextChanged}
588
- searchText={""}
589
- />
590
- ),
591
- focusable: true,
592
- populatedProps: {},
593
- },
594
- ]}
529
+ isFilterable={true}
530
+ items={[]}
595
531
  role="listbox"
596
532
  open={true}
597
533
  // mock the opener elements
@@ -602,37 +538,21 @@ describe("DropdownCore", () => {
602
538
  );
603
539
 
604
540
  // Assert
605
- expect(
606
- screen.getByTestId("dropdown-core-no-results"),
607
- ).toBeInTheDocument();
541
+ expect(screen.getByText("No results")).toBeInTheDocument();
608
542
  });
609
543
 
610
- it("SearchTextInput should be focused when opened", () => {
544
+ it("SearchField should be focused when opened and there's no selection", async () => {
611
545
  // Arrange
612
- const handleSearchTextChanged = jest.fn();
613
- const handleOpen = jest.fn();
614
546
 
615
547
  // Act
616
548
  render(
617
549
  <DropdownCore
618
- initialFocusedIndex={0}
619
- onOpenChanged={handleOpen}
620
- onSearchTextChanged={handleSearchTextChanged}
550
+ initialFocusedIndex={undefined}
551
+ onOpenChanged={jest.fn()}
552
+ onSearchTextChanged={jest.fn()}
621
553
  searchText="ab"
622
- items={[
623
- {
624
- component: (
625
- <SearchTextInput
626
- testId="item-0"
627
- key="search-text-input"
628
- onChange={handleSearchTextChanged}
629
- searchText={""}
630
- />
631
- ),
632
- focusable: true,
633
- populatedProps: {},
634
- },
635
- ]}
554
+ isFilterable={true}
555
+ items={[]}
636
556
  role="listbox"
637
557
  open={true}
638
558
  // mock the opener elements
@@ -642,33 +562,50 @@ describe("DropdownCore", () => {
642
562
  );
643
563
 
644
564
  // Assert
645
- expect(screen.getByPlaceholderText("Filter")).toHaveFocus();
565
+ waitFor(() => {
566
+ expect(screen.getByRole("textbox")).toHaveFocus();
567
+ });
646
568
  });
647
569
 
648
- it("When SearchTextInput has input and focused, tab key should not close the select", () => {
570
+ it("SearchField should trigger change when the user types in", () => {
571
+ // Arrange
572
+ const onSearchTextChangedMock = jest.fn();
573
+
574
+ render(
575
+ <DropdownCore
576
+ initialFocusedIndex={undefined}
577
+ onOpenChanged={jest.fn()}
578
+ onSearchTextChanged={onSearchTextChangedMock}
579
+ searchText=""
580
+ isFilterable={true}
581
+ items={[]}
582
+ role="listbox"
583
+ open={true}
584
+ // mock the opener elements
585
+ opener={<button />}
586
+ openerElement={null}
587
+ />,
588
+ );
589
+
590
+ // Act
591
+ const searchField = screen.getByRole("textbox");
592
+ userEvent.type(searchField, "option 1");
593
+
594
+ // Assert
595
+ expect(onSearchTextChangedMock).toHaveBeenCalled();
596
+ });
597
+
598
+ it("When SearchField has input and focused, tab key should not close the select", async () => {
649
599
  // Arrange
650
- const handleSearchTextChanged = jest.fn();
651
600
  const handleOpen = jest.fn();
652
601
 
653
602
  render(
654
603
  <DropdownCore
655
604
  onOpenChanged={handleOpen}
656
- onSearchTextChanged={handleSearchTextChanged}
605
+ onSearchTextChanged={jest.fn()}
657
606
  searchText="ab"
658
- items={[
659
- {
660
- component: (
661
- <SearchTextInput
662
- testId="item-0"
663
- key="search-text-input"
664
- onChange={handleSearchTextChanged}
665
- searchText={""}
666
- />
667
- ),
668
- focusable: true,
669
- populatedProps: {},
670
- },
671
- ]}
607
+ isFilterable={true}
608
+ items={[]}
672
609
  role="listbox"
673
610
  open={true}
674
611
  // mock the opener elements
@@ -682,32 +619,23 @@ describe("DropdownCore", () => {
682
619
 
683
620
  // Assert
684
621
  expect(handleOpen).toHaveBeenCalledTimes(0);
685
- expect(screen.getByTestId("item-0")).toHaveFocus();
622
+ waitFor(() => {
623
+ expect(
624
+ screen.getByRole("button", {name: "Clear search"}),
625
+ ).toHaveFocus();
626
+ });
686
627
  });
687
628
 
688
- it("When SearchTextInput exists and focused, space key pressing should be allowed", () => {
629
+ it("When SearchField exists and focused, space key pressing should be allowed", () => {
689
630
  // Arrange
690
- const handleSearchTextChanged = jest.fn();
691
631
  const preventDefaultMock = jest.fn();
692
632
 
693
633
  render(
694
634
  <DropdownCore
695
635
  onSearchTextChanged={jest.fn()}
696
636
  searchText="ab"
697
- items={[
698
- {
699
- component: (
700
- <SearchTextInput
701
- testId="item-0"
702
- key="search-text-input"
703
- onChange={handleSearchTextChanged}
704
- searchText={"ab"}
705
- />
706
- ),
707
- focusable: true,
708
- populatedProps: {},
709
- },
710
- ]}
637
+ isFilterable={true}
638
+ items={[]}
711
639
  role="listbox"
712
640
  open={true}
713
641
  // mock the opener elements
@@ -718,7 +646,7 @@ describe("DropdownCore", () => {
718
646
  );
719
647
 
720
648
  // Act
721
- const searchInput = screen.getByTestId("item-0");
649
+ const searchInput = screen.getByRole("textbox");
722
650
  // eslint-disable-next-line testing-library/prefer-user-event
723
651
  fireEvent.keyDown(searchInput, {
724
652
  keyCode: 32,
@@ -755,8 +683,9 @@ describe("DropdownCore", () => {
755
683
  initialFocusedIndex={undefined}
756
684
  onSearchTextChanged={jest.fn()}
757
685
  searchText=""
686
+ isFilterable={true}
758
687
  // mock the items
759
- items={[searchFieldItem, ...optionItems]}
688
+ items={optionItems}
760
689
  role="listbox"
761
690
  open={true}
762
691
  // mock the opener elements
@@ -784,8 +713,9 @@ describe("DropdownCore", () => {
784
713
  initialFocusedIndex={undefined}
785
714
  onSearchTextChanged={jest.fn()}
786
715
  searchText=""
716
+ isFilterable={true}
787
717
  // mock the items
788
- items={[searchFieldItem, ...optionItems]}
718
+ items={optionItems}
789
719
  role="listbox"
790
720
  open={true}
791
721
  // mock the opener elements
@@ -831,29 +761,5 @@ describe("DropdownCore", () => {
831
761
  // Assert
832
762
  expect(container).toHaveTextContent("3 items");
833
763
  });
834
-
835
- it("shouldn't include the search field as part of the options", async () => {
836
- // Arrange
837
-
838
- // Act
839
- const {container} = render(
840
- <DropdownCore
841
- initialFocusedIndex={undefined}
842
- onSearchTextChanged={jest.fn()}
843
- searchText=""
844
- // mock the items (3 options + search field)
845
- items={[searchFieldItem, ...items]}
846
- role="listbox"
847
- open={true}
848
- // mock the opener elements
849
- opener={<button />}
850
- openerElement={null}
851
- onOpenChanged={jest.fn()}
852
- />,
853
- );
854
-
855
- // Assert
856
- expect(container).toHaveTextContent("3 items");
857
- });
858
764
  });
859
765
  });
@@ -1078,9 +1078,7 @@ describe("MultiSelect", () => {
1078
1078
  userEvent.type(screen.getByPlaceholderText("Filter"), "other");
1079
1079
 
1080
1080
  // Assert
1081
- expect(screen.getByRole("listbox")).toHaveTextContent(
1082
- "No hay resultados",
1083
- );
1081
+ expect(screen.getByText("No hay resultados")).toBeInTheDocument();
1084
1082
  });
1085
1083
 
1086
1084
  it("passes the custom label to the select all shortcut", () => {