@simplybusiness/mobius 5.4.0 → 5.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cjs/components/Checkbox/Checkbox.js +14 -5
  3. package/dist/cjs/components/Checkbox/Checkbox.js.map +1 -1
  4. package/dist/cjs/components/Combobox/Combobox.js +129 -0
  5. package/dist/cjs/components/Combobox/Combobox.js.map +1 -0
  6. package/dist/cjs/components/Combobox/fixtures.js +244 -0
  7. package/dist/cjs/components/Combobox/fixtures.js.map +1 -0
  8. package/dist/cjs/components/Combobox/index.js +21 -0
  9. package/dist/cjs/components/Combobox/index.js.map +1 -0
  10. package/dist/cjs/components/Combobox/types.js +6 -0
  11. package/dist/cjs/components/Combobox/types.js.map +1 -0
  12. package/dist/cjs/components/index.js +1 -0
  13. package/dist/cjs/components/index.js.map +1 -1
  14. package/dist/cjs/hooks/useTextField/useTextField.js +1 -0
  15. package/dist/cjs/hooks/useTextField/useTextField.js.map +1 -1
  16. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  17. package/dist/esm/components/Checkbox/Checkbox.js +15 -6
  18. package/dist/esm/components/Checkbox/Checkbox.js.map +1 -1
  19. package/dist/esm/components/Combobox/Combobox.js +114 -0
  20. package/dist/esm/components/Combobox/Combobox.js.map +1 -0
  21. package/dist/esm/components/Combobox/fixtures.js +226 -0
  22. package/dist/esm/components/Combobox/fixtures.js.map +1 -0
  23. package/dist/esm/components/Combobox/index.js +4 -0
  24. package/dist/esm/components/Combobox/index.js.map +1 -0
  25. package/dist/esm/components/Combobox/types.js +3 -0
  26. package/dist/esm/components/Combobox/types.js.map +1 -0
  27. package/dist/esm/components/index.js +1 -0
  28. package/dist/esm/components/index.js.map +1 -1
  29. package/dist/esm/hooks/useTextField/types.js.map +1 -1
  30. package/dist/esm/hooks/useTextField/useTextField.js +1 -0
  31. package/dist/esm/hooks/useTextField/useTextField.js.map +1 -1
  32. package/dist/types/components/Combobox/Combobox.d.ts +3 -0
  33. package/dist/types/components/Combobox/Combobox.stories.d.ts +7 -0
  34. package/dist/types/components/Combobox/Combobox.test.d.ts +1 -0
  35. package/dist/types/components/Combobox/fixtures.d.ts +5 -0
  36. package/dist/types/components/Combobox/index.d.ts +2 -0
  37. package/dist/types/components/Combobox/types.d.ts +16 -0
  38. package/dist/types/components/index.d.ts +1 -0
  39. package/dist/types/hooks/useTextField/types.d.ts +3 -2
  40. package/package.json +1 -1
  41. package/src/components/Checkbox/Checkbox.test.tsx +1 -2
  42. package/src/components/Checkbox/Checkbox.tsx +21 -7
  43. package/src/components/Combobox/Combobox.css +30 -0
  44. package/src/components/Combobox/Combobox.stories.tsx +26 -0
  45. package/src/components/Combobox/Combobox.test.tsx +578 -0
  46. package/src/components/Combobox/Combobox.tsx +154 -0
  47. package/src/components/Combobox/fixtures.tsx +93 -0
  48. package/src/components/Combobox/index.tsx +2 -0
  49. package/src/components/Combobox/types.tsx +20 -0
  50. package/src/components/index.tsx +1 -0
  51. package/src/hooks/useTextField/types.tsx +7 -1
  52. package/src/hooks/useTextField/useTextField.test.tsx +1 -0
  53. package/src/hooks/useTextField/useTextField.tsx +1 -0
