@arbor-education/design-system.components 0.8.1 → 0.10.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.
- package/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +22 -0
- package/dist/components/button/Button.d.ts.map +1 -1
- package/dist/components/button/Button.js +2 -2
- package/dist/components/button/Button.js.map +1 -1
- package/dist/components/combobox/Combobox.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.js +10 -8
- package/dist/components/combobox/Combobox.js.map +1 -1
- package/dist/components/combobox/Combobox.stories.d.ts +1 -0
- package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.stories.js +16 -0
- package/dist/components/combobox/Combobox.stories.js.map +1 -1
- package/dist/components/combobox/Combobox.test.js +107 -61
- package/dist/components/combobox/Combobox.test.js.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts +4 -2
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.js +11 -4
- package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.d.ts +3 -1
- package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.js +10 -2
- package/dist/components/combobox/ComboboxTrigger.js.map +1 -1
- package/dist/components/combobox/types.d.ts +3 -0
- package/dist/components/combobox/types.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +3 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxPopoverBehavior.js +7 -6
- package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -1
- package/dist/components/combobox/useComboboxState.d.ts.map +1 -1
- package/dist/components/combobox/useComboboxState.js +4 -1
- package/dist/components/combobox/useComboboxState.js.map +1 -1
- package/dist/components/datePicker/DatePicker.d.ts +4 -1
- package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.js +77 -37
- package/dist/components/datePicker/DatePicker.js.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.d.ts +28 -3
- package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
- package/dist/components/datePicker/DatePicker.stories.js +62 -9
- package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
- package/dist/components/datePicker/DatePicker.test.js +133 -66
- package/dist/components/datePicker/DatePicker.test.js.map +1 -1
- package/dist/components/datePicker/DatePickerCalendarHeader.d.ts +8 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.d.ts.map +1 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.js +36 -0
- package/dist/components/datePicker/DatePickerCalendarHeader.js.map +1 -0
- package/dist/components/datePicker/dateInputUtils.d.ts +25 -0
- package/dist/components/datePicker/dateInputUtils.d.ts.map +1 -0
- package/dist/components/datePicker/dateInputUtils.js +60 -0
- package/dist/components/datePicker/dateInputUtils.js.map +1 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts +2 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts.map +1 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.js +4 -0
- package/dist/components/datePicker/datePickerTestUtils.test-helpers.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts +22 -0
- package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.js +132 -0
- package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +77 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.js +163 -0
- package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.d.ts +2 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.d.ts.map +1 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.js +235 -0
- package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -0
- package/dist/components/formField/FormField.d.ts +4 -0
- package/dist/components/formField/FormField.d.ts.map +1 -1
- package/dist/components/formField/FormField.js +2 -1
- package/dist/components/formField/FormField.js.map +1 -1
- package/dist/components/formField/FormField.stories.d.ts.map +1 -1
- package/dist/components/formField/FormField.stories.js +4 -1
- package/dist/components/formField/FormField.stories.js.map +1 -1
- package/dist/components/formField/FormField.test.d.ts.map +1 -1
- package/dist/components/formField/FormField.test.js +10 -5
- package/dist/components/formField/FormField.test.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +7 -3
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +12 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.d.ts +4 -1
- package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.js +5 -4
- package/dist/components/formField/inputs/text/TextInput.js.map +1 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts +4 -1
- package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts +29 -0
- package/dist/components/formField/inputs/time/TimeInput.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.js +67 -0
- package/dist/components/formField/inputs/time/TimeInput.js.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +60 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.js +132 -0
- package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.test.d.ts +2 -0
- package/dist/components/formField/inputs/time/TimeInput.test.d.ts.map +1 -0
- package/dist/components/formField/inputs/time/TimeInput.test.js +58 -0
- package/dist/components/formField/inputs/time/TimeInput.test.js.map +1 -0
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +2 -0
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +1 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +37 -0
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts +3 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.js +15 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.js.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts +2 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js +31 -0
- package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js.map +1 -0
- package/dist/index.css +309 -4
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/button/Button.tsx +2 -1
- package/src/components/combobox/Combobox.stories.tsx +18 -0
- package/src/components/combobox/Combobox.test.tsx +131 -61
- package/src/components/combobox/Combobox.tsx +15 -6
- package/src/components/combobox/ComboboxButtonTrigger.tsx +54 -25
- package/src/components/combobox/ComboboxTrigger.tsx +39 -15
- package/src/components/combobox/combobox.scss +18 -0
- package/src/components/combobox/types.ts +3 -0
- package/src/components/combobox/useComboboxPopoverBehavior.ts +10 -5
- package/src/components/combobox/useComboboxState.ts +4 -1
- package/src/components/datePicker/DatePicker.stories.tsx +67 -9
- package/src/components/datePicker/DatePicker.test.tsx +157 -72
- package/src/components/datePicker/DatePicker.tsx +163 -69
- package/src/components/datePicker/DatePickerCalendarHeader.tsx +82 -0
- package/src/components/datePicker/date-field-hint.scss +152 -0
- package/src/components/datePicker/dateInputUtils.ts +117 -0
- package/src/components/datePicker/datePicker.scss +53 -29
- package/src/components/datePicker/datePickerTestUtils.test-helpers.ts +6 -0
- package/src/components/dateTimePicker/DateTimePicker.stories.tsx +202 -0
- package/src/components/dateTimePicker/DateTimePicker.test.tsx +295 -0
- package/src/components/dateTimePicker/DateTimePicker.tsx +293 -0
- package/src/components/dateTimePicker/dateTimePicker.scss +17 -0
- package/src/components/formField/FormField.stories.tsx +10 -1
- package/src/components/formField/FormField.test.tsx +11 -5
- package/src/components/formField/FormField.tsx +5 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +28 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +8 -2
- package/src/components/formField/inputs/text/TextInput.tsx +6 -3
- package/src/components/formField/inputs/time/TimeInput.stories.tsx +170 -0
- package/src/components/formField/inputs/time/TimeInput.test.tsx +86 -0
- package/src/components/formField/inputs/time/TimeInput.tsx +168 -0
- package/src/components/formField/inputs/time/timeInput.scss +33 -0
- package/src/components/row/row.scss +2 -2
- package/src/components/table/Table.stories.tsx +48 -0
- package/src/components/table/Table.tsx +2 -0
- package/src/components/table/cellRenderers/BooleanCellRenderer.test.tsx +37 -0
- package/src/components/table/cellRenderers/BooleanCellRenderer.tsx +34 -0
- package/src/components/table/cellRenderers/booleanCellRenderer.scss +7 -0
- package/src/index.scss +3 -0
- package/src/index.ts +5 -0
|
@@ -147,6 +147,30 @@ describe('Combobox', () => {
|
|
|
147
147
|
expect(search).toHaveFocus();
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
test('button trigger keeps popover with search input when filter matches nothing', async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
render(<Combobox options={people} triggerVariant="button" placeholder="Students" />);
|
|
153
|
+
await user.click(screen.getByRole('button', { name: 'Open suggestions' }));
|
|
154
|
+
|
|
155
|
+
const search = screen.getByRole('combobox');
|
|
156
|
+
await user.type(search, 'zzz');
|
|
157
|
+
|
|
158
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
159
|
+
expect(screen.queryAllByRole('option')).toHaveLength(0);
|
|
160
|
+
|
|
161
|
+
await user.clear(search);
|
|
162
|
+
expect(within(screen.getByRole('listbox')).getAllByRole('option')).toHaveLength(people.length);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('button trigger closes popover after single-select option click', async () => {
|
|
166
|
+
const user = userEvent.setup();
|
|
167
|
+
render(<Combobox options={people} triggerVariant="button" placeholder="Students" />);
|
|
168
|
+
await user.click(screen.getByRole('button', { name: 'Open suggestions' }));
|
|
169
|
+
await user.click(screen.getByText('Alice Johnson'));
|
|
170
|
+
|
|
171
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
150
174
|
test('button trigger single-select does not show selection count badge', () => {
|
|
151
175
|
render(
|
|
152
176
|
<Combobox
|
|
@@ -158,6 +182,33 @@ describe('Combobox', () => {
|
|
|
158
182
|
expect(screen.queryByLabelText(/selected item/i)).not.toBeInTheDocument();
|
|
159
183
|
});
|
|
160
184
|
|
|
185
|
+
test('button trigger can render plain text instead of tags for a selected value', () => {
|
|
186
|
+
render(
|
|
187
|
+
<Combobox
|
|
188
|
+
options={people}
|
|
189
|
+
triggerVariant="button"
|
|
190
|
+
selectedValueDisplay="text"
|
|
191
|
+
defaultValue={['alice']}
|
|
192
|
+
/>,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(screen.getByRole('button', { name: /alice johnson/i })).toHaveTextContent('Alice Johnson');
|
|
196
|
+
expect(document.querySelector('.ds-tag')).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('renders custom trigger end content alongside the trigger', () => {
|
|
200
|
+
render(
|
|
201
|
+
<Combobox
|
|
202
|
+
options={people}
|
|
203
|
+
triggerVariant="button"
|
|
204
|
+
showDropdownTrigger={false}
|
|
205
|
+
triggerEndContent={<span>clock</span>}
|
|
206
|
+
/>,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(screen.getByText('clock')).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
|
|
161
212
|
test('button trigger multi-select shows salmon count badge when multiple chips', () => {
|
|
162
213
|
render(
|
|
163
214
|
<Combobox
|
|
@@ -326,20 +377,22 @@ describe('Combobox', () => {
|
|
|
326
377
|
});
|
|
327
378
|
|
|
328
379
|
test('Ctrl+A selects all chips when input is focused', async () => {
|
|
380
|
+
const user = userEvent.setup();
|
|
329
381
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
330
382
|
const input = screen.getByRole('combobox');
|
|
331
|
-
|
|
383
|
+
await user.click(input);
|
|
332
384
|
|
|
333
|
-
await
|
|
385
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
334
386
|
|
|
335
387
|
const selectedTags = document.querySelectorAll('.ds-tag--selected');
|
|
336
388
|
expect(selectedTags).toHaveLength(2);
|
|
337
389
|
});
|
|
338
390
|
|
|
339
391
|
test('Cmd+A selects all chips in single-select mode', async () => {
|
|
392
|
+
const user = userEvent.setup();
|
|
340
393
|
render(<Combobox options={people} defaultValue={['alice']} />);
|
|
341
394
|
const input = screen.getByRole('combobox');
|
|
342
|
-
|
|
395
|
+
await user.click(input);
|
|
343
396
|
|
|
344
397
|
fireEvent.keyDown(input, { key: 'a', metaKey: true });
|
|
345
398
|
|
|
@@ -348,54 +401,58 @@ describe('Combobox', () => {
|
|
|
348
401
|
});
|
|
349
402
|
|
|
350
403
|
test('Backspace clears all selected chips after select-all', async () => {
|
|
404
|
+
const user = userEvent.setup();
|
|
351
405
|
const onValueChange = vi.fn();
|
|
352
406
|
render(
|
|
353
407
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
354
408
|
);
|
|
355
409
|
const input = screen.getByRole('combobox');
|
|
356
|
-
|
|
410
|
+
await user.click(input);
|
|
357
411
|
|
|
358
|
-
await
|
|
359
|
-
await
|
|
412
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
413
|
+
await user.keyboard('{Backspace}');
|
|
360
414
|
|
|
361
415
|
expect(onValueChange).toHaveBeenLastCalledWith([]);
|
|
362
416
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
363
417
|
});
|
|
364
418
|
|
|
365
419
|
test('Delete clears all selected chips after select-all', async () => {
|
|
420
|
+
const user = userEvent.setup();
|
|
366
421
|
const onValueChange = vi.fn();
|
|
367
422
|
render(
|
|
368
423
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
369
424
|
);
|
|
370
425
|
const input = screen.getByRole('combobox');
|
|
371
|
-
|
|
426
|
+
await user.click(input);
|
|
372
427
|
|
|
373
|
-
await
|
|
374
|
-
await
|
|
428
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
429
|
+
await user.keyboard('{Delete}');
|
|
375
430
|
|
|
376
431
|
expect(onValueChange).toHaveBeenLastCalledWith([]);
|
|
377
432
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
378
433
|
});
|
|
379
434
|
|
|
380
435
|
test('typing after chip select-all clears selected chip styling', async () => {
|
|
436
|
+
const user = userEvent.setup();
|
|
381
437
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
382
438
|
const input = screen.getByRole('combobox');
|
|
383
|
-
|
|
439
|
+
await user.click(input);
|
|
384
440
|
|
|
385
|
-
await
|
|
441
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
386
442
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(2);
|
|
387
443
|
|
|
388
|
-
await
|
|
444
|
+
await user.type(input, 'c');
|
|
389
445
|
|
|
390
446
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
391
447
|
});
|
|
392
448
|
|
|
393
449
|
test('Ctrl+A with no chips keeps native input behavior and does not crash', async () => {
|
|
450
|
+
const user = userEvent.setup();
|
|
394
451
|
render(<Combobox options={people} />);
|
|
395
452
|
const input = screen.getByRole('combobox');
|
|
396
|
-
|
|
453
|
+
await user.click(input);
|
|
397
454
|
|
|
398
|
-
await
|
|
455
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
399
456
|
|
|
400
457
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
401
458
|
});
|
|
@@ -555,21 +612,23 @@ describe('Combobox', () => {
|
|
|
555
612
|
});
|
|
556
613
|
|
|
557
614
|
test('keyboard: ArrowDown opens the listbox', async () => {
|
|
615
|
+
const user = userEvent.setup();
|
|
558
616
|
render(<Combobox options={people} />);
|
|
559
617
|
const input = screen.getByRole('combobox');
|
|
560
|
-
|
|
618
|
+
await user.click(input);
|
|
561
619
|
|
|
562
|
-
await
|
|
620
|
+
await user.keyboard('{ArrowDown}');
|
|
563
621
|
|
|
564
622
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
565
623
|
});
|
|
566
624
|
|
|
567
625
|
test('keyboard: Alt+ArrowDown opens the listbox', async () => {
|
|
626
|
+
const user = userEvent.setup();
|
|
568
627
|
render(<Combobox options={people} />);
|
|
569
628
|
const input = screen.getByRole('combobox');
|
|
570
|
-
|
|
629
|
+
await user.click(input);
|
|
571
630
|
|
|
572
|
-
await
|
|
631
|
+
await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
|
|
573
632
|
|
|
574
633
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
575
634
|
});
|
|
@@ -612,14 +671,15 @@ describe('Combobox', () => {
|
|
|
612
671
|
});
|
|
613
672
|
|
|
614
673
|
test('keyboard: Backspace on empty input removes last chip', async () => {
|
|
674
|
+
const user = userEvent.setup();
|
|
615
675
|
const onValueChange = vi.fn();
|
|
616
676
|
render(
|
|
617
677
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
618
678
|
);
|
|
619
679
|
const input = screen.getByRole('combobox');
|
|
620
|
-
|
|
680
|
+
await user.click(input);
|
|
621
681
|
|
|
622
|
-
await
|
|
682
|
+
await user.keyboard('{Backspace}');
|
|
623
683
|
|
|
624
684
|
expect(onValueChange).toHaveBeenCalledWith(['alice']);
|
|
625
685
|
});
|
|
@@ -695,7 +755,7 @@ describe('Combobox', () => {
|
|
|
695
755
|
|
|
696
756
|
expect(onCreateNew).toHaveBeenCalledWith('Taylor');
|
|
697
757
|
expect(onValueChange).toHaveBeenCalledWith(['new-taylor']);
|
|
698
|
-
expect(screen.
|
|
758
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
699
759
|
});
|
|
700
760
|
|
|
701
761
|
test('allowCreate duplicate prevention selects existing option', async () => {
|
|
@@ -1017,131 +1077,141 @@ describe('Combobox', () => {
|
|
|
1017
1077
|
});
|
|
1018
1078
|
|
|
1019
1079
|
describe('chip keyboard navigation', () => {
|
|
1020
|
-
test('ArrowLeft at caret 0 focuses the last chip', () => {
|
|
1080
|
+
test('ArrowLeft at caret 0 focuses the last chip', async () => {
|
|
1081
|
+
const user = userEvent.setup();
|
|
1021
1082
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1022
1083
|
const input = screen.getByRole('combobox');
|
|
1023
|
-
|
|
1084
|
+
await user.click(input);
|
|
1024
1085
|
|
|
1025
|
-
|
|
1086
|
+
await user.keyboard('{ArrowLeft}');
|
|
1026
1087
|
|
|
1027
1088
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1028
1089
|
expect(tags[1]).toHaveClass('ds-tag--selected');
|
|
1029
1090
|
expect(tags[0]).not.toHaveClass('ds-tag--selected');
|
|
1030
1091
|
});
|
|
1031
1092
|
|
|
1032
|
-
test('ArrowLeft navigates through chips from right to left', () => {
|
|
1093
|
+
test('ArrowLeft navigates through chips from right to left', async () => {
|
|
1094
|
+
const user = userEvent.setup();
|
|
1033
1095
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob', 'charlie']} />);
|
|
1034
1096
|
const input = screen.getByRole('combobox');
|
|
1035
|
-
|
|
1097
|
+
await user.click(input);
|
|
1036
1098
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1099
|
+
await user.keyboard('{ArrowLeft}');
|
|
1100
|
+
await user.keyboard('{ArrowLeft}');
|
|
1039
1101
|
|
|
1040
1102
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1041
1103
|
expect(tags[1]).toHaveClass('ds-tag--selected');
|
|
1042
1104
|
expect(tags[2]).not.toHaveClass('ds-tag--selected');
|
|
1043
1105
|
});
|
|
1044
1106
|
|
|
1045
|
-
test('ArrowLeft does not go past the first chip', () => {
|
|
1107
|
+
test('ArrowLeft does not go past the first chip', async () => {
|
|
1108
|
+
const user = userEvent.setup();
|
|
1046
1109
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1047
1110
|
const input = screen.getByRole('combobox');
|
|
1048
|
-
|
|
1111
|
+
await user.click(input);
|
|
1049
1112
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1113
|
+
await user.keyboard('{ArrowLeft}');
|
|
1114
|
+
await user.keyboard('{ArrowLeft}');
|
|
1115
|
+
await user.keyboard('{ArrowLeft}');
|
|
1053
1116
|
|
|
1054
1117
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1055
1118
|
expect(tags[0]).toHaveClass('ds-tag--selected');
|
|
1056
1119
|
});
|
|
1057
1120
|
|
|
1058
|
-
test('ArrowRight past the last chip returns focus to input (exits chip nav)', () => {
|
|
1121
|
+
test('ArrowRight past the last chip returns focus to input (exits chip nav)', async () => {
|
|
1122
|
+
const user = userEvent.setup();
|
|
1059
1123
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1060
1124
|
const input = screen.getByRole('combobox');
|
|
1061
|
-
|
|
1125
|
+
await user.click(input);
|
|
1062
1126
|
|
|
1063
|
-
|
|
1127
|
+
await user.keyboard('{ArrowLeft}');
|
|
1064
1128
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1065
1129
|
|
|
1066
|
-
|
|
1130
|
+
await user.keyboard('{ArrowRight}');
|
|
1067
1131
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1068
1132
|
});
|
|
1069
1133
|
|
|
1070
|
-
test('Backspace on focused chip removes it', () => {
|
|
1134
|
+
test('Backspace on focused chip removes it', async () => {
|
|
1135
|
+
const user = userEvent.setup();
|
|
1071
1136
|
const onValueChange = vi.fn();
|
|
1072
1137
|
render(
|
|
1073
1138
|
<Combobox options={people} multiple defaultValue={['alice', 'bob', 'charlie']} onValueChange={onValueChange} />,
|
|
1074
1139
|
);
|
|
1075
1140
|
const input = screen.getByRole('combobox');
|
|
1076
|
-
|
|
1141
|
+
await user.click(input);
|
|
1077
1142
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1143
|
+
await user.keyboard('{ArrowLeft}');
|
|
1144
|
+
await user.keyboard('{ArrowLeft}');
|
|
1145
|
+
await user.keyboard('{Backspace}');
|
|
1081
1146
|
|
|
1082
1147
|
expect(onValueChange).toHaveBeenCalledWith(['alice', 'charlie']);
|
|
1083
1148
|
});
|
|
1084
1149
|
|
|
1085
|
-
test('Delete on focused chip removes it', () => {
|
|
1150
|
+
test('Delete on focused chip removes it', async () => {
|
|
1151
|
+
const user = userEvent.setup();
|
|
1086
1152
|
const onValueChange = vi.fn();
|
|
1087
1153
|
render(
|
|
1088
1154
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
1089
1155
|
);
|
|
1090
1156
|
const input = screen.getByRole('combobox');
|
|
1091
|
-
|
|
1157
|
+
await user.click(input);
|
|
1092
1158
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1159
|
+
await user.keyboard('{ArrowLeft}');
|
|
1160
|
+
await user.keyboard('{ArrowLeft}');
|
|
1161
|
+
await user.keyboard('{Delete}');
|
|
1096
1162
|
|
|
1097
1163
|
expect(onValueChange).toHaveBeenCalledWith(['bob']);
|
|
1098
1164
|
});
|
|
1099
1165
|
|
|
1100
|
-
test('Backspace on first chip focuses next or exits', () => {
|
|
1166
|
+
test('Backspace on first chip focuses next or exits', async () => {
|
|
1167
|
+
const user = userEvent.setup();
|
|
1101
1168
|
const onValueChange = vi.fn();
|
|
1102
1169
|
render(
|
|
1103
1170
|
<Combobox options={people} multiple defaultValue={['alice']} onValueChange={onValueChange} />,
|
|
1104
1171
|
);
|
|
1105
1172
|
const input = screen.getByRole('combobox');
|
|
1106
|
-
|
|
1173
|
+
await user.click(input);
|
|
1107
1174
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1175
|
+
await user.keyboard('{ArrowLeft}');
|
|
1176
|
+
await user.keyboard('{Backspace}');
|
|
1110
1177
|
|
|
1111
1178
|
expect(onValueChange).toHaveBeenCalledWith([]);
|
|
1112
1179
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1113
1180
|
});
|
|
1114
1181
|
|
|
1115
1182
|
test('typing exits chip nav mode', async () => {
|
|
1183
|
+
const user = userEvent.setup();
|
|
1116
1184
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1117
1185
|
const input = screen.getByRole('combobox');
|
|
1118
|
-
|
|
1186
|
+
await user.click(input);
|
|
1119
1187
|
|
|
1120
|
-
|
|
1188
|
+
await user.keyboard('{ArrowLeft}');
|
|
1121
1189
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1122
1190
|
|
|
1123
|
-
await
|
|
1191
|
+
await user.type(input, 'c');
|
|
1124
1192
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1125
1193
|
});
|
|
1126
1194
|
|
|
1127
|
-
test('Escape exits chip nav mode', () => {
|
|
1195
|
+
test('Escape exits chip nav mode', async () => {
|
|
1196
|
+
const user = userEvent.setup();
|
|
1128
1197
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1129
1198
|
const input = screen.getByRole('combobox');
|
|
1130
|
-
|
|
1199
|
+
await user.click(input);
|
|
1131
1200
|
|
|
1132
|
-
|
|
1201
|
+
await user.keyboard('{ArrowLeft}');
|
|
1133
1202
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1134
1203
|
|
|
1135
|
-
|
|
1204
|
+
await user.keyboard('{Escape}');
|
|
1136
1205
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1137
1206
|
});
|
|
1138
1207
|
|
|
1139
|
-
test('Cmd+A during chip nav enters bulk-select-all mode', () => {
|
|
1208
|
+
test('Cmd+A during chip nav enters bulk-select-all mode', async () => {
|
|
1209
|
+
const user = userEvent.setup();
|
|
1140
1210
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1141
1211
|
const input = screen.getByRole('combobox');
|
|
1142
|
-
|
|
1212
|
+
await user.click(input);
|
|
1143
1213
|
|
|
1144
|
-
|
|
1214
|
+
await user.keyboard('{ArrowLeft}');
|
|
1145
1215
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1146
1216
|
|
|
1147
1217
|
fireEvent.keyDown(input, { key: 'a', metaKey: true });
|
|
@@ -30,7 +30,9 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
30
30
|
disabled = false,
|
|
31
31
|
dropdownOnFocus = true,
|
|
32
32
|
triggerVariant = 'input',
|
|
33
|
+
selectedValueDisplay = 'tags',
|
|
33
34
|
showDropdownTrigger = true,
|
|
35
|
+
triggerEndContent,
|
|
34
36
|
showSelectionCountBadge = triggerVariant === 'button' && multiple,
|
|
35
37
|
selectionCountA11yLabel,
|
|
36
38
|
loading = false,
|
|
@@ -94,6 +96,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
94
96
|
const { listboxRef, getOptionId, scrollHighlightedIntoView } = useComboboxListboxDom({
|
|
95
97
|
comboboxId,
|
|
96
98
|
});
|
|
99
|
+
const chipKeyboardEnabled = selectedValueDisplay === 'tags';
|
|
97
100
|
|
|
98
101
|
const {
|
|
99
102
|
selectedChipValues,
|
|
@@ -103,7 +106,9 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
103
106
|
focusedChipIndex,
|
|
104
107
|
setFocusedChipIndex,
|
|
105
108
|
exitChipNav,
|
|
106
|
-
} = useChipSelection(selectedValues);
|
|
109
|
+
} = useChipSelection(chipKeyboardEnabled ? selectedValues : []);
|
|
110
|
+
|
|
111
|
+
const renderSearchInputInListbox = triggerVariant === 'button';
|
|
107
112
|
|
|
108
113
|
const {
|
|
109
114
|
shouldRenderPopoverContent,
|
|
@@ -136,6 +141,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
136
141
|
clearChipSelection,
|
|
137
142
|
exitChipNav,
|
|
138
143
|
triggerContainsInput: triggerVariant === 'input',
|
|
144
|
+
renderSearchInputInListbox,
|
|
139
145
|
});
|
|
140
146
|
|
|
141
147
|
const optionGroups = useMemo(() => buildOptionGroups(filteredOptions), [filteredOptions]);
|
|
@@ -220,7 +226,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
220
226
|
scrollHighlightedIntoView,
|
|
221
227
|
selectOption,
|
|
222
228
|
selectedChipValues,
|
|
223
|
-
selectedValues,
|
|
229
|
+
selectedValues: chipKeyboardEnabled ? selectedValues : [],
|
|
224
230
|
setFocusedChipIndex,
|
|
225
231
|
setHighlightIndex,
|
|
226
232
|
setIsOpen,
|
|
@@ -247,7 +253,6 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
247
253
|
const preventMouseDefault = useCallback((e: React.MouseEvent) => {
|
|
248
254
|
e.preventDefault();
|
|
249
255
|
}, []);
|
|
250
|
-
const renderSearchInputInListbox = triggerVariant === 'button';
|
|
251
256
|
|
|
252
257
|
const handlePopoverOpenAutoFocus = useCallback(
|
|
253
258
|
(e: Event) => {
|
|
@@ -262,7 +267,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
262
267
|
);
|
|
263
268
|
|
|
264
269
|
const resolvedSelectionCountA11yLabel = useMemo(() => {
|
|
265
|
-
if (!showSelectionCountBadge || selectedValues.length === 0) {
|
|
270
|
+
if (!showSelectionCountBadge || !chipKeyboardEnabled || selectedValues.length === 0) {
|
|
266
271
|
return undefined;
|
|
267
272
|
}
|
|
268
273
|
if (typeof selectionCountA11yLabel === 'function') {
|
|
@@ -272,7 +277,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
272
277
|
return selectionCountA11yLabel;
|
|
273
278
|
}
|
|
274
279
|
return `${selectedValues.length} selected item${selectedValues.length === 1 ? '' : 's'}`;
|
|
275
|
-
}, [selectedValues.length, selectionCountA11yLabel, showSelectionCountBadge]);
|
|
280
|
+
}, [chipKeyboardEnabled, selectedValues.length, selectionCountA11yLabel, showSelectionCountBadge]);
|
|
276
281
|
|
|
277
282
|
const handleButtonTriggerKeyDown = useCallback(
|
|
278
283
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -312,6 +317,8 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
312
317
|
aria-invalid={ariaInvalid}
|
|
313
318
|
aria-label={ariaLabel}
|
|
314
319
|
showDropdownTrigger={showDropdownTrigger}
|
|
320
|
+
selectedValueDisplay={selectedValueDisplay}
|
|
321
|
+
triggerEndContent={triggerEndContent}
|
|
315
322
|
selectedChips={selectedChips}
|
|
316
323
|
selectedChipValuesSet={selectedChipValuesSet}
|
|
317
324
|
focusedChipIndex={focusedChipIndex}
|
|
@@ -337,6 +344,8 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
337
344
|
ariaDescribedBy={ariaDescribedBy}
|
|
338
345
|
ariaInvalid={ariaInvalid}
|
|
339
346
|
showDropdownTrigger={showDropdownTrigger}
|
|
347
|
+
selectedValueDisplay={selectedValueDisplay}
|
|
348
|
+
triggerEndContent={triggerEndContent}
|
|
340
349
|
selectedChips={selectedChips}
|
|
341
350
|
selectedChipValuesSet={selectedChipValuesSet}
|
|
342
351
|
focusedChipIndex={focusedChipIndex}
|
|
@@ -345,7 +354,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
345
354
|
handleTriggerClick={handleTriggerClick}
|
|
346
355
|
handleTriggerKeyDown={handleButtonTriggerKeyDown}
|
|
347
356
|
handleChevronClick={handleChevronClick}
|
|
348
|
-
showSelectionCountBadge={showSelectionCountBadge}
|
|
357
|
+
showSelectionCountBadge={showSelectionCountBadge && chipKeyboardEnabled}
|
|
349
358
|
selectionCountA11yLabel={resolvedSelectionCountA11yLabel}
|
|
350
359
|
/>
|
|
351
360
|
)}
|
|
@@ -4,7 +4,7 @@ import { Icon } from 'Components/icon/Icon';
|
|
|
4
4
|
import { Tag } from 'Components/tag/Tag';
|
|
5
5
|
import { Popover } from 'radix-ui';
|
|
6
6
|
import { useMemo, useRef } from 'react';
|
|
7
|
-
import type { ComboboxAriaInvalid, ComboboxOption } from './types';
|
|
7
|
+
import type { ComboboxAriaInvalid, ComboboxOption, ComboboxSelectedValueDisplay } from './types';
|
|
8
8
|
import { useElementWidth } from './useElementWidth';
|
|
9
9
|
import { useVisibleChips } from './useVisibleChips';
|
|
10
10
|
|
|
@@ -19,6 +19,8 @@ export type ComboboxButtonTriggerProps = {
|
|
|
19
19
|
ariaDescribedBy?: string;
|
|
20
20
|
ariaInvalid?: ComboboxAriaInvalid;
|
|
21
21
|
showDropdownTrigger: boolean;
|
|
22
|
+
selectedValueDisplay: ComboboxSelectedValueDisplay;
|
|
23
|
+
triggerEndContent?: React.ReactNode;
|
|
22
24
|
selectedChips: ComboboxOption[];
|
|
23
25
|
selectedChipValuesSet: Set<string>;
|
|
24
26
|
focusedChipIndex: number | null;
|
|
@@ -42,6 +44,8 @@ export const ComboboxButtonTrigger = ({
|
|
|
42
44
|
ariaDescribedBy,
|
|
43
45
|
ariaInvalid,
|
|
44
46
|
showDropdownTrigger,
|
|
47
|
+
selectedValueDisplay,
|
|
48
|
+
triggerEndContent,
|
|
45
49
|
selectedChips,
|
|
46
50
|
selectedChipValuesSet,
|
|
47
51
|
focusedChipIndex,
|
|
@@ -66,6 +70,11 @@ export const ComboboxButtonTrigger = ({
|
|
|
66
70
|
[resolveTagLabel, selectedChips],
|
|
67
71
|
);
|
|
68
72
|
|
|
73
|
+
const selectedValueText = useMemo(
|
|
74
|
+
() => selectedChips.map(resolveTagLabel).join(', '),
|
|
75
|
+
[resolveTagLabel, selectedChips],
|
|
76
|
+
);
|
|
77
|
+
const usesTagDisplay = selectedValueDisplay === 'tags';
|
|
69
78
|
const shouldShowBadge = showSelectionCountBadge && selectedChips.length > 0;
|
|
70
79
|
|
|
71
80
|
const contentWidth = useElementWidth(contentRef, `${chipWatchKey}-${isOpen}-${shouldShowBadge}`);
|
|
@@ -104,8 +113,8 @@ export const ComboboxButtonTrigger = ({
|
|
|
104
113
|
const visibleChips = canMeasure
|
|
105
114
|
? layout.visibleChipIndices.map(index => selectedChips[index]!).filter(Boolean)
|
|
106
115
|
: selectedChips;
|
|
107
|
-
const showEllipsis = canMeasure ? layout.showEllipsis : false;
|
|
108
|
-
const showBadge = canMeasure ? layout.showBadge : shouldShowBadge;
|
|
116
|
+
const showEllipsis = usesTagDisplay && (canMeasure ? layout.showEllipsis : false);
|
|
117
|
+
const showBadge = usesTagDisplay && (canMeasure ? layout.showBadge : shouldShowBadge);
|
|
109
118
|
|
|
110
119
|
const renderSelectionTag = (opt: ComboboxOption, chipIdx: number, onRemove?: () => void) => (
|
|
111
120
|
<Tag
|
|
@@ -148,18 +157,36 @@ export const ComboboxButtonTrigger = ({
|
|
|
148
157
|
onKeyDown={handleTriggerKeyDown}
|
|
149
158
|
>
|
|
150
159
|
<div className="ds-combobox__button-content" ref={contentRef}>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
{usesTagDisplay
|
|
161
|
+
? (
|
|
162
|
+
<div className="ds-combobox__button-tags-viewport">
|
|
163
|
+
<div className="ds-combobox__button-tags-track">
|
|
164
|
+
{selectedChips.length === 0 && (
|
|
165
|
+
<span className="ds-combobox__button-placeholder">{placeholder}</span>
|
|
166
|
+
)}
|
|
167
|
+
{visibleChips.map((opt, chipIdx) =>
|
|
168
|
+
renderSelectionTag(opt, chipIdx, disabled ? undefined : () => removeValue(opt.value)))}
|
|
169
|
+
</div>
|
|
170
|
+
{showEllipsis && <span className="ds-combobox__button-ellipsis" aria-hidden="true">…</span>}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
: (
|
|
174
|
+
<span
|
|
175
|
+
className={classNames({
|
|
176
|
+
'ds-combobox__button-placeholder': selectedValueText.length === 0,
|
|
177
|
+
'ds-combobox__selected-value': selectedValueText.length > 0,
|
|
178
|
+
})}
|
|
179
|
+
>
|
|
180
|
+
{selectedValueText || placeholder}
|
|
181
|
+
</span>
|
|
155
182
|
)}
|
|
156
|
-
{visibleChips.map((opt, chipIdx) =>
|
|
157
|
-
renderSelectionTag(opt, chipIdx, disabled ? undefined : () => removeValue(opt.value)))}
|
|
158
|
-
</div>
|
|
159
|
-
{showEllipsis && <span className="ds-combobox__button-ellipsis" aria-hidden="true">…</span>}
|
|
160
|
-
</div>
|
|
161
183
|
{showBadge && renderSelectionCountBadge(true)}
|
|
162
184
|
</div>
|
|
185
|
+
{triggerEndContent && (
|
|
186
|
+
<span className="ds-combobox__end-content" aria-hidden="true">
|
|
187
|
+
{triggerEndContent}
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
163
190
|
{showDropdownTrigger && (
|
|
164
191
|
<button
|
|
165
192
|
type="button"
|
|
@@ -173,20 +200,22 @@ export const ComboboxButtonTrigger = ({
|
|
|
173
200
|
</button>
|
|
174
201
|
)}
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
{usesTagDisplay && (
|
|
204
|
+
<div className="ds-combobox__measure" aria-hidden="true">
|
|
205
|
+
{/* Mirror the rendered chips off-screen so width calculations use the real Tag layout. */}
|
|
206
|
+
<div className="ds-combobox__button-tags-track" ref={measureTrackRef}>
|
|
207
|
+
{selectedChips.map((opt, chipIdx) => (
|
|
208
|
+
<span key={`measure-${opt.value}`} className="ds-combobox__measure-chip">
|
|
209
|
+
{renderSelectionTag(opt, chipIdx, disabled ? undefined : () => {})}
|
|
210
|
+
</span>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
<span ref={ellipsisProbeRef} className="ds-combobox__button-ellipsis">…</span>
|
|
214
|
+
<span ref={badgeProbeRef}>
|
|
215
|
+
{shouldShowBadge ? renderSelectionCountBadge(false) : null}
|
|
216
|
+
</span>
|
|
184
217
|
</div>
|
|
185
|
-
|
|
186
|
-
<span ref={badgeProbeRef}>
|
|
187
|
-
{shouldShowBadge ? renderSelectionCountBadge(false) : null}
|
|
188
|
-
</span>
|
|
189
|
-
</div>
|
|
218
|
+
)}
|
|
190
219
|
</div>
|
|
191
220
|
</Popover.Anchor>
|
|
192
221
|
);
|