@arbor-education/design-system.components 0.9.0 → 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.
Files changed (116) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/dist/components/button/Button.d.ts.map +1 -1
  4. package/dist/components/button/Button.js +2 -2
  5. package/dist/components/button/Button.js.map +1 -1
  6. package/dist/components/combobox/Combobox.d.ts.map +1 -1
  7. package/dist/components/combobox/Combobox.js +2 -1
  8. package/dist/components/combobox/Combobox.js.map +1 -1
  9. package/dist/components/combobox/Combobox.test.js +98 -61
  10. package/dist/components/combobox/Combobox.test.js.map +1 -1
  11. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +3 -1
  12. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -1
  13. package/dist/components/combobox/useComboboxPopoverBehavior.js +7 -6
  14. package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -1
  15. package/dist/components/combobox/useComboboxState.d.ts.map +1 -1
  16. package/dist/components/combobox/useComboboxState.js +4 -1
  17. package/dist/components/combobox/useComboboxState.js.map +1 -1
  18. package/dist/components/datePicker/DatePicker.d.ts +4 -1
  19. package/dist/components/datePicker/DatePicker.d.ts.map +1 -1
  20. package/dist/components/datePicker/DatePicker.js +77 -37
  21. package/dist/components/datePicker/DatePicker.js.map +1 -1
  22. package/dist/components/datePicker/DatePicker.stories.d.ts +28 -3
  23. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  24. package/dist/components/datePicker/DatePicker.stories.js +62 -9
  25. package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
  26. package/dist/components/datePicker/DatePicker.test.js +133 -66
  27. package/dist/components/datePicker/DatePicker.test.js.map +1 -1
  28. package/dist/components/datePicker/DatePickerCalendarHeader.d.ts +8 -0
  29. package/dist/components/datePicker/DatePickerCalendarHeader.d.ts.map +1 -0
  30. package/dist/components/datePicker/DatePickerCalendarHeader.js +36 -0
  31. package/dist/components/datePicker/DatePickerCalendarHeader.js.map +1 -0
  32. package/dist/components/datePicker/dateInputUtils.d.ts +25 -0
  33. package/dist/components/datePicker/dateInputUtils.d.ts.map +1 -0
  34. package/dist/components/datePicker/dateInputUtils.js +60 -0
  35. package/dist/components/datePicker/dateInputUtils.js.map +1 -0
  36. package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts +2 -0
  37. package/dist/components/datePicker/datePickerTestUtils.test-helpers.d.ts.map +1 -0
  38. package/dist/components/datePicker/datePickerTestUtils.test-helpers.js +4 -0
  39. package/dist/components/datePicker/datePickerTestUtils.test-helpers.js.map +1 -0
  40. package/dist/components/dateTimePicker/DateTimePicker.d.ts +22 -0
  41. package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -0
  42. package/dist/components/dateTimePicker/DateTimePicker.js +132 -0
  43. package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -0
  44. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +77 -0
  45. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -0
  46. package/dist/components/dateTimePicker/DateTimePicker.stories.js +163 -0
  47. package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -0
  48. package/dist/components/dateTimePicker/DateTimePicker.test.d.ts +2 -0
  49. package/dist/components/dateTimePicker/DateTimePicker.test.d.ts.map +1 -0
  50. package/dist/components/dateTimePicker/DateTimePicker.test.js +235 -0
  51. package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -0
  52. package/dist/components/formField/FormField.test.d.ts.map +1 -1
  53. package/dist/components/formField/FormField.test.js +5 -5
  54. package/dist/components/formField/FormField.test.js.map +1 -1
  55. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts +1 -0
  56. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  57. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +7 -3
  58. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  59. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js +12 -0
  60. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.test.js.map +1 -1
  61. package/dist/components/formField/inputs/text/TextInput.d.ts +4 -1
  62. package/dist/components/formField/inputs/text/TextInput.d.ts.map +1 -1
  63. package/dist/components/formField/inputs/text/TextInput.js +5 -4
  64. package/dist/components/formField/inputs/text/TextInput.js.map +1 -1
  65. package/dist/components/formField/inputs/text/TextInput.stories.d.ts +4 -1
  66. package/dist/components/formField/inputs/text/TextInput.stories.d.ts.map +1 -1
  67. package/dist/components/table/Table.d.ts.map +1 -1
  68. package/dist/components/table/Table.js +2 -0
  69. package/dist/components/table/Table.js.map +1 -1
  70. package/dist/components/table/Table.stories.d.ts +1 -0
  71. package/dist/components/table/Table.stories.d.ts.map +1 -1
  72. package/dist/components/table/Table.stories.js +37 -0
  73. package/dist/components/table/Table.stories.js.map +1 -1
  74. package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts +3 -0
  75. package/dist/components/table/cellRenderers/BooleanCellRenderer.d.ts.map +1 -0
  76. package/dist/components/table/cellRenderers/BooleanCellRenderer.js +15 -0
  77. package/dist/components/table/cellRenderers/BooleanCellRenderer.js.map +1 -0
  78. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts +2 -0
  79. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.d.ts.map +1 -0
  80. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js +31 -0
  81. package/dist/components/table/cellRenderers/BooleanCellRenderer.test.js.map +1 -0
  82. package/dist/index.css +258 -3
  83. package/dist/index.css.map +1 -1
  84. package/dist/index.d.ts +3 -0
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +2 -0
  87. package/dist/index.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/components/button/Button.tsx +2 -1
  90. package/src/components/combobox/Combobox.test.tsx +104 -61
  91. package/src/components/combobox/Combobox.tsx +3 -1
  92. package/src/components/combobox/useComboboxPopoverBehavior.ts +10 -5
  93. package/src/components/combobox/useComboboxState.ts +4 -1
  94. package/src/components/datePicker/DatePicker.stories.tsx +67 -9
  95. package/src/components/datePicker/DatePicker.test.tsx +157 -72
  96. package/src/components/datePicker/DatePicker.tsx +163 -69
  97. package/src/components/datePicker/DatePickerCalendarHeader.tsx +82 -0
  98. package/src/components/datePicker/date-field-hint.scss +152 -0
  99. package/src/components/datePicker/dateInputUtils.ts +117 -0
  100. package/src/components/datePicker/datePicker.scss +53 -29
  101. package/src/components/datePicker/datePickerTestUtils.test-helpers.ts +6 -0
  102. package/src/components/dateTimePicker/DateTimePicker.stories.tsx +202 -0
  103. package/src/components/dateTimePicker/DateTimePicker.test.tsx +295 -0
  104. package/src/components/dateTimePicker/DateTimePicker.tsx +293 -0
  105. package/src/components/dateTimePicker/dateTimePicker.scss +17 -0
  106. package/src/components/formField/FormField.test.tsx +5 -5
  107. package/src/components/formField/inputs/selectDropdown/SelectDropdown.test.tsx +28 -0
  108. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +8 -2
  109. package/src/components/formField/inputs/text/TextInput.tsx +6 -3
  110. package/src/components/table/Table.stories.tsx +48 -0
  111. package/src/components/table/Table.tsx +2 -0
  112. package/src/components/table/cellRenderers/BooleanCellRenderer.test.tsx +37 -0
  113. package/src/components/table/cellRenderers/BooleanCellRenderer.tsx +34 -0
  114. package/src/components/table/cellRenderers/booleanCellRenderer.scss +7 -0
  115. package/src/index.scss +2 -0
  116. package/src/index.ts +3 -0
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { Banner, BANNER_LEVEL } from './components/banner/Banner';
4
4
  export { Button } from './components/button/Button';
