@arbor-education/design-system.components 0.9.0 → 0.11.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/.claude/agent-memory/blanche-designspert/MEMORY.md +64 -0
- package/.claude/agent-memory/blanche-designspert/token-review-patterns.md +29 -0
- package/.claude/agent-memory/dorothy-fact-checker/MEMORY.md +129 -0
- package/.claude/agent-memory/rose-storybookspert/MEMORY.md +29 -0
- package/.claude/agent-memory/rose-storybookspert/patterns.md +132 -0
- package/.claude/agent-memory/sophia-componentspert/MEMORY.md +14 -0
- package/.claude/agent-memory/sophia-componentspert/components.md +367 -0
- package/.claude/agents/blanche-designspert.md +150 -0
- package/.claude/agents/dorothy-fact-checker.md +145 -0
- package/.claude/agents/rose-storybookspert.md +148 -0
- package/.claude/agents/sophia-componentspert.md +133 -0
- package/.claude/component-library.md +1107 -0
- package/.claude/design-assessment-daily-attendance-2026-04-10.md +566 -0
- package/.claude/figma-assessment-7154-58899.md +404 -0
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-11086-97537.md +392 -0
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-41974.md +474 -0
- package/.claude/figma-assessment-UKQfcxnT4rlHHNuiumt4o1-551-43094.md +462 -0
- package/.claude/figma-assessment-fcFK4CGzkz2fVyY3koX8ZE-7154-59061.md +440 -0
- package/.claude/migration-report-custom-report-writer-2026-02-19.md +591 -0
- package/.claude/skills/analyze-design/README.md +295 -0
- package/.claude/skills/analyze-design/SKILL.md +741 -0
- package/.claude/skills/create-page/README.md +246 -0
- package/.claude/skills/create-page/SKILL.md +634 -0
- package/.claude/skills/create-page/design-analysis-template.md +333 -0
- package/.claude/skills/create-page/page-template.scss +118 -0
- package/.claude/skills/create-page/page-template.tsx +230 -0
- package/.claude/skills/map-legacy/README.md +87 -0
- package/.claude/skills/map-legacy/SKILL.md +465 -0
- package/.claude/skills/migrate-page/README.md +125 -0
- package/.claude/skills/migrate-page/SKILL.md +374 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +31 -0
- package/CONTRIBUTING.md +191 -0
- package/README.md +110 -20
- 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 +2 -1
- package/dist/components/combobox/Combobox.js.map +1 -1
- package/dist/components/combobox/Combobox.test.js +98 -61
- package/dist/components/combobox/Combobox.test.js.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.test.d.ts.map +1 -1
- package/dist/components/formField/FormField.test.js +5 -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/table/DSDefaultColDef.js +2 -2
- package/dist/components/table/DSDefaultColDef.js.map +1 -1
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +4 -0
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +2 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +132 -3
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +106 -5
- package/dist/components/table/Table.test.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/components/table/cellRenderers/CheckboxCellRenderer.d.ts +3 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.js +12 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.js.map +1 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.d.ts +2 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.d.ts.map +1 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.js +65 -0
- package/dist/components/table/cellRenderers/CheckboxCellRenderer.test.js.map +1 -0
- package/dist/index.css +259 -4
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +4 -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.test.tsx +104 -61
- package/src/components/combobox/Combobox.tsx +3 -1
- 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.test.tsx +5 -5
- 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/table/DSDefaultColDef.ts +2 -2
- package/src/components/table/Table.stories.tsx +147 -3
- package/src/components/table/Table.test.tsx +131 -5
- package/src/components/table/Table.tsx +4 -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/CheckboxCellRenderer.test.tsx +74 -0
- package/src/components/table/cellRenderers/CheckboxCellRenderer.tsx +28 -0
- package/src/components/table/cellRenderers/booleanCellRenderer.scss +7 -0
- package/src/components/table/table.scss +1 -1
- package/src/index.scss +2 -0
- package/src/index.ts +4 -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
|
|
@@ -353,20 +377,22 @@ describe('Combobox', () => {
|
|
|
353
377
|
});
|
|
354
378
|
|
|
355
379
|
test('Ctrl+A selects all chips when input is focused', async () => {
|
|
380
|
+
const user = userEvent.setup();
|
|
356
381
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
357
382
|
const input = screen.getByRole('combobox');
|
|
358
|
-
|
|
383
|
+
await user.click(input);
|
|
359
384
|
|
|
360
|
-
await
|
|
385
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
361
386
|
|
|
362
387
|
const selectedTags = document.querySelectorAll('.ds-tag--selected');
|
|
363
388
|
expect(selectedTags).toHaveLength(2);
|
|
364
389
|
});
|
|
365
390
|
|
|
366
391
|
test('Cmd+A selects all chips in single-select mode', async () => {
|
|
392
|
+
const user = userEvent.setup();
|
|
367
393
|
render(<Combobox options={people} defaultValue={['alice']} />);
|
|
368
394
|
const input = screen.getByRole('combobox');
|
|
369
|
-
|
|
395
|
+
await user.click(input);
|
|
370
396
|
|
|
371
397
|
fireEvent.keyDown(input, { key: 'a', metaKey: true });
|
|
372
398
|
|
|
@@ -375,54 +401,58 @@ describe('Combobox', () => {
|
|
|
375
401
|
});
|
|
376
402
|
|
|
377
403
|
test('Backspace clears all selected chips after select-all', async () => {
|
|
404
|
+
const user = userEvent.setup();
|
|
378
405
|
const onValueChange = vi.fn();
|
|
379
406
|
render(
|
|
380
407
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
381
408
|
);
|
|
382
409
|
const input = screen.getByRole('combobox');
|
|
383
|
-
|
|
410
|
+
await user.click(input);
|
|
384
411
|
|
|
385
|
-
await
|
|
386
|
-
await
|
|
412
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
413
|
+
await user.keyboard('{Backspace}');
|
|
387
414
|
|
|
388
415
|
expect(onValueChange).toHaveBeenLastCalledWith([]);
|
|
389
416
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
390
417
|
});
|
|
391
418
|
|
|
392
419
|
test('Delete clears all selected chips after select-all', async () => {
|
|
420
|
+
const user = userEvent.setup();
|
|
393
421
|
const onValueChange = vi.fn();
|
|
394
422
|
render(
|
|
395
423
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
396
424
|
);
|
|
397
425
|
const input = screen.getByRole('combobox');
|
|
398
|
-
|
|
426
|
+
await user.click(input);
|
|
399
427
|
|
|
400
|
-
await
|
|
401
|
-
await
|
|
428
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
429
|
+
await user.keyboard('{Delete}');
|
|
402
430
|
|
|
403
431
|
expect(onValueChange).toHaveBeenLastCalledWith([]);
|
|
404
432
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
405
433
|
});
|
|
406
434
|
|
|
407
435
|
test('typing after chip select-all clears selected chip styling', async () => {
|
|
436
|
+
const user = userEvent.setup();
|
|
408
437
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
409
438
|
const input = screen.getByRole('combobox');
|
|
410
|
-
|
|
439
|
+
await user.click(input);
|
|
411
440
|
|
|
412
|
-
await
|
|
441
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
413
442
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(2);
|
|
414
443
|
|
|
415
|
-
await
|
|
444
|
+
await user.type(input, 'c');
|
|
416
445
|
|
|
417
446
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
418
447
|
});
|
|
419
448
|
|
|
420
449
|
test('Ctrl+A with no chips keeps native input behavior and does not crash', async () => {
|
|
450
|
+
const user = userEvent.setup();
|
|
421
451
|
render(<Combobox options={people} />);
|
|
422
452
|
const input = screen.getByRole('combobox');
|
|
423
|
-
|
|
453
|
+
await user.click(input);
|
|
424
454
|
|
|
425
|
-
await
|
|
455
|
+
await user.keyboard('{Control>}a{/Control}');
|
|
426
456
|
|
|
427
457
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
428
458
|
});
|
|
@@ -582,21 +612,23 @@ describe('Combobox', () => {
|
|
|
582
612
|
});
|
|
583
613
|
|
|
584
614
|
test('keyboard: ArrowDown opens the listbox', async () => {
|
|
615
|
+
const user = userEvent.setup();
|
|
585
616
|
render(<Combobox options={people} />);
|
|
586
617
|
const input = screen.getByRole('combobox');
|
|
587
|
-
|
|
618
|
+
await user.click(input);
|
|
588
619
|
|
|
589
|
-
await
|
|
620
|
+
await user.keyboard('{ArrowDown}');
|
|
590
621
|
|
|
591
622
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
592
623
|
});
|
|
593
624
|
|
|
594
625
|
test('keyboard: Alt+ArrowDown opens the listbox', async () => {
|
|
626
|
+
const user = userEvent.setup();
|
|
595
627
|
render(<Combobox options={people} />);
|
|
596
628
|
const input = screen.getByRole('combobox');
|
|
597
|
-
|
|
629
|
+
await user.click(input);
|
|
598
630
|
|
|
599
|
-
await
|
|
631
|
+
await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
|
|
600
632
|
|
|
601
633
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
602
634
|
});
|
|
@@ -639,14 +671,15 @@ describe('Combobox', () => {
|
|
|
639
671
|
});
|
|
640
672
|
|
|
641
673
|
test('keyboard: Backspace on empty input removes last chip', async () => {
|
|
674
|
+
const user = userEvent.setup();
|
|
642
675
|
const onValueChange = vi.fn();
|
|
643
676
|
render(
|
|
644
677
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
645
678
|
);
|
|
646
679
|
const input = screen.getByRole('combobox');
|
|
647
|
-
|
|
680
|
+
await user.click(input);
|
|
648
681
|
|
|
649
|
-
await
|
|
682
|
+
await user.keyboard('{Backspace}');
|
|
650
683
|
|
|
651
684
|
expect(onValueChange).toHaveBeenCalledWith(['alice']);
|
|
652
685
|
});
|
|
@@ -722,7 +755,7 @@ describe('Combobox', () => {
|
|
|
722
755
|
|
|
723
756
|
expect(onCreateNew).toHaveBeenCalledWith('Taylor');
|
|
724
757
|
expect(onValueChange).toHaveBeenCalledWith(['new-taylor']);
|
|
725
|
-
expect(screen.
|
|
758
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
726
759
|
});
|
|
727
760
|
|
|
728
761
|
test('allowCreate duplicate prevention selects existing option', async () => {
|
|
@@ -1044,131 +1077,141 @@ describe('Combobox', () => {
|
|
|
1044
1077
|
});
|
|
1045
1078
|
|
|
1046
1079
|
describe('chip keyboard navigation', () => {
|
|
1047
|
-
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();
|
|
1048
1082
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1049
1083
|
const input = screen.getByRole('combobox');
|
|
1050
|
-
|
|
1084
|
+
await user.click(input);
|
|
1051
1085
|
|
|
1052
|
-
|
|
1086
|
+
await user.keyboard('{ArrowLeft}');
|
|
1053
1087
|
|
|
1054
1088
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1055
1089
|
expect(tags[1]).toHaveClass('ds-tag--selected');
|
|
1056
1090
|
expect(tags[0]).not.toHaveClass('ds-tag--selected');
|
|
1057
1091
|
});
|
|
1058
1092
|
|
|
1059
|
-
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();
|
|
1060
1095
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob', 'charlie']} />);
|
|
1061
1096
|
const input = screen.getByRole('combobox');
|
|
1062
|
-
|
|
1097
|
+
await user.click(input);
|
|
1063
1098
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1099
|
+
await user.keyboard('{ArrowLeft}');
|
|
1100
|
+
await user.keyboard('{ArrowLeft}');
|
|
1066
1101
|
|
|
1067
1102
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1068
1103
|
expect(tags[1]).toHaveClass('ds-tag--selected');
|
|
1069
1104
|
expect(tags[2]).not.toHaveClass('ds-tag--selected');
|
|
1070
1105
|
});
|
|
1071
1106
|
|
|
1072
|
-
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();
|
|
1073
1109
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1074
1110
|
const input = screen.getByRole('combobox');
|
|
1075
|
-
|
|
1111
|
+
await user.click(input);
|
|
1076
1112
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1113
|
+
await user.keyboard('{ArrowLeft}');
|
|
1114
|
+
await user.keyboard('{ArrowLeft}');
|
|
1115
|
+
await user.keyboard('{ArrowLeft}');
|
|
1080
1116
|
|
|
1081
1117
|
const tags = document.querySelectorAll('.ds-tag');
|
|
1082
1118
|
expect(tags[0]).toHaveClass('ds-tag--selected');
|
|
1083
1119
|
});
|
|
1084
1120
|
|
|
1085
|
-
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();
|
|
1086
1123
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1087
1124
|
const input = screen.getByRole('combobox');
|
|
1088
|
-
|
|
1125
|
+
await user.click(input);
|
|
1089
1126
|
|
|
1090
|
-
|
|
1127
|
+
await user.keyboard('{ArrowLeft}');
|
|
1091
1128
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1092
1129
|
|
|
1093
|
-
|
|
1130
|
+
await user.keyboard('{ArrowRight}');
|
|
1094
1131
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1095
1132
|
});
|
|
1096
1133
|
|
|
1097
|
-
test('Backspace on focused chip removes it', () => {
|
|
1134
|
+
test('Backspace on focused chip removes it', async () => {
|
|
1135
|
+
const user = userEvent.setup();
|
|
1098
1136
|
const onValueChange = vi.fn();
|
|
1099
1137
|
render(
|
|
1100
1138
|
<Combobox options={people} multiple defaultValue={['alice', 'bob', 'charlie']} onValueChange={onValueChange} />,
|
|
1101
1139
|
);
|
|
1102
1140
|
const input = screen.getByRole('combobox');
|
|
1103
|
-
|
|
1141
|
+
await user.click(input);
|
|
1104
1142
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1143
|
+
await user.keyboard('{ArrowLeft}');
|
|
1144
|
+
await user.keyboard('{ArrowLeft}');
|
|
1145
|
+
await user.keyboard('{Backspace}');
|
|
1108
1146
|
|
|
1109
1147
|
expect(onValueChange).toHaveBeenCalledWith(['alice', 'charlie']);
|
|
1110
1148
|
});
|
|
1111
1149
|
|
|
1112
|
-
test('Delete on focused chip removes it', () => {
|
|
1150
|
+
test('Delete on focused chip removes it', async () => {
|
|
1151
|
+
const user = userEvent.setup();
|
|
1113
1152
|
const onValueChange = vi.fn();
|
|
1114
1153
|
render(
|
|
1115
1154
|
<Combobox options={people} multiple defaultValue={['alice', 'bob']} onValueChange={onValueChange} />,
|
|
1116
1155
|
);
|
|
1117
1156
|
const input = screen.getByRole('combobox');
|
|
1118
|
-
|
|
1157
|
+
await user.click(input);
|
|
1119
1158
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1159
|
+
await user.keyboard('{ArrowLeft}');
|
|
1160
|
+
await user.keyboard('{ArrowLeft}');
|
|
1161
|
+
await user.keyboard('{Delete}');
|
|
1123
1162
|
|
|
1124
1163
|
expect(onValueChange).toHaveBeenCalledWith(['bob']);
|
|
1125
1164
|
});
|
|
1126
1165
|
|
|
1127
|
-
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();
|
|
1128
1168
|
const onValueChange = vi.fn();
|
|
1129
1169
|
render(
|
|
1130
1170
|
<Combobox options={people} multiple defaultValue={['alice']} onValueChange={onValueChange} />,
|
|
1131
1171
|
);
|
|
1132
1172
|
const input = screen.getByRole('combobox');
|
|
1133
|
-
|
|
1173
|
+
await user.click(input);
|
|
1134
1174
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1175
|
+
await user.keyboard('{ArrowLeft}');
|
|
1176
|
+
await user.keyboard('{Backspace}');
|
|
1137
1177
|
|
|
1138
1178
|
expect(onValueChange).toHaveBeenCalledWith([]);
|
|
1139
1179
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1140
1180
|
});
|
|
1141
1181
|
|
|
1142
1182
|
test('typing exits chip nav mode', async () => {
|
|
1183
|
+
const user = userEvent.setup();
|
|
1143
1184
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1144
1185
|
const input = screen.getByRole('combobox');
|
|
1145
|
-
|
|
1186
|
+
await user.click(input);
|
|
1146
1187
|
|
|
1147
|
-
|
|
1188
|
+
await user.keyboard('{ArrowLeft}');
|
|
1148
1189
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1149
1190
|
|
|
1150
|
-
await
|
|
1191
|
+
await user.type(input, 'c');
|
|
1151
1192
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1152
1193
|
});
|
|
1153
1194
|
|
|
1154
|
-
test('Escape exits chip nav mode', () => {
|
|
1195
|
+
test('Escape exits chip nav mode', async () => {
|
|
1196
|
+
const user = userEvent.setup();
|
|
1155
1197
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1156
1198
|
const input = screen.getByRole('combobox');
|
|
1157
|
-
|
|
1199
|
+
await user.click(input);
|
|
1158
1200
|
|
|
1159
|
-
|
|
1201
|
+
await user.keyboard('{ArrowLeft}');
|
|
1160
1202
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1161
1203
|
|
|
1162
|
-
|
|
1204
|
+
await user.keyboard('{Escape}');
|
|
1163
1205
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(0);
|
|
1164
1206
|
});
|
|
1165
1207
|
|
|
1166
|
-
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();
|
|
1167
1210
|
render(<Combobox options={people} multiple defaultValue={['alice', 'bob']} />);
|
|
1168
1211
|
const input = screen.getByRole('combobox');
|
|
1169
|
-
|
|
1212
|
+
await user.click(input);
|
|
1170
1213
|
|
|
1171
|
-
|
|
1214
|
+
await user.keyboard('{ArrowLeft}');
|
|
1172
1215
|
expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
|
|
1173
1216
|
|
|
1174
1217
|
fireEvent.keyDown(input, { key: 'a', metaKey: true });
|
|
@@ -108,6 +108,8 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
108
108
|
exitChipNav,
|
|
109
109
|
} = useChipSelection(chipKeyboardEnabled ? selectedValues : []);
|
|
110
110
|
|
|
111
|
+
const renderSearchInputInListbox = triggerVariant === 'button';
|
|
112
|
+
|
|
111
113
|
const {
|
|
112
114
|
shouldRenderPopoverContent,
|
|
113
115
|
shouldShowPopover,
|
|
@@ -139,6 +141,7 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
139
141
|
clearChipSelection,
|
|
140
142
|
exitChipNav,
|
|
141
143
|
triggerContainsInput: triggerVariant === 'input',
|
|
144
|
+
renderSearchInputInListbox,
|
|
142
145
|
});
|
|
143
146
|
|
|
144
147
|
const optionGroups = useMemo(() => buildOptionGroups(filteredOptions), [filteredOptions]);
|
|
@@ -250,7 +253,6 @@ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
|
|
|
250
253
|
const preventMouseDefault = useCallback((e: React.MouseEvent) => {
|
|
251
254
|
e.preventDefault();
|
|
252
255
|
}, []);
|
|
253
|
-
const renderSearchInputInListbox = triggerVariant === 'button';
|
|
254
256
|
|
|
255
257
|
const handlePopoverOpenAutoFocus = useCallback(
|
|
256
258
|
(e: Event) => {
|
|
@@ -26,6 +26,8 @@ type UseComboboxPopoverBehaviorParams = {
|
|
|
26
26
|
clearChipSelection: () => void;
|
|
27
27
|
exitChipNav: () => void;
|
|
28
28
|
triggerContainsInput?: boolean;
|
|
29
|
+
/** Search field is inside the popover (button trigger); keep shell mounted while open even with zero matches. */
|
|
30
|
+
renderSearchInputInListbox: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export type UseComboboxPopoverBehaviorReturn = {
|
|
@@ -61,10 +63,13 @@ export const useComboboxPopoverBehavior = ({
|
|
|
61
63
|
clearChipSelection,
|
|
62
64
|
exitChipNav,
|
|
63
65
|
triggerContainsInput = true,
|
|
66
|
+
renderSearchInputInListbox,
|
|
64
67
|
}: UseComboboxPopoverBehaviorParams): UseComboboxPopoverBehaviorReturn => {
|
|
65
|
-
const shouldRenderPopoverContent
|
|
68
|
+
const shouldRenderPopoverContent
|
|
69
|
+
= loading || totalItems > 0 || (renderSearchInputInListbox && isOpen);
|
|
66
70
|
const shouldShowPopover = isOpen && shouldRenderPopoverContent;
|
|
67
71
|
const showListboxLoading = Boolean(loading && shouldShowPopover);
|
|
72
|
+
const canOpenPopover = loading || totalItems > 0 || renderSearchInputInListbox;
|
|
68
73
|
|
|
69
74
|
const handleInputChange = useCallback(
|
|
70
75
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
@@ -110,10 +115,10 @@ export const useComboboxPopoverBehavior = ({
|
|
|
110
115
|
|
|
111
116
|
const requestOpen = useCallback(() => {
|
|
112
117
|
if (disabled) return;
|
|
113
|
-
if (
|
|
118
|
+
if (canOpenPopover) {
|
|
114
119
|
openPopover();
|
|
115
120
|
}
|
|
116
|
-
}, [disabled, openPopover,
|
|
121
|
+
}, [disabled, openPopover, canOpenPopover]);
|
|
117
122
|
|
|
118
123
|
const handleTriggerClick = useCallback(() => {
|
|
119
124
|
if (disabled) return;
|
|
@@ -148,11 +153,11 @@ export const useComboboxPopoverBehavior = ({
|
|
|
148
153
|
if (triggerContainsInput) {
|
|
149
154
|
inputRef.current?.focus();
|
|
150
155
|
}
|
|
151
|
-
if (
|
|
156
|
+
if (canOpenPopover) {
|
|
152
157
|
openPopover();
|
|
153
158
|
}
|
|
154
159
|
},
|
|
155
|
-
[disabled, inputRef, openPopover,
|
|
160
|
+
[disabled, inputRef, openPopover, canOpenPopover, triggerContainsInput],
|
|
156
161
|
);
|
|
157
162
|
|
|
158
163
|
const handlePopoverInteractOutside = useCallback(
|
|
@@ -129,8 +129,11 @@ export const useComboboxState = (params: UseComboboxStateParams): UseComboboxSta
|
|
|
129
129
|
: [optionValue];
|
|
130
130
|
updateValue(next);
|
|
131
131
|
setQuery('');
|
|
132
|
+
if (!multiple) {
|
|
133
|
+
setIsOpen(false);
|
|
134
|
+
}
|
|
132
135
|
},
|
|
133
|
-
[multiple, selectedValues, selectedValuesSet, updateValue],
|
|
136
|
+
[multiple, selectedValues, selectedValuesSet, setIsOpen, updateValue],
|
|
134
137
|
);
|
|
135
138
|
|
|
136
139
|
const removeValue = useCallback(
|
|
@@ -5,17 +5,35 @@ import { DatePicker } from './DatePicker';
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: 'Components/DatePicker',
|
|
7
7
|
component: DatePicker,
|
|
8
|
+
decorators: [
|
|
9
|
+
Story => (
|
|
10
|
+
<div style={{ maxWidth: '220px', width: '100%' }}>
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
8
15
|
parameters: {
|
|
9
16
|
layout: 'centered',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'`DatePicker` uses a native `type="date"` input (`yyyy-MM-dd`) with a synced calendar popover. When empty, hint copy uses a decorative span (browsers do not reliably show `placeholder` on native date fields). `displayFormat` controls that hint (`DD/MM/YYYY` vs `Pick a date`). The custom month/year header uses the design-system select control.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
10
23
|
},
|
|
11
24
|
tags: ['autodocs'],
|
|
12
25
|
args: {
|
|
13
26
|
onChange: fn(),
|
|
14
27
|
},
|
|
15
28
|
argTypes: {
|
|
16
|
-
|
|
29
|
+
displayFormat: {
|
|
30
|
+
control: 'inline-radio',
|
|
31
|
+
options: ['default', 'friendly'],
|
|
32
|
+
description: 'Controls the empty-state hint text (`DD/MM/YYYY` vs `Pick a date`). The field value is always native ISO `yyyy-MM-dd`.',
|
|
33
|
+
},
|
|
34
|
+
placeholder: {
|
|
17
35
|
control: 'text',
|
|
18
|
-
description: '
|
|
36
|
+
description: 'Optional override for the empty-state hint (defaults from `displayFormat`).',
|
|
19
37
|
},
|
|
20
38
|
className: {
|
|
21
39
|
control: 'text',
|
|
@@ -30,18 +48,58 @@ const meta = {
|
|
|
30
48
|
export default meta;
|
|
31
49
|
type Story = StoryObj<typeof meta>;
|
|
32
50
|
|
|
33
|
-
export const Default: Story = {
|
|
51
|
+
export const Default: Story = {
|
|
52
|
+
parameters: {
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
story: 'Empty field shows hint **DD/MM/YYYY** (`displayFormat="default"`).',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
34
60
|
|
|
35
|
-
export const
|
|
36
|
-
name: '
|
|
61
|
+
export const FriendlyFormat: Story = {
|
|
62
|
+
name: 'Friendly Format',
|
|
37
63
|
args: {
|
|
38
|
-
|
|
64
|
+
displayFormat: 'friendly',
|
|
65
|
+
},
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
story: 'Empty field shows hint **Pick a date**.',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
39
72
|
},
|
|
40
73
|
};
|
|
41
74
|
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
/** `placeholder` overrides the default empty-state hint. */
|
|
76
|
+
export const PlaceholderCustomOverride: Story = {
|
|
77
|
+
name: 'Placeholder · custom copy',
|
|
44
78
|
args: {
|
|
45
|
-
|
|
79
|
+
displayFormat: 'default',
|
|
80
|
+
placeholder: 'e.g. 25/12/2024',
|
|
81
|
+
},
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
story: 'The optional `placeholder` prop replaces the default hint.',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Friendly format with custom hint copy. */
|
|
92
|
+
export const PlaceholderFriendlyWithCustomCopy: Story = {
|
|
93
|
+
name: 'Placeholder · friendly format, custom hint',
|
|
94
|
+
args: {
|
|
95
|
+
displayFormat: 'friendly',
|
|
96
|
+
placeholder: 'Choose a date…',
|
|
97
|
+
},
|
|
98
|
+
parameters: {
|
|
99
|
+
docs: {
|
|
100
|
+
description: {
|
|
101
|
+
story: '`displayFormat="friendly"` with a custom hint instead of **Pick a date**.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
46
104
|
},
|
|
47
105
|
};
|