@@ -0,0 +1,578 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { Combobox } from "./Combobox";
4
+ import { FRUITS, FRUITS_OBJECTS } from "./fixtures";
5
+
6
+ describe("Combobox with string options", () => {
7
+ it("should render without crashing", () => {
8
+ render(<Combobox options={FRUITS} />);
9
+ });
10
+
11
+ it("should use the default value provided", () => {
12
+ render(<Combobox options={FRUITS} defaultValue="Apple" />);
13
+ const input = screen.getByRole("combobox") as HTMLInputElement;
14
+ expect(input.value).toBe("Apple");
15
+ });
16
+
17
+ it("should render a combobox", () => {
18
+ render(<Combobox options={FRUITS} />);
19
+ const input = screen.getByRole("combobox");
20
+ expect(input).toBeInTheDocument();
21
+ });
22
+
23
+ it("should accept a ref and forward it to the input", () => {
24
+ const ref = { current: null };
25
+ render(<Combobox options={FRUITS} ref={ref} />);
26
+ expect(ref.current).not.toBeNull();
27
+ });
28
+
29
+ it("should not render a listbox by default", () => {
30
+ render(<Combobox options={FRUITS} />);
31
+ const listbox = screen.queryByRole("listbox");
32
+ expect(listbox).not.toBeInTheDocument();
33
+ });
34
+
35
+ it("should render a listbox when the input is focused", async () => {
36
+ render(<Combobox options={FRUITS} />);
37
+ const input = screen.getByRole("combobox");
38
+ await userEvent.click(input);
39
+ const listbox = screen.getByRole("listbox");
40
+ expect(listbox).toBeInTheDocument();
41
+ });
42
+
43
+ it("should render all of the options in the listbox", async () => {
44
+ render(<Combobox options={FRUITS} />);
45
+ const input = screen.getByRole("combobox");
46
+ await userEvent.click(input);
47
+ const options = screen.getAllByRole("option");
48
+ expect(options).toHaveLength(FRUITS.length);
49
+ });
50
+
51
+ it("should call the onSelected callback when an option is selected", async () => {
52
+ const onSelected = jest.fn();
53
+ render(<Combobox options={FRUITS} onSelected={onSelected} />);
54
+ const input = screen.getByRole("combobox");
55
+ await userEvent.click(input);
56
+ const options = screen.getAllByRole("option");
57
+ await userEvent.click(options[0]);
58
+ expect(onSelected).toHaveBeenCalledWith(FRUITS[0]);
59
+ });
60
+
61
+ it("should update the input value when an option is selected", async () => {
62
+ render(<Combobox options={FRUITS} />);
63
+ const input = screen.getByRole("combobox");
64
+ await userEvent.click(input);
65
+ const options = screen.getAllByRole("option");
66
+ await userEvent.click(options[0]);
67
+ expect(input).toHaveValue(FRUITS[0]);
68
+ });
69
+
70
+ it("should close the listbox when the input is blurred", async () => {
71
+ render(<Combobox options={FRUITS} />);
72
+ const input = screen.getByRole("combobox");
73
+ await userEvent.click(input);
74
+ await userEvent.tab();
75
+ const listbox = screen.queryByRole("listbox");
76
+ expect(listbox).not.toBeInTheDocument();
77
+ });
78
+
79
+ it("should close the listbox when an option is selected", async () => {
80
+ render(<Combobox options={FRUITS} />);
81
+ const input = screen.getByRole("combobox");
82
+ await userEvent.click(input);
83
+ const options = screen.getAllByRole("option");
84
+ await userEvent.click(options[0]);
85
+ const listbox = screen.queryByRole("listbox");
86
+ expect(listbox).not.toBeInTheDocument();
87
+ });
88
+
89
+ it("should filter the options based on the input value", async () => {
90
+ render(<Combobox options={FRUITS} />);
91
+ const input = screen.getByRole("combobox");
92
+ await userEvent.type(input, "Ap");
93
+ const options = screen.getAllByRole("option");
94
+ expect(options).toHaveLength(6);
95
+ });
96
+
97
+ it("should ignore case when filtering options", async () => {
98
+ render(<Combobox options={FRUITS} />);
99
+ const input = screen.getByRole("combobox");
100
+ await userEvent.type(input, "AP");
101
+ const options = screen.getAllByRole("option");
102
+ expect(options).toHaveLength(6);
103
+ });
104
+
105
+ it("should select the top option when pressing Enter", async () => {
106
+ const onSelected = jest.fn();
107
+ render(<Combobox options={FRUITS} onSelected={onSelected} />);
108
+ const input = screen.getByRole("combobox");
109
+ await userEvent.type(input, "Ap");
110
+ await userEvent.type(input, "{enter}");
111
+ expect(onSelected).toHaveBeenCalledWith("Apple");
112
+ });
113
+
114
+ it("should not select an option when pressing Enter with no options", async () => {
115
+ const onSelected = jest.fn();
116
+ render(<Combobox options={FRUITS} onSelected={onSelected} />);
117
+ const input = screen.getByRole("combobox");
118
+ await userEvent.type(input, "Zz");
119
+ await userEvent.type(input, "{enter}");
120
+ expect(onSelected).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it("should select the highlighted option when pressing Enter", async () => {
124
+ const onSelected = jest.fn();
125
+ render(<Combobox options={FRUITS} onSelected={onSelected} />);
126
+ const input = screen.getByRole("combobox");
127
+ await userEvent.click(input);
128
+ await userEvent.keyboard("{arrowdown}");
129
+ await userEvent.keyboard("{enter}");
130
+ expect(onSelected).toHaveBeenCalledWith(FRUITS[0]);
131
+ });
132
+
133
+ it("should open the listbox when pressing ArrowDown", async () => {
134
+ render(<Combobox options={FRUITS} />);
135
+ screen.getByRole("combobox");
136
+ await userEvent.tab();
137
+ await userEvent.keyboard("{arrowdown}");
138
+ const listbox = screen.getByRole("listbox");
139
+ expect(listbox).toBeInTheDocument();
140
+ });
141
+
142
+ it("should highlight the first option when pressing ArrowDown", async () => {
143
+ render(<Combobox options={FRUITS} />);
144
+ screen.getByRole("combobox");
145
+ await userEvent.tab();
146
+ await userEvent.keyboard("{arrowdown}");
147
+ const options = screen.getAllByRole("option");
148
+ expect(options[0]).toHaveAttribute("aria-selected", "true");
149
+ });
150
+
151
+ it("should open the listbox when pressing ArrowUp", async () => {
152
+ render(<Combobox options={FRUITS} />);
153
+ screen.getByRole("combobox");
154
+ await userEvent.tab();
155
+ await userEvent.keyboard("{arrowup}");
156
+ const listbox = screen.getByRole("listbox");
157
+ expect(listbox).toBeInTheDocument();
158
+ });
159
+
160
+ it("should highlight the last option when pressing ArrowUp", async () => {
161
+ render(<Combobox options={FRUITS} />);
162
+ screen.getByRole("combobox");
163
+ await userEvent.tab();
164
+ await userEvent.keyboard("{arrowup}");
165
+ const options = screen.getAllByRole("option");
166
+ expect(options[options.length - 1]).toHaveAttribute(
167
+ "aria-selected",
168
+ "true",
169
+ );
170
+ });
171
+
172
+ it("should close the listbox when pressing Escape", async () => {
173
+ render(<Combobox options={FRUITS} />);
174
+ const input = screen.getByRole("combobox");
175
+ await userEvent.click(input);
176
+ await userEvent.keyboard("{escape}");
177
+ const listbox = screen.queryByRole("listbox");
178
+ expect(listbox).not.toBeInTheDocument();
179
+ });
180
+
181
+ it("should not call onSelected when pressing Escape", async () => {
182
+ const onSelected = jest.fn();
183
+ render(<Combobox options={FRUITS} onSelected={onSelected} />);
184
+ const input = screen.getByRole("combobox");
185
+ await userEvent.click(input);
186
+ await userEvent.keyboard("{escape}");
187
+ expect(onSelected).not.toHaveBeenCalled();
188
+ });
189
+
190
+ it("should reset the highlighted index when the input value changes", async () => {
191
+ render(<Combobox options={FRUITS} />);
192
+ const input = screen.getByRole("combobox");
193
+ await userEvent.click(input);
194
+ await userEvent.keyboard("{arrowdown}");
195
+ await userEvent.type(input, "Ap");
196
+ const options = screen.getAllByRole("option");
197
+ expect(options[0]).not.toHaveAttribute("aria-selected", "true");
198
+ });
199
+
200
+ describe("classnames", () => {
201
+ it("should have the correct classnames for the combobox", () => {
202
+ const { container } = render(<Combobox options={FRUITS} />);
203
+ expect(container.firstChild).toHaveClass("mobius mobius-combobox");
204
+ });
205
+
206
+ it("should have the correct classnames for the input", () => {
207
+ render(<Combobox options={FRUITS} />);
208
+ const input = screen.getByRole("combobox");
209
+ expect(input).toHaveClass("mobius-combobox__input");
210
+ });
211
+
212
+ it("should have the correct classnames for the listbox", async () => {
213
+ render(<Combobox options={FRUITS} />);
214
+ const input = screen.getByRole("combobox");
215
+ await userEvent.click(input);
216
+ const listbox = screen.getByRole("listbox");
217
+ expect(listbox).toHaveClass("mobius-combobox__list");
218
+ });
219
+
220
+ it("should have the correct classnames for the options", async () => {
221
+ render(<Combobox options={FRUITS} />);
222
+ const input = screen.getByRole("combobox");
223
+ await userEvent.click(input);
224
+ const options = screen.getAllByRole("option");
225
+ options.forEach(option => {
226
+ expect(option).toHaveClass("mobius-combobox__option");
227
+ });
228
+ });
229
+
230
+ it("should apply the highlighted class to the highlighted option", async () => {
231
+ render(<Combobox options={FRUITS} />);
232
+ const input = screen.getByRole("combobox");
233
+ await userEvent.click(input);
234
+ await userEvent.keyboard("{arrowdown}");
235
+ const options = screen.getAllByRole("option");
236
+ expect(options[0]).toHaveClass("mobius-combobox__option--is-highlighted");
237
+ });
238
+ });
239
+
240
+ describe("ARIA", () => {
241
+ it("should set aria-autocomplete to list", () => {
242
+ render(<Combobox options={FRUITS} />);
243
+ const input = screen.getByRole("combobox");
244
+ expect(input).toHaveAttribute("aria-autocomplete", "list");
245
+ });
246
+
247
+ it("should set aria-haspopup to listbox", () => {
248
+ render(<Combobox options={FRUITS} />);
249
+ const input = screen.getByRole("combobox");
250
+ expect(input).toHaveAttribute("aria-haspopup", "listbox");
251
+ });
252
+
253
+ it("should set aria-controls to the listbox id", async () => {
254
+ render(<Combobox options={FRUITS} />);
255
+ const input = screen.getByRole("combobox");
256
+ await userEvent.click(input);
257
+ const listbox = screen.getByRole("listbox");
258
+ expect(input).toHaveAttribute("aria-controls", listbox.id);
259
+ });
260
+
261
+ it("should set aria-expanded to false by default", () => {
262
+ render(<Combobox options={FRUITS} />);
263
+ const input = screen.getByRole("combobox");
264
+ expect(input).toHaveAttribute("aria-expanded", "false");
265
+ });
266
+
267
+ it("should set aria-expanded to true when the listbox is open", async () => {
268
+ render(<Combobox options={FRUITS} />);
269
+ const input = screen.getByRole("combobox");
270
+ await userEvent.click(input);
271
+ expect(input).toHaveAttribute("aria-expanded", "true");
272
+ });
273
+
274
+ it("should set aria-selected to false for all options", async () => {
275
+ render(<Combobox options={FRUITS} />);
276
+ const input = screen.getByRole("combobox");
277
+ await userEvent.click(input);
278
+ const options = screen.getAllByRole("option");
279
+ options.forEach(option => {
280
+ expect(option).toHaveAttribute("aria-selected", "false");
281
+ });
282
+ });
283
+
284
+ it("should set aria-selected to true for the highlighted option", async () => {
285
+ render(<Combobox options={FRUITS} />);
286
+ const input = screen.getByRole("combobox");
287
+ await userEvent.click(input);
288
+ const options = screen.getAllByRole("option");
289
+ await userEvent.keyboard("{arrowdown}{arrowdown}");
290
+ expect(options[1]).toHaveAttribute("aria-selected", "true");
291
+ });
292
+
293
+ it("should set aria-activedescendant to undefined by default", () => {
294
+ render(<Combobox options={FRUITS} />);
295
+ const input = screen.getByRole("combobox");
296
+ expect(input).not.toHaveAttribute("aria-activedescendant");
297
+ });
298
+
299
+ it("should set aria-activedescendant based on the highlightedIndex", async () => {
300
+ render(<Combobox options={FRUITS} />);
301
+ const input = screen.getByRole("combobox");
302
+ await userEvent.tab();
303
+ const options = screen.getAllByRole("option");
304
+ await userEvent.keyboard("{arrowdown}");
305
+ expect(input).toHaveAttribute(
306
+ "aria-activedescendant",
307
+ `${options[0].id}`,
308
+ );
309
+ });
310
+ });
311
+ });
312
+
313
+ describe("Combobox with object options", () => {
314
+ it("should render without crashing", () => {
315
+ render(<Combobox options={FRUITS_OBJECTS} />);
316
+ });
317
+
318
+ it("should use the default value provided", () => {
319
+ render(<Combobox options={FRUITS_OBJECTS} defaultValue="apple" />);
320
+ const input = screen.getByRole("combobox") as HTMLInputElement;
321
+ expect(input.value).toBe("apple");
322
+ });
323
+
324
+ it("should render a combobox", () => {
325
+ render(<Combobox options={FRUITS_OBJECTS} />);
326
+ const input = screen.getByRole("combobox");
327
+ expect(input).toBeInTheDocument();
328
+ });
329
+
330
+ it("should accept a ref and forward it to the input", () => {
331
+ const ref = { current: null };
332
+ render(<Combobox options={FRUITS_OBJECTS} ref={ref} />);
333
+ expect(ref.current).not.toBeNull();
334
+ });
335
+
336
+ it("should not render a listbox by default", () => {
337
+ render(<Combobox options={FRUITS_OBJECTS} />);
338
+ const listbox = screen.queryByRole("listbox");
339
+ expect(listbox).not.toBeInTheDocument();
340
+ });
341
+
342
+ it("should render a listbox when the input is focused", async () => {
343
+ render(<Combobox options={FRUITS_OBJECTS} />);
344
+ const input = screen.getByRole("combobox");
345
+ await userEvent.click(input);
346
+ const listbox = screen.getByRole("listbox");
347
+ expect(listbox).toBeInTheDocument();
348
+ });
349
+
350
+ it("should render all of the options in the listbox", async () => {
351
+ render(<Combobox options={FRUITS_OBJECTS} />);
352
+ const input = screen.getByRole("combobox");
353
+ await userEvent.click(input);
354
+ const options = screen.getAllByRole("option");
355
+ expect(options).toHaveLength(FRUITS_OBJECTS.length);
356
+ });
357
+
358
+ it("should call the onSelected callback when an option is selected", async () => {
359
+ const onSelected = jest.fn();
360
+ render(<Combobox options={FRUITS_OBJECTS} onSelected={onSelected} />);
361
+ const input = screen.getByRole("combobox");
362
+ await userEvent.click(input);
363
+ const options = screen.getAllByRole("option");
364
+ await userEvent.click(options[0]);
365
+ expect(onSelected).toHaveBeenCalledWith(FRUITS_OBJECTS[0].value);
366
+ });
367
+
368
+ it("should update the input value when an option is selected", async () => {
369
+ render(<Combobox options={FRUITS_OBJECTS} />);
370
+ const input = screen.getByRole("combobox");
371
+ await userEvent.click(input);
372
+ const options = screen.getAllByRole("option");
373
+ await userEvent.click(options[0]);
374
+ expect(input).toHaveValue(FRUITS_OBJECTS[0].value);
375
+ });
376
+
377
+ it("should close the listbox when the input is blurred", async () => {
378
+ render(<Combobox options={FRUITS_OBJECTS} />);
379
+ const input = screen.getByRole("combobox");
380
+ await userEvent.click(input);
381
+ await userEvent.tab();
382
+ const listbox = screen.queryByRole("listbox");
383
+ expect(listbox).not.toBeInTheDocument();
384
+ });
385
+
386
+ it("should close the listbox when an option is selected", async () => {
387
+ render(<Combobox options={FRUITS_OBJECTS} />);
388
+ const input = screen.getByRole("combobox");
389
+ await userEvent.click(input);
390
+ const options = screen.getAllByRole("option");
391
+ await userEvent.click(options[0]);
392
+ const listbox = screen.queryByRole("listbox");
393
+ expect(listbox).not.toBeInTheDocument();
394
+ });
395
+
396
+ it("should filter the options based on the input value", async () => {
397
+ render(<Combobox options={FRUITS_OBJECTS} />);
398
+ const input = screen.getByRole("combobox");
399
+ await userEvent.type(input, "Ap");
400
+ const options = screen.getAllByRole("option");
401
+ expect(options).toHaveLength(6);
402
+ });
403
+
404
+ it("should ignore case when filtering options", async () => {
405
+ render(<Combobox options={FRUITS_OBJECTS} />);
406
+ const input = screen.getByRole("combobox");
407
+ await userEvent.type(input, "AP");
408
+ const options = screen.getAllByRole("option");
409
+ expect(options).toHaveLength(6);
410
+ });
411
+
412
+ it("should select the top option when pressing Enter", async () => {
413
+ const onSelected = jest.fn();
414
+ render(<Combobox options={FRUITS_OBJECTS} onSelected={onSelected} />);
415
+ const input = screen.getByRole("combobox");
416
+ await userEvent.type(input, "Ap");
417
+ await userEvent.type(input, "{enter}");
418
+ expect(onSelected).toHaveBeenCalledWith(FRUITS_OBJECTS[0].value);
419
+ });
420
+
421
+ it("should not select an option when pressing Enter with no options", async () => {
422
+ const onSelected = jest.fn();
423
+ render(<Combobox options={FRUITS_OBJECTS} onSelected={onSelected} />);
424
+ const input = screen.getByRole("combobox");
425
+ await userEvent.type(input, "Zz");
426
+ await userEvent.type(input, "{enter}");
427
+ expect(onSelected).not.toHaveBeenCalled();
428
+ });
429
+
430
+ it("should select the highlighted option when pressing Enter", async () => {
431
+ const onSelected = jest.fn();
432
+ render(<Combobox options={FRUITS_OBJECTS} onSelected={onSelected} />);
433
+ const input = screen.getByRole("combobox");
434
+ await userEvent.click(input);
435
+ await userEvent.keyboard("{arrowdown}");
436
+ await userEvent.keyboard("{enter}");
437
+ expect(onSelected).toHaveBeenCalledWith(FRUITS_OBJECTS[0].value);
438
+ });
439
+
440
+ it("should open the listbox when pressing ArrowDown", async () => {
441
+ render(<Combobox options={FRUITS_OBJECTS} />);
442
+ screen.getByRole("combobox");
443
+ await userEvent.tab();
444
+ await userEvent.keyboard("{arrowdown}");
445
+ const listbox = screen.getByRole("listbox");
446
+ expect(listbox).toBeInTheDocument();
447
+ });
448
+
449
+ it("should highlight the first option when pressing ArrowDown", async () => {
450
+ render(<Combobox options={FRUITS_OBJECTS} />);
451
+ screen.getByRole("combobox");
452
+ await userEvent.tab();
453
+ await userEvent.keyboard("{arrowdown}");
454
+ const options = screen.getAllByRole("option");
455
+ expect(options[0]).toHaveAttribute("aria-selected", "true");
456
+ });
457
+
458
+ it("should open the listbox when pressing ArrowUp", async () => {
459
+ render(<Combobox options={FRUITS_OBJECTS} />);
460
+ screen.getByRole("combobox");
461
+ await userEvent.tab();
462
+ await userEvent.keyboard("{arrowup}");
463
+ const listbox = screen.getByRole("listbox");
464
+ expect(listbox).toBeInTheDocument();
465
+ });
466
+
467
+ it("should highlight the last option when pressing ArrowUp", async () => {
468
+ render(<Combobox options={FRUITS_OBJECTS} />);
469
+ screen.getByRole("combobox");
470
+ await userEvent.tab();
471
+ await userEvent.keyboard("{arrowup}");
472
+ const options = screen.getAllByRole("option");
473
+ expect(options[options.length - 1]).toHaveAttribute(
474
+ "aria-selected",
475
+ "true",
476
+ );
477
+ });
478
+
479
+ it("should close the listbox when pressing Escape", async () => {
480
+ render(<Combobox options={FRUITS_OBJECTS} />);
481
+ const input = screen.getByRole("combobox");
482
+ await userEvent.click(input);
483
+ await userEvent.keyboard("{escape}");
484
+ const listbox = screen.queryByRole("listbox");
485
+ expect(listbox).not.toBeInTheDocument();
486
+ });
487
+
488
+ it("should not call onSelected when pressing Escape", async () => {
489
+ const onSelected = jest.fn();
490
+ render(<Combobox options={FRUITS_OBJECTS} onSelected={onSelected} />);
491
+ const input = screen.getByRole("combobox");
492
+ await userEvent.click(input);
493
+ await userEvent.keyboard("{escape}");
494
+ expect(onSelected).not.toHaveBeenCalled();
495
+ });
496
+
497
+ it("should reset the highlighted index when the input value changes", async () => {
498
+ render(<Combobox options={FRUITS_OBJECTS} />);
499
+ const input = screen.getByRole("combobox");
500
+ await userEvent.click(input);
501
+ await userEvent.keyboard("{arrowdown}");
502
+ await userEvent.type(input, "Ap");
503
+ const options = screen.getAllByRole("option");
504
+ expect(options[0]).not.toHaveAttribute("aria-selected", "true");
505
+ });
506
+
507
+ describe("ARIA", () => {
508
+ it("should set aria-autocomplete to list", () => {
509
+ render(<Combobox options={FRUITS_OBJECTS} />);
510
+ const input = screen.getByRole("combobox");
511
+ expect(input).toHaveAttribute("aria-autocomplete", "list");
512
+ });
513
+
514
+ it("should set aria-haspopup to listbox", () => {
515
+ render(<Combobox options={FRUITS_OBJECTS} />);
516
+ const input = screen.getByRole("combobox");
517
+ expect(input).toHaveAttribute("aria-haspopup", "listbox");
518
+ });
519
+
520
+ it("should set aria-controls to the listbox id", async () => {
521
+ render(<Combobox options={FRUITS_OBJECTS} />);
522
+ const input = screen.getByRole("combobox");
523
+ await userEvent.click(input);
524
+ const listbox = screen.getByRole("listbox");
525
+ expect(input).toHaveAttribute("aria-controls", listbox.id);
526
+ });
527
+
528
+ it("should set aria-expanded to false by default", () => {
529
+ render(<Combobox options={FRUITS_OBJECTS} />);
530
+ const input = screen.getByRole("combobox");
531
+ expect(input).toHaveAttribute("aria-expanded", "false");
532
+ });
533
+
534
+ it("should set aria-expanded to true when the listbox is open", async () => {
535
+ render(<Combobox options={FRUITS_OBJECTS} />);
536
+ const input = screen.getByRole("combobox");
537
+ await userEvent.click(input);
538
+ expect(input).toHaveAttribute("aria-expanded", "true");
539
+ });
540
+
541
+ it("should set aria-selected to false for all options", async () => {
542
+ render(<Combobox options={FRUITS_OBJECTS} />);
543
+ const input = screen.getByRole("combobox");
544
+ await userEvent.click(input);
545
+ const options = screen.getAllByRole("option");
546
+ options.forEach(option => {
547
+ expect(option).toHaveAttribute("aria-selected", "false");
548
+ });
549
+ });
550
+
551
+ it("should set aria-selected to true for the highlighted option", async () => {
552
+ render(<Combobox options={FRUITS_OBJECTS} />);
553
+ const input = screen.getByRole("combobox");
554
+ await userEvent.click(input);
555
+ const options = screen.getAllByRole("option");
556
+ await userEvent.keyboard("{arrowdown}{arrowdown}");
557
+ expect(options[1]).toHaveAttribute("aria-selected", "true");
558
+ });
559
+
560
+ it("should set aria-activedescendant to undefined by default", () => {
561
+ render(<Combobox options={FRUITS_OBJECTS} />);
562
+ const input = screen.getByRole("combobox");
563
+ expect(input).not.toHaveAttribute("aria-activedescendant");
564
+ });
565
+
566
+ it("should set aria-activedescendant based on the highlightedIndex", async () => {
567
+ render(<Combobox options={FRUITS_OBJECTS} />);
568
+ const input = screen.getByRole("combobox");
569
+ await userEvent.tab();
570
+ const options = screen.getAllByRole("option");
571
+ await userEvent.keyboard("{arrowdown}");
572
+ expect(input).toHaveAttribute(
573
+ "aria-activedescendant",
574
+ `${options[0].id}`,
575
+ );
576
+ });
577
+ });
578
+ });