5
5
  export { Card } from './components/card/Card';
6
6
  export { DatePicker } from './components/datePicker/DatePicker';
7
+ export { DateTimePicker } from './components/dateTimePicker/DateTimePicker';
7
8
  export { Dropdown } from './components/dropdown/Dropdown';
8
9
  export { Tag } from './components/tag/Tag';
9
10
  export { Dot } from './components/dot/Dot';
@@ -32,6 +33,7 @@ export { SingleUser } from './components/singleUser/SingleUser';
32
33
  export { Slideover } from './components/slideover/Slideover';
33
34
  export { SlideoverManager } from './components/slideoverManager/SlideoverManager';
34
35
  export { DefaultCellRenderer } from './components/table/cellRenderers/DefaultCellRenderer';
36
+ export { BooleanCellRenderer } from './components/table/cellRenderers/BooleanCellRenderer';
35
37
  export { DSDefaultColDef } from './components/table/DSDefaultColDef';
36
38
  export { GridApiContext } from './components/table/GridApiContext';
37
39
  export { Table } from './components/table/Table';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbor-education/design-system.components",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The component library for the design system (the baby)",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -37,6 +37,7 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
37
37
  iconLeftName,
38
38
  iconLeftScreenReaderText,
39
39
  borderless = false,
40
+ error = false,
40
41
  ...rest
41
42
  } = props;
42
43
 
@@ -46,7 +47,7 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
46
47
  `ds-button--${variant}`,
47
48
  `ds-button--${size}`,
48
49
  {
49
- 'ds-button--error': props.error,
50
+ 'ds-button--error': error,
50
51
  'ds-button--no-horizontal-padding': !hasHorizontalPadding,
51
52
  'ds-button--icon-only': !children && (iconRightName || iconLeftName),
52
53
  'ds-button--borderless': borderless,
@@ -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
- input.focus();
383
+ await user.click(input);
359
384
 
360
- await userEvent.keyboard('{Control>}a{/Control}');
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
- input.focus();
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
- input.focus();
410
+ await user.click(input);
384
411
 
385
- await userEvent.keyboard('{Control>}a{/Control}');
386
- await userEvent.keyboard('{Backspace}');
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
- input.focus();
426
+ await user.click(input);
399
427
 
400
- await userEvent.keyboard('{Control>}a{/Control}');
401
- await userEvent.keyboard('{Delete}');
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
- input.focus();
439
+ await user.click(input);
411
440
 
412
- await userEvent.keyboard('{Control>}a{/Control}');
441
+ await user.keyboard('{Control>}a{/Control}');
413
442
  expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(2);
414
443
 
415
- await userEvent.type(input, 'c');
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
- input.focus();
453
+ await user.click(input);
424
454
 
425
- await userEvent.keyboard('{Control>}a{/Control}');
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
- input.focus();
618
+ await user.click(input);
588
619
 
589
- await userEvent.keyboard('{ArrowDown}');
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
- input.focus();
629
+ await user.click(input);
598
630
 
599
- await userEvent.keyboard('{Alt>}{ArrowDown}{/Alt}');
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
- input.focus();
680
+ await user.click(input);
648
681
 
649
- await userEvent.keyboard('{Backspace}');
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.getByRole('listbox')).toBeInTheDocument();
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
- input.focus();
1084
+ await user.click(input);
1051
1085
 
1052
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
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
- input.focus();
1097
+ await user.click(input);
1063
1098
 
1064
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1065
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
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
- input.focus();
1111
+ await user.click(input);
1076
1112
 
1077
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1078
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1079
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
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
- input.focus();
1125
+ await user.click(input);
1089
1126
 
1090
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1127
+ await user.keyboard('{ArrowLeft}');
1091
1128
  expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
1092
1129
 
1093
- fireEvent.keyDown(input, { key: 'ArrowRight' });
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
- input.focus();
1141
+ await user.click(input);
1104
1142
 
1105
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1106
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1107
- fireEvent.keyDown(input, { key: 'Backspace' });
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
- input.focus();
1157
+ await user.click(input);
1119
1158
 
1120
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1121
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1122
- fireEvent.keyDown(input, { key: 'Delete' });
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
- input.focus();
1173
+ await user.click(input);
1134
1174
 
1135
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1136
- fireEvent.keyDown(input, { key: 'Backspace' });
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
- input.focus();
1186
+ await user.click(input);
1146
1187
 
1147
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1188
+ await user.keyboard('{ArrowLeft}');
1148
1189
  expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
1149
1190
 
1150
- await userEvent.type(input, 'c');
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
- input.focus();
1199
+ await user.click(input);
1158
1200
 
1159
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
1201
+ await user.keyboard('{ArrowLeft}');
1160
1202
  expect(document.querySelectorAll('.ds-tag--selected')).toHaveLength(1);
1161
1203
 
1162
- fireEvent.keyDown(input, { key: 'Escape' });
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
- input.focus();
1212
+ await user.click(input);
1170
1213
 
1171
- fireEvent.keyDown(input, { key: 'ArrowLeft' });
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 = loading || totalItems > 0;
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 (shouldRenderPopoverContent) {
118
+ if (canOpenPopover) {
114
119
  openPopover();
115
120
  }
116
- }, [disabled, openPopover, shouldRenderPopoverContent]);
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 (shouldRenderPopoverContent) {
156
+ if (canOpenPopover) {
152
157
  openPopover();
153
158
  }
154
159
  },
155
- [disabled, inputRef, openPopover, shouldRenderPopoverContent, triggerContainsInput],
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
- dateFormat: {
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: 'Date format string using date-fns tokens (e.g. "dd/MM/yyyy", "MM/dd/yyyy", "yyyy-MM-dd")',
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 USDateFormat: Story = {
36
- name: 'US Date Format (MM/dd/yyyy)',
61
+ export const FriendlyFormat: Story = {
62
+ name: 'Friendly Format',
37
63
  args: {
38
- dateFormat: 'MM/dd/yyyy',
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
- export const ISOFormat: Story = {
43
- name: 'ISO Format (yyyy-MM-dd)',
75
+ /** `placeholder` overrides the default empty-state hint. */
76
+ export const PlaceholderCustomOverride: Story = {
77
+ name: 'Placeholder · custom copy',
44
78
  args: {
45
- dateFormat: 'yyyy-MM-dd',
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
  };