@khanacademy/wonder-blocks-dropdown 5.3.8 → 5.4.0

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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  import * as React from "react";
2
3
  import {render, screen} from "@testing-library/react";
3
4
  import {PointerEventsCheckLevel, userEvent} from "@testing-library/user-event";
@@ -145,29 +146,6 @@ describe("ActionMenu", () => {
145
146
  expect(screen.queryByRole("menu")).not.toBeInTheDocument();
146
147
  });
147
148
 
148
- it("updates the aria-expanded value when opening", async () => {
149
- // Arrange
150
- render(
151
- <ActionMenu
152
- menuText={"Action menu!"}
153
- testId="openTest"
154
- onChange={onChange}
155
- selectedValues={[]}
156
- >
157
- <ActionItem label="Action" onClick={onClick} />
158
- <SeparatorItem />
159
- <OptionItem label="Toggle" value="toggle" onClick={onToggle} />
160
- </ActionMenu>,
161
- );
162
-
163
- // Act
164
- const opener = await screen.findByRole("button");
165
- await userEvent.click(opener);
166
-
167
- // Assert
168
- expect(opener).toHaveAttribute("aria-expanded", "true");
169
- });
170
-
171
149
  it("triggers actions", async () => {
172
150
  // Arrange
173
151
  const onChange = jest.fn();
@@ -572,4 +550,633 @@ describe("ActionMenu", () => {
572
550
  expect(opener).toHaveTextContent("Action menu!");
573
551
  });
574
552
  });
553
+
554
+ describe("With OptionItems", () => {
555
+ it("Should render option items with `role=menuitemcheckbox`", async () => {
556
+ // Arrange
557
+ render(
558
+ <ActionMenu
559
+ menuText="Action menu!"
560
+ testId="openTest"
561
+ onChange={onChange}
562
+ >
563
+ <OptionItem
564
+ label="Toggle A"
565
+ value="toggle-a"
566
+ testId="toggle-a"
567
+ />
568
+ <OptionItem
569
+ label="Toggle B"
570
+ value="toggle-b"
571
+ testId="toggle-b"
572
+ />
573
+ </ActionMenu>,
574
+ );
575
+
576
+ // Act
577
+ // open the menu
578
+ const opener = await screen.findByRole("button");
579
+ await userEvent.click(opener);
580
+
581
+ // Assert
582
+ expect(
583
+ await screen.findAllByRole("menuitemcheckbox", {hidden: true}),
584
+ ).toHaveLength(2);
585
+ });
586
+
587
+ it("Should render non-selected option items with `aria-checked` set to `false`", async () => {
588
+ // Arrange
589
+ render(
590
+ <ActionMenu
591
+ menuText="Action menu!"
592
+ testId="openTest"
593
+ onChange={onChange}
594
+ selectedValues={[]}
595
+ >
596
+ <OptionItem
597
+ label="Toggle A"
598
+ value="toggle-a"
599
+ testId="toggle-a"
600
+ />
601
+ <OptionItem
602
+ label="Toggle B"
603
+ value="toggle-b"
604
+ testId="toggle-b"
605
+ />
606
+ </ActionMenu>,
607
+ );
608
+
609
+ // Act
610
+ // open the menu
611
+ const opener = await screen.findByRole("button");
612
+ await userEvent.click(opener);
613
+
614
+ // Assert
615
+ const menuItemCheckboxes = await screen.findAllByRole(
616
+ "menuitemcheckbox",
617
+ {
618
+ hidden: true,
619
+ },
620
+ );
621
+ expect(menuItemCheckboxes.at(0)).toHaveAttribute(
622
+ "aria-checked",
623
+ "false",
624
+ );
625
+ expect(menuItemCheckboxes.at(1)).toHaveAttribute(
626
+ "aria-checked",
627
+ "false",
628
+ );
629
+ });
630
+
631
+ it("Should render selected option items with `aria-checked` set to `true`", async () => {
632
+ // Arrange
633
+ render(
634
+ <ActionMenu
635
+ menuText="Action menu!"
636
+ testId="openTest"
637
+ onChange={onChange}
638
+ selectedValues={["toggle-a"]}
639
+ >
640
+ <OptionItem
641
+ label="Toggle A"
642
+ value="toggle-a"
643
+ testId="toggle-a"
644
+ />
645
+ <OptionItem
646
+ label="Toggle B"
647
+ value="toggle-b"
648
+ testId="toggle-b"
649
+ />
650
+ </ActionMenu>,
651
+ );
652
+
653
+ // Act
654
+ // open the menu
655
+ const opener = await screen.findByRole("button");
656
+ await userEvent.click(opener);
657
+
658
+ // Assert
659
+ const menuItemCheckboxes = await screen.findAllByRole(
660
+ "menuitemcheckbox",
661
+ {
662
+ hidden: true,
663
+ },
664
+ );
665
+ expect(menuItemCheckboxes.at(0)).toHaveAttribute(
666
+ "aria-checked",
667
+ "true",
668
+ );
669
+ });
670
+
671
+ it("Should not use `aria-selected` attribute on selected and non-selected options", async () => {
672
+ // Arrange
673
+ render(
674
+ <ActionMenu
675
+ menuText="Action menu!"
676
+ testId="openTest"
677
+ onChange={onChange}
678
+ selectedValues={["toggle-a"]}
679
+ >
680
+ <OptionItem
681
+ label="Toggle A"
682
+ value="toggle-a"
683
+ testId="toggle-a"
684
+ />
685
+ <OptionItem
686
+ label="Toggle B"
687
+ value="toggle-b"
688
+ testId="toggle-b"
689
+ />
690
+ </ActionMenu>,
691
+ );
692
+
693
+ // Act
694
+ // open the menu
695
+ const opener = await screen.findByRole("button");
696
+ await userEvent.click(opener);
697
+
698
+ // Assert
699
+ const menuItemCheckboxes = await screen.findAllByRole(
700
+ "menuitemcheckbox",
701
+ {
702
+ hidden: true,
703
+ },
704
+ );
705
+ expect(menuItemCheckboxes.at(0)).not.toHaveAttribute(
706
+ "aria-selected",
707
+ );
708
+ expect(menuItemCheckboxes.at(1)).not.toHaveAttribute(
709
+ "aria-selected",
710
+ );
711
+ });
712
+
713
+ it("Should render action items with `role=menuitem` and option items with `role=menuitemcheckbox`", async () => {
714
+ // Arrange
715
+ render(
716
+ <ActionMenu
717
+ menuText="Action menu!"
718
+ testId="openTest"
719
+ onChange={onChange}
720
+ >
721
+ <ActionItem label="Action" />
722
+ <OptionItem
723
+ label="Toggle A"
724
+ value="toggle-a"
725
+ testId="toggle-a"
726
+ />
727
+ </ActionMenu>,
728
+ );
729
+
730
+ // Act
731
+ // open the menu
732
+ const opener = await screen.findByRole("button");
733
+ await userEvent.click(opener);
734
+
735
+ // Assert
736
+ expect(
737
+ await screen.findAllByRole("menuitem", {hidden: true}),
738
+ ).toHaveLength(1);
739
+ expect(
740
+ await screen.findAllByRole("menuitemcheckbox", {hidden: true}),
741
+ ).toHaveLength(1);
742
+ });
743
+
744
+ describe("With Virtualization", () => {
745
+ it("Should render option items with `role=menuitemcheckbox` when there are many options", async () => {
746
+ // Arrange
747
+ render(
748
+ <ActionMenu
749
+ menuText="Action menu!"
750
+ testId="openTest"
751
+ onChange={onChange}
752
+ >
753
+ {[...new Array(126)].map((_, i) => (
754
+ <OptionItem
755
+ label={`Toggle ${i}`}
756
+ key={i}
757
+ value={`toggle-${i}`}
758
+ />
759
+ ))}
760
+ </ActionMenu>,
761
+ );
762
+
763
+ // Act
764
+ // open the menu
765
+ const opener = await screen.findByRole("button");
766
+ await userEvent.click(opener);
767
+
768
+ // Assert
769
+ // Note there are less than the option items amount because they are
770
+ // virtualized
771
+ expect(
772
+ await screen.findAllByRole("menuitemcheckbox", {
773
+ hidden: true,
774
+ }),
775
+ ).toHaveLength(14);
776
+ expect(
777
+ screen.queryAllByRole("menuitem", {
778
+ hidden: true,
779
+ }),
780
+ ).toHaveLength(0);
781
+ });
782
+
783
+ it("Should render selected option items with `aria-checked=true` when there are many options", async () => {
784
+ // Arrange
785
+ render(
786
+ <ActionMenu
787
+ menuText="Action menu!"
788
+ testId="openTest"
789
+ onChange={onChange}
790
+ selectedValues={["toggle-0"]}
791
+ >
792
+ {[...new Array(126)].map((_, i) => (
793
+ <OptionItem
794
+ label={`Toggle ${i}`}
795
+ key={i}
796
+ value={`toggle-${i}`}
797
+ />
798
+ ))}
799
+ </ActionMenu>,
800
+ );
801
+
802
+ // Act
803
+ // open the menu
804
+ const opener = await screen.findByRole("button");
805
+ await userEvent.click(opener);
806
+
807
+ // Assert
808
+ const menuItemCheckboxes = await screen.findAllByRole(
809
+ "menuitemcheckbox",
810
+ {
811
+ hidden: true,
812
+ },
813
+ );
814
+ expect(menuItemCheckboxes.at(0)).toHaveAttribute(
815
+ "aria-checked",
816
+ "true",
817
+ );
818
+ });
819
+
820
+ it("Should render non-selected option items with `aria-checked=false` when there are many options", async () => {
821
+ // Arrange
822
+ render(
823
+ <ActionMenu
824
+ menuText="Action menu!"
825
+ testId="openTest"
826
+ onChange={onChange}
827
+ selectedValues={[]}
828
+ >
829
+ {[...new Array(126)].map((_, i) => (
830
+ <OptionItem
831
+ label={`Toggle ${i}`}
832
+ key={i}
833
+ value={`toggle-${i}`}
834
+ />
835
+ ))}
836
+ </ActionMenu>,
837
+ );
838
+
839
+ // Act
840
+ // open the menu
841
+ const opener = await screen.findByRole("button");
842
+ await userEvent.click(opener);
843
+
844
+ // Assert
845
+ const menuItemCheckboxes = await screen.findAllByRole(
846
+ "menuitemcheckbox",
847
+ {
848
+ hidden: true,
849
+ },
850
+ );
851
+ expect(menuItemCheckboxes.at(0)).toHaveAttribute(
852
+ "aria-checked",
853
+ "false",
854
+ );
855
+ });
856
+
857
+ it("Should not use `aria-selected` attribute on selected and non-selected options", async () => {
858
+ // Arrange
859
+ render(
860
+ <ActionMenu
861
+ menuText="Action menu!"
862
+ testId="openTest"
863
+ onChange={onChange}
864
+ selectedValues={["toggle-0"]}
865
+ >
866
+ {[...new Array(126)].map((_, i) => (
867
+ <OptionItem
868
+ label={`Toggle ${i}`}
869
+ key={i}
870
+ value={`toggle-${i}`}
871
+ />
872
+ ))}
873
+ </ActionMenu>,
874
+ );
875
+
876
+ // Act
877
+ // open the menu
878
+ const opener = await screen.findByRole("button");
879
+ await userEvent.click(opener);
880
+
881
+ // Assert
882
+ const menuItemCheckboxes = await screen.findAllByRole(
883
+ "menuitemcheckbox",
884
+ {
885
+ hidden: true,
886
+ },
887
+ );
888
+ expect(menuItemCheckboxes.at(0)).not.toHaveAttribute(
889
+ "aria-selected",
890
+ );
891
+ expect(menuItemCheckboxes.at(1)).not.toHaveAttribute(
892
+ "aria-selected",
893
+ );
894
+ });
895
+ });
896
+ });
897
+
898
+ describe("Ids", () => {
899
+ it("Should auto-generate an id for the opener if `id` prop is not provided", async () => {
900
+ // Arrange
901
+ render(
902
+ <ActionMenu menuText={"Action menu!"}>
903
+ <ActionItem label="Create" />
904
+ </ActionMenu>,
905
+ );
906
+
907
+ // Act
908
+ const opener = await screen.findByRole("button");
909
+
910
+ // Assert
911
+ // Expect autogenerated id to be in the form uid-action-menu-opener-[number]-wb-id
912
+ expect(opener).toHaveAttribute(
913
+ "id",
914
+ expect.stringMatching(/^uid-action-menu-opener-\d+-wb-id$/),
915
+ );
916
+ });
917
+
918
+ it("Should use the `id` prop if provided", async () => {
919
+ // Arrange
920
+ const id = "test-id";
921
+ render(
922
+ <ActionMenu menuText={"Action menu!"} id={id}>
923
+ <ActionItem label="Create" />
924
+ </ActionMenu>,
925
+ );
926
+
927
+ // Act
928
+ const opener = await screen.findByRole("button");
929
+
930
+ // Assert
931
+ expect(opener).toHaveAttribute("id", id);
932
+ });
933
+ it("Should auto-generate an id for the dropdown if `dropdownId` prop is not provided", async () => {
934
+ // Arrange
935
+ render(
936
+ <ActionMenu menuText={"Action menu!"}>
937
+ <ActionItem label="Create" />
938
+ </ActionMenu>,
939
+ );
940
+
941
+ // Act
942
+ // Open the dropdown
943
+ const opener = await screen.findByRole("button");
944
+ await userEvent.click(opener);
945
+
946
+ // Assert
947
+ expect(
948
+ await screen.findByRole("menu", {hidden: true}),
949
+ ).toHaveAttribute(
950
+ "id",
951
+ expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
952
+ );
953
+ });
954
+
955
+ it("Should use the `dropdownId` prop if provided", async () => {
956
+ // Arrange
957
+ const dropdownId = "test-id";
958
+ render(
959
+ <ActionMenu menuText={"Action menu!"} dropdownId={dropdownId}>
960
+ <ActionItem label="Create" />
961
+ </ActionMenu>,
962
+ );
963
+
964
+ // Act
965
+ // Open the dropdown
966
+ const opener = await screen.findByRole("button");
967
+ await userEvent.click(opener);
968
+
969
+ // Assert
970
+ expect(
971
+ await screen.findByRole("menu", {hidden: true}),
972
+ ).toHaveAttribute("id", dropdownId);
973
+ });
974
+ });
975
+
976
+ describe("a11y > aria-controls", () => {
977
+ it("Should set the `aria-controls` attribute on the default opener to the provided dropdownId prop", async () => {
978
+ // Arrange
979
+ const dropdownId = "test-id";
980
+ render(
981
+ <ActionMenu menuText={"Action menu!"} dropdownId={dropdownId}>
982
+ <ActionItem label="Create" />
983
+ </ActionMenu>,
984
+ );
985
+
986
+ // Act
987
+ const opener = await screen.findByRole("button");
988
+ await userEvent.click(opener);
989
+ const dropdown = await screen.findByRole("menu", {hidden: true});
990
+
991
+ // Assert
992
+ expect(opener).toHaveAttribute("aria-controls", dropdown.id);
993
+ expect(opener).toHaveAttribute("aria-controls", dropdownId);
994
+ });
995
+
996
+ it("Should set the `aria-controls` attribute on the default opener to the auto-generated dropdownId", async () => {
997
+ // Arrange
998
+ render(
999
+ <ActionMenu menuText={"Action menu!"}>
1000
+ <ActionItem label="Create" />
1001
+ </ActionMenu>,
1002
+ );
1003
+
1004
+ // Act
1005
+ const opener = await screen.findByRole("button");
1006
+ await userEvent.click(opener);
1007
+ const dropdown = await screen.findByRole("menu", {hidden: true});
1008
+
1009
+ // Assert
1010
+ expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1011
+ expect(opener).toHaveAttribute(
1012
+ "aria-controls",
1013
+ expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
1014
+ );
1015
+ });
1016
+
1017
+ it("Should set the `aria-controls` attribute on the custom opener to the provided dropdownId prop", async () => {
1018
+ // Arrange
1019
+ const dropdownId = "test-id";
1020
+ render(
1021
+ <ActionMenu
1022
+ menuText={"Action menu!"}
1023
+ dropdownId={dropdownId}
1024
+ opener={() => (
1025
+ <button aria-label="Search" onClick={jest.fn()} />
1026
+ )}
1027
+ >
1028
+ <ActionItem label="Action" onClick={onClick} />
1029
+ </ActionMenu>,
1030
+ );
1031
+
1032
+ // Act
1033
+ const opener = await screen.findByLabelText("Search");
1034
+ await userEvent.click(opener);
1035
+ const dropdown = await screen.findByRole("menu", {hidden: true});
1036
+
1037
+ // Assert
1038
+ expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1039
+ expect(opener).toHaveAttribute("aria-controls", dropdownId);
1040
+ });
1041
+
1042
+ it("Should set the `aria-controls` attribute on the custom opener to the auto-generated dropdownId", async () => {
1043
+ // Arrange
1044
+ render(
1045
+ <ActionMenu
1046
+ menuText={"Action menu!"}
1047
+ opener={() => (
1048
+ <button aria-label="Search" onClick={jest.fn()} />
1049
+ )}
1050
+ >
1051
+ <ActionItem label="Action" onClick={onClick} />
1052
+ </ActionMenu>,
1053
+ );
1054
+
1055
+ // Act
1056
+ const opener = await screen.findByLabelText("Search");
1057
+ await userEvent.click(opener);
1058
+ const dropdown = await screen.findByRole("menu", {hidden: true});
1059
+
1060
+ // Assert
1061
+ expect(opener).toHaveAttribute("aria-controls", dropdown.id);
1062
+ expect(opener).toHaveAttribute(
1063
+ "aria-controls",
1064
+ expect.stringMatching(/^uid-action-menu-dropdown-\d+-wb-id$/),
1065
+ );
1066
+ });
1067
+ });
1068
+
1069
+ describe("a11y > aria-haspopup", () => {
1070
+ it("should have aria-haspopup set on the opener", async () => {
1071
+ // Arrange
1072
+ render(
1073
+ <ActionMenu menuText={"Action menu!"} onChange={onChange}>
1074
+ <ActionItem label="Action" onClick={onClick} />
1075
+ </ActionMenu>,
1076
+ );
1077
+
1078
+ // Act
1079
+ const opener = await screen.findByRole("button");
1080
+
1081
+ // Assert
1082
+ expect(opener).toHaveAttribute("aria-haspopup", "menu");
1083
+ });
1084
+
1085
+ it("should have aria-haspopup set on the custom opener", async () => {
1086
+ // Arrange
1087
+ render(
1088
+ <ActionMenu
1089
+ menuText={"Action menu!"}
1090
+ onChange={onChange}
1091
+ opener={() => (
1092
+ <button aria-label="Search" onClick={jest.fn()} />
1093
+ )}
1094
+ >
1095
+ <ActionItem label="Action" onClick={onClick} />
1096
+ </ActionMenu>,
1097
+ );
1098
+
1099
+ // Act
1100
+ const opener = await screen.findByLabelText("Search");
1101
+
1102
+ // Assert
1103
+ expect(opener).toHaveAttribute("aria-haspopup", "menu");
1104
+ });
1105
+ });
1106
+
1107
+ describe("a11y > aria-expanded", () => {
1108
+ it("should have aria-expanded=false when closed", async () => {
1109
+ // Arrange
1110
+ render(
1111
+ <ActionMenu menuText={"Action menu!"} onChange={onChange}>
1112
+ <ActionItem label="Action" onClick={onClick} />
1113
+ </ActionMenu>,
1114
+ );
1115
+
1116
+ // Act
1117
+ const opener = await screen.findByRole("button");
1118
+
1119
+ // Assert
1120
+ expect(opener).toHaveAttribute("aria-expanded", "false");
1121
+ });
1122
+
1123
+ it("updates the aria-expanded value when opening", async () => {
1124
+ // Arrange
1125
+ render(
1126
+ <ActionMenu menuText={"Action menu!"} onChange={onChange}>
1127
+ <ActionItem label="Action" onClick={onClick} />
1128
+ </ActionMenu>,
1129
+ );
1130
+
1131
+ // Act
1132
+ const opener = await screen.findByRole("button");
1133
+ await userEvent.click(opener);
1134
+
1135
+ // Assert
1136
+ expect(opener).toHaveAttribute("aria-expanded", "true");
1137
+ });
1138
+
1139
+ it("should have aria-expanded=false when closed and using a custom opener", async () => {
1140
+ // Arrange
1141
+ render(
1142
+ <ActionMenu
1143
+ menuText={"Action menu!"}
1144
+ onChange={onChange}
1145
+ opener={() => (
1146
+ <button aria-label="Search" onClick={jest.fn()} />
1147
+ )}
1148
+ >
1149
+ <ActionItem label="Action" onClick={onClick} />
1150
+ </ActionMenu>,
1151
+ );
1152
+
1153
+ // Act
1154
+ const opener = await screen.findByLabelText("Search");
1155
+
1156
+ // Assert
1157
+ expect(opener).toHaveAttribute("aria-expanded", "false");
1158
+ });
1159
+
1160
+ it("updates the aria-expanded value when opening and using a custom opener", async () => {
1161
+ // Arrange
1162
+ render(
1163
+ <ActionMenu
1164
+ menuText={"Action menu!"}
1165
+ onChange={onChange}
1166
+ opener={() => (
1167
+ <button aria-label="Search" onClick={jest.fn()} />
1168
+ )}
1169
+ >
1170
+ <ActionItem label="Action" onClick={onClick} />
1171
+ </ActionMenu>,
1172
+ );
1173
+
1174
+ // Act
1175
+ const opener = await screen.findByLabelText("Search");
1176
+ await userEvent.click(opener);
1177
+
1178
+ // Assert
1179
+ expect(opener).toHaveAttribute("aria-expanded", "true");
1180
+ });
1181
+ });
575
1182
  });