@khanacademy/wonder-blocks-dropdown 5.3.5 → 5.3.7

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.
@@ -0,0 +1,220 @@
1
+ import * as React from "react";
2
+ import {fireEvent, render, screen} from "@testing-library/react";
3
+ import {userEvent} from "@testing-library/user-event";
4
+
5
+ import SelectOpener from "../select-opener";
6
+
7
+ describe("SelectOpener", () => {
8
+ describe("onOpenChanged", () => {
9
+ const children = "text";
10
+ it("should trigger using the mouse", async () => {
11
+ // Arrange
12
+ const onOpenMock = jest.fn();
13
+
14
+ render(
15
+ <SelectOpener
16
+ onOpenChanged={onOpenMock}
17
+ disabled={false}
18
+ error={false}
19
+ isPlaceholder={false}
20
+ light={false}
21
+ open={false}
22
+ >
23
+ {children}
24
+ </SelectOpener>,
25
+ );
26
+
27
+ // Act
28
+ // Press the button.
29
+ await userEvent.click(await screen.findByRole("button"));
30
+
31
+ // Assert
32
+ expect(onOpenMock).toHaveBeenCalledTimes(1);
33
+ });
34
+
35
+ it("should trigger by pressing {Space}", async () => {
36
+ // Arrange
37
+ const onOpenMock = jest.fn();
38
+
39
+ render(
40
+ <SelectOpener
41
+ onOpenChanged={onOpenMock}
42
+ disabled={false}
43
+ error={false}
44
+ isPlaceholder={false}
45
+ light={false}
46
+ open={false}
47
+ >
48
+ {children}
49
+ </SelectOpener>,
50
+ );
51
+
52
+ // Act
53
+ // Press the button.
54
+ const button = await screen.findByRole("button");
55
+ // NOTE: we need to use fireEvent here because await userEvent doesn't
56
+ // support keyUp/Down events and we use these handlers to override
57
+ // the default behavior of the button.
58
+ // eslint-disable-next-line testing-library/prefer-user-event
59
+ fireEvent.keyDown(button, {
60
+ key: " ",
61
+ });
62
+ // eslint-disable-next-line testing-library/prefer-user-event
63
+ fireEvent.keyUp(button, {
64
+ key: " ",
65
+ });
66
+
67
+ // Assert
68
+ expect(onOpenMock).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it("should trigger by pressing {Enter}", async () => {
72
+ // Arrange
73
+ const onOpenMock = jest.fn();
74
+
75
+ render(
76
+ <SelectOpener
77
+ onOpenChanged={onOpenMock}
78
+ disabled={false}
79
+ error={false}
80
+ isPlaceholder={false}
81
+ light={false}
82
+ open={false}
83
+ >
84
+ {children}
85
+ </SelectOpener>,
86
+ );
87
+
88
+ // Act
89
+ // Press the button.
90
+ const button = await screen.findByRole("button");
91
+ // NOTE: we need to use fireEvent here because await userEvent doesn't
92
+ // support keyUp/Down events and we use these handlers to override
93
+ // the default behavior of the button.
94
+ // eslint-disable-next-line testing-library/prefer-user-event
95
+ fireEvent.keyDown(button, {
96
+ key: "Enter",
97
+ });
98
+ // NOTE: We need to trigger multiple events to simulate the browser
99
+ // behavior of pressing Enter on a button. By default, browsers will
100
+ // trigger a click event on keyDown, but we need to trigger it on
101
+ // keyUp.
102
+ // eslint-disable-next-line testing-library/prefer-user-event
103
+ fireEvent.keyDown(button, {
104
+ key: "Enter",
105
+ });
106
+ // eslint-disable-next-line testing-library/prefer-user-event
107
+ fireEvent.keyUp(button, {
108
+ key: "Enter",
109
+ });
110
+
111
+ // Assert
112
+ expect(onOpenMock).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("should not trigger using the mouse if it is disabled", async () => {
116
+ // Arrange
117
+ const onOpenMock = jest.fn();
118
+
119
+ render(
120
+ <SelectOpener
121
+ onOpenChanged={onOpenMock}
122
+ disabled={true}
123
+ error={false}
124
+ isPlaceholder={false}
125
+ light={false}
126
+ open={false}
127
+ >
128
+ {children}
129
+ </SelectOpener>,
130
+ );
131
+
132
+ // Act
133
+ // Press the button.
134
+ await userEvent.click(await screen.findByRole("button"));
135
+
136
+ // Assert
137
+ expect(onOpenMock).toHaveBeenCalledTimes(0);
138
+ });
139
+
140
+ it("should not trigger by pressing {Space} if it is disabled", async () => {
141
+ // Arrange
142
+ const onOpenMock = jest.fn();
143
+
144
+ render(
145
+ <SelectOpener
146
+ onOpenChanged={onOpenMock}
147
+ disabled={true}
148
+ error={false}
149
+ isPlaceholder={false}
150
+ light={false}
151
+ open={false}
152
+ >
153
+ {children}
154
+ </SelectOpener>,
155
+ );
156
+
157
+ // Act
158
+ // Press the button.
159
+ const button = await screen.findByRole("button");
160
+ // NOTE: we need to use fireEvent here because await userEvent doesn't
161
+ // support keyUp/Down events and we use these handlers to override
162
+ // the default behavior of the button.
163
+ // eslint-disable-next-line testing-library/prefer-user-event
164
+ fireEvent.keyDown(button, {
165
+ key: " ",
166
+ });
167
+ // eslint-disable-next-line testing-library/prefer-user-event
168
+ fireEvent.keyUp(button, {
169
+ key: " ",
170
+ });
171
+
172
+ // Assert
173
+ expect(onOpenMock).toHaveBeenCalledTimes(0);
174
+ });
175
+
176
+ it("should not trigger by pressing {Enter} if it is disabled", async () => {
177
+ // Arrange
178
+ const onOpenMock = jest.fn();
179
+
180
+ render(
181
+ <SelectOpener
182
+ onOpenChanged={onOpenMock}
183
+ disabled={true}
184
+ error={false}
185
+ isPlaceholder={false}
186
+ light={false}
187
+ open={false}
188
+ >
189
+ {children}
190
+ </SelectOpener>,
191
+ );
192
+
193
+ // Act
194
+ // Press the button.
195
+ const button = await screen.findByRole("button");
196
+ // NOTE: we need to use fireEvent here because await userEvent doesn't
197
+ // support keyUp/Down events and we use these handlers to override
198
+ // the default behavior of the button.
199
+ // eslint-disable-next-line testing-library/prefer-user-event
200
+ fireEvent.keyDown(button, {
201
+ key: "Enter",
202
+ });
203
+ // NOTE: We need to trigger multiple events to simulate the browser
204
+ // behavior of pressing Enter on a button. By default, browsers will
205
+ // trigger a click event on keyDown, but we need to trigger it on
206
+ // keyUp.
207
+ // eslint-disable-next-line testing-library/prefer-user-event
208
+ fireEvent.keyDown(button, {
209
+ key: "Enter",
210
+ });
211
+ // eslint-disable-next-line testing-library/prefer-user-event
212
+ fireEvent.keyUp(button, {
213
+ key: "Enter",
214
+ });
215
+
216
+ // Assert
217
+ expect(onOpenMock).toHaveBeenCalledTimes(0);
218
+ });
219
+ });
220
+ });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import * as React from "react";
3
- import {fireEvent, render, screen} from "@testing-library/react";
3
+ import {render, screen} from "@testing-library/react";
4
4
  import {
5
5
  userEvent as ue,
6
6
  PointerEventsCheckLevel,
@@ -1075,57 +1075,210 @@ describe("SingleSelect", () => {
1075
1075
  });
1076
1076
  });
1077
1077
 
1078
- describe("error state styles", () => {
1079
- it("should apply the error styles to the dropdown on hover", async () => {
1078
+ describe("a11y > Focusable", () => {
1079
+ it("should be focusable", () => {
1080
1080
  // Arrange
1081
- const userEvent = doRender(
1081
+ doRender(
1082
1082
  <SingleSelect
1083
- onChange={onChange}
1084
- placeholder="Choose a fruit"
1085
- error={true}
1086
- testId="singleselect-error-hover"
1083
+ placeholder="Default placeholder"
1084
+ onChange={jest.fn()}
1085
+ testId="select-focus-test"
1087
1086
  >
1088
- {[<OptionItem label="Banana" value="banana" />]}
1087
+ <OptionItem label="item 1" value="1" />
1088
+ <OptionItem label="item 2" value="2" />
1089
+ <OptionItem label="item 3" value="3" />
1089
1090
  </SingleSelect>,
1090
1091
  );
1091
- const dropdown = await screen.findByTestId(
1092
- "singleselect-error-hover",
1092
+
1093
+ // Act
1094
+ const singleSelect = screen.getByTestId("select-focus-test");
1095
+ singleSelect.focus();
1096
+
1097
+ // Assert
1098
+ expect(singleSelect).toHaveFocus();
1099
+ });
1100
+
1101
+ it("should be focusable when disabled", () => {
1102
+ // Arrange
1103
+ doRender(
1104
+ <SingleSelect
1105
+ placeholder="Default placeholder"
1106
+ onChange={jest.fn()}
1107
+ testId="select-focus-test"
1108
+ disabled={true}
1109
+ >
1110
+ <OptionItem label="item 1" value="1" />
1111
+ <OptionItem label="item 2" value="2" />
1112
+ <OptionItem label="item 3" value="3" />
1113
+ </SingleSelect>,
1093
1114
  );
1094
1115
 
1095
1116
  // Act
1096
- await userEvent.hover(dropdown);
1117
+ const singleSelect = screen.getByTestId("select-focus-test");
1118
+ singleSelect.focus();
1097
1119
 
1098
1120
  // Assert
1099
- expect(dropdown).toHaveStyle("border-color: #d92916");
1100
- expect(dropdown).toHaveStyle("border-width: 2px");
1121
+ expect(singleSelect).toHaveFocus();
1101
1122
  });
1123
+ });
1102
1124
 
1103
- it("should apply the error styles to the dropdown on mouse down", async () => {
1125
+ describe("Disabled state", () => {
1126
+ it("should set the `aria-disabled` attribute to `true` if `disabled` prop is `true`", () => {
1104
1127
  // Arrange
1128
+
1129
+ // Act
1105
1130
  doRender(
1106
1131
  <SingleSelect
1107
- onChange={onChange}
1108
- placeholder="Choose a fruit"
1109
- error={true}
1110
- testId="singleselect-error-active"
1132
+ placeholder="Default placeholder"
1133
+ onChange={jest.fn()}
1134
+ testId="select-focus-test"
1135
+ disabled={true}
1111
1136
  >
1112
- {[<OptionItem label="Banana" value="banana" />]}
1137
+ <OptionItem label="item 1" value="1" />
1138
+ <OptionItem label="item 2" value="2" />
1139
+ <OptionItem label="item 3" value="3" />
1113
1140
  </SingleSelect>,
1114
1141
  );
1115
- const dropdown = await screen.findByTestId(
1116
- "singleselect-error-active",
1142
+ const singleSelect = screen.getByTestId("select-focus-test");
1143
+
1144
+ // Assert
1145
+ expect(singleSelect).toHaveAttribute("aria-disabled", "true");
1146
+ });
1147
+
1148
+ it("should not set the `disabled` attribute if `disabled` prop is `true` since `aria-disabled` is used instead", () => {
1149
+ // Arrange
1150
+
1151
+ // Act
1152
+ doRender(
1153
+ <SingleSelect
1154
+ placeholder="Default placeholder"
1155
+ onChange={jest.fn()}
1156
+ testId="select-focus-test"
1157
+ disabled={true}
1158
+ >
1159
+ <OptionItem label="item 1" value="1" />
1160
+ <OptionItem label="item 2" value="2" />
1161
+ <OptionItem label="item 3" value="3" />
1162
+ </SingleSelect>,
1117
1163
  );
1164
+ const singleSelect = screen.getByTestId("select-focus-test");
1165
+
1166
+ // Assert
1167
+ expect(singleSelect).not.toHaveAttribute("disabled");
1168
+ });
1169
+
1170
+ it("should not be opened if it is disabled and `open` prop is set to true", () => {
1171
+ // Arrange
1118
1172
 
1119
1173
  // Act
1120
- // eslint-disable-next-line testing-library/prefer-user-event
1121
- fireEvent.mouseDown(dropdown);
1174
+ doRender(
1175
+ <SingleSelect
1176
+ placeholder="Default placeholder"
1177
+ onChange={jest.fn()}
1178
+ disabled={true}
1179
+ opened={true}
1180
+ >
1181
+ <OptionItem label="item 1" value="1" />
1182
+ <OptionItem label="item 2" value="2" />
1183
+ <OptionItem label="item 3" value="3" />
1184
+ </SingleSelect>,
1185
+ );
1122
1186
 
1123
1187
  // Assert
1124
- expect(dropdown).toHaveStyle("border-color: #d92916");
1125
- expect(dropdown).toHaveStyle("border-width: 2px");
1126
- expect(dropdown).toHaveStyle(
1127
- "background-color: rgb(243, 187, 180)",
1188
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
1189
+ });
1190
+ });
1191
+
1192
+ describe("a11y > Focusable", () => {
1193
+ it("should be focusable", () => {
1194
+ // Arrange
1195
+ doRender(
1196
+ <SingleSelect
1197
+ placeholder="Default placeholder"
1198
+ onChange={jest.fn()}
1199
+ testId="select-focus-test"
1200
+ >
1201
+ <OptionItem label="item 1" value="1" />
1202
+ <OptionItem label="item 2" value="2" />
1203
+ <OptionItem label="item 3" value="3" />
1204
+ </SingleSelect>,
1205
+ );
1206
+
1207
+ // Act
1208
+ const singleSelect = screen.getByTestId("select-focus-test");
1209
+ singleSelect.focus();
1210
+
1211
+ // Assert
1212
+ expect(singleSelect).toHaveFocus();
1213
+ });
1214
+
1215
+ it("should be focusable when disabled", () => {
1216
+ // Arrange
1217
+ doRender(
1218
+ <SingleSelect
1219
+ placeholder="Default placeholder"
1220
+ onChange={jest.fn()}
1221
+ testId="select-focus-test"
1222
+ disabled={true}
1223
+ >
1224
+ <OptionItem label="item 1" value="1" />
1225
+ <OptionItem label="item 2" value="2" />
1226
+ <OptionItem label="item 3" value="3" />
1227
+ </SingleSelect>,
1228
+ );
1229
+
1230
+ // Act
1231
+ const singleSelect = screen.getByTestId("select-focus-test");
1232
+ singleSelect.focus();
1233
+
1234
+ // Assert
1235
+ expect(singleSelect).toHaveFocus();
1236
+ });
1237
+ });
1238
+
1239
+ describe("Disabled state", () => {
1240
+ it("should set the `aria-disabled` attribute to `true` if `disabled` prop is `true`", () => {
1241
+ // Arrange
1242
+
1243
+ // Act
1244
+ doRender(
1245
+ <SingleSelect
1246
+ placeholder="Default placeholder"
1247
+ onChange={jest.fn()}
1248
+ testId="select-focus-test"
1249
+ disabled={true}
1250
+ >
1251
+ <OptionItem label="item 1" value="1" />
1252
+ <OptionItem label="item 2" value="2" />
1253
+ <OptionItem label="item 3" value="3" />
1254
+ </SingleSelect>,
1128
1255
  );
1256
+ const singleSelect = screen.getByTestId("select-focus-test");
1257
+
1258
+ // Assert
1259
+ expect(singleSelect).toHaveAttribute("aria-disabled", "true");
1260
+ });
1261
+
1262
+ it("should not set the `disabled` attribute if `disabled` prop is `true` since `aria-disabled` is used instead", () => {
1263
+ // Arrange
1264
+
1265
+ // Act
1266
+ doRender(
1267
+ <SingleSelect
1268
+ placeholder="Default placeholder"
1269
+ onChange={jest.fn()}
1270
+ testId="select-focus-test"
1271
+ disabled={true}
1272
+ >
1273
+ <OptionItem label="item 1" value="1" />
1274
+ <OptionItem label="item 2" value="2" />
1275
+ <OptionItem label="item 3" value="3" />
1276
+ </SingleSelect>,
1277
+ );
1278
+ const singleSelect = screen.getByTestId("select-focus-test");
1279
+
1280
+ // Assert
1281
+ expect(singleSelect).not.toHaveAttribute("disabled");
1129
1282
  });
1130
1283
  });
1131
1284
  });
@@ -169,6 +169,10 @@ type ExportProps = Readonly<{
169
169
  * top. The items will be filtered by the input.
170
170
  */
171
171
  isFilterable?: boolean;
172
+ /**
173
+ * Whether the dropdown and it's interactions should be disabled.
174
+ */
175
+ disabled?: boolean;
172
176
 
173
177
  // Optional props with defaults
174
178
  /**
@@ -1040,12 +1044,12 @@ class DropdownCore extends React.Component<Props, State> {
1040
1044
  }
1041
1045
 
1042
1046
  render(): React.ReactNode {
1043
- const {open, opener, style, className} = this.props;
1047
+ const {open, opener, style, className, disabled} = this.props;
1044
1048
 
1045
1049
  return (
1046
1050
  <View
1047
- onKeyDown={this.handleKeyDown}
1048
- onKeyUp={this.handleKeyUp}
1051
+ onKeyDown={!disabled ? this.handleKeyDown : undefined}
1052
+ onKeyUp={!disabled ? this.handleKeyUp : undefined}
1049
1053
  style={[styles.menuWrapper, style]}
1050
1054
  className={className}
1051
1055
  >
@@ -241,7 +241,12 @@ export default class MultiSelect extends React.Component<Props, State> {
241
241
  state: State,
242
242
  ): Partial<State> | null {
243
243
  return {
244
- open: typeof props.opened === "boolean" ? props.opened : state.open,
244
+ // open should always be false if select is disabled
245
+ open: props.disabled
246
+ ? false
247
+ : typeof props.opened === "boolean"
248
+ ? props.opened
249
+ : state.open,
245
250
  };
246
251
  }
247
252
 
@@ -549,6 +554,7 @@ export default class MultiSelect extends React.Component<Props, State> {
549
554
  isFilterable,
550
555
  "aria-invalid": ariaInvalid,
551
556
  "aria-required": ariaRequired,
557
+ disabled,
552
558
  } = this.props;
553
559
  const {open, searchText} = this.state;
554
560
  const {clearSearch, filter, noResults, someSelected} =
@@ -599,6 +605,7 @@ export default class MultiSelect extends React.Component<Props, State> {
599
605
  }}
600
606
  aria-invalid={ariaInvalid}
601
607
  aria-required={ariaRequired}
608
+ disabled={disabled}
602
609
  />
603
610
  );
604
611
  }