@dbcdk/react-components 0.0.211 → 0.0.213

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 (56) hide show
  1. package/README.md +6 -1
  2. package/dist/components/button/Button.cjs +2 -1
  3. package/dist/components/button/Button.d.ts +6 -1
  4. package/dist/components/button/Button.js +2 -1
  5. package/dist/components/button/Button.module.css +14 -0
  6. package/dist/components/color-scheme-button/ColorSchemeButton.cjs +2 -2
  7. package/dist/components/color-scheme-button/ColorSchemeButton.js +2 -2
  8. package/dist/components/forms/file-upload/FileUpload.cjs +4 -0
  9. package/dist/components/forms/file-upload/FileUpload.js +4 -0
  10. package/dist/components/forms/form-select/FormSelect.cjs +4 -0
  11. package/dist/components/forms/form-select/FormSelect.d.ts +1 -1
  12. package/dist/components/forms/form-select/FormSelect.js +4 -0
  13. package/dist/components/forms/input/Input.cjs +4 -0
  14. package/dist/components/forms/input/Input.js +4 -0
  15. package/dist/components/forms/input-container/InputContainer.cjs +1 -1
  16. package/dist/components/forms/input-container/InputContainer.js +1 -1
  17. package/dist/components/forms/input-container/InputContainer.module.css +12 -1
  18. package/dist/components/forms/radio-buttons/RadioButtonGroup.cjs +6 -5
  19. package/dist/components/forms/radio-buttons/RadioButtonGroup.js +2 -5
  20. package/dist/components/forms/radio-buttons/RadioButtons.module.css +15 -0
  21. package/dist/components/forms/select/Select.cjs +4 -0
  22. package/dist/components/forms/select/Select.d.ts +1 -1
  23. package/dist/components/forms/select/Select.js +4 -0
  24. package/dist/components/forms/text-area/Textarea.cjs +4 -0
  25. package/dist/components/forms/text-area/Textarea.js +4 -0
  26. package/dist/components/forms/typeahead/Typeahead.cjs +109 -42
  27. package/dist/components/forms/typeahead/Typeahead.d.ts +6 -0
  28. package/dist/components/forms/typeahead/Typeahead.js +109 -42
  29. package/dist/components/forms/typeahead/Typeahead.module.css +16 -0
  30. package/dist/components/headline/CollapsibleHeadline.cjs +15 -18
  31. package/dist/components/headline/CollapsibleHeadline.d.ts +2 -2
  32. package/dist/components/headline/CollapsibleHeadline.js +15 -18
  33. package/dist/components/headline/Headline.module.css +22 -0
  34. package/dist/components/sidebar/components/SidebarItem.cjs +2 -0
  35. package/dist/components/sidebar/components/SidebarItem.d.ts +2 -1
  36. package/dist/components/sidebar/components/SidebarItem.js +2 -0
  37. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.cjs +2 -0
  38. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.d.ts +2 -1
  39. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +2 -0
  40. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.module.css +1 -1
  41. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +40 -0
  42. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.cjs +2 -0
  43. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.d.ts +2 -1
  44. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +2 -0
  45. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +9 -0
  46. package/dist/components/sidebar/components/sidebar-items/SidebarItems.cjs +6 -4
  47. package/dist/components/sidebar/components/sidebar-items/SidebarItems.js +6 -4
  48. package/dist/components/theme-script/ThemeScript.cjs +1 -1
  49. package/dist/components/theme-script/ThemeScript.d.ts +1 -1
  50. package/dist/components/theme-script/ThemeScript.js +1 -1
  51. package/dist/hooks/useColorScheme.cjs +1 -1
  52. package/dist/hooks/useColorScheme.js +1 -1
  53. package/dist/hooks/useGetActiveTheme.cjs +1 -1
  54. package/dist/hooks/useGetActiveTheme.js +1 -1
  55. package/dist/styles/themes/dbc/colors.css +4 -4
  56. package/package.json +1 -1
@@ -32,6 +32,23 @@ function _interopNamespace(e) {
32
32
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
33
33
  var styles__default = /*#__PURE__*/_interopDefault(styles);
34
34
 
35
+ function orderByGroup(options) {
36
+ if (!options.some((option) => option.group)) return options;
37
+ const groups = /* @__PURE__ */ new Map();
38
+ for (const option of options) {
39
+ const group = option.group || void 0;
40
+ const entries = groups.get(group);
41
+ if (entries) entries.push(option);
42
+ else groups.set(group, [option]);
43
+ }
44
+ return Array.from(groups.values()).flat();
45
+ }
46
+ function findEnabledIndex(items, start, step) {
47
+ for (let index = start; index >= 0 && index < items.length; index += step) {
48
+ if (!items[index].disabled) return index;
49
+ }
50
+ return -1;
51
+ }
35
52
  function parseLengthToPx(value, referenceFontSize = 16) {
36
53
  if (!value) return 0;
37
54
  const trimmed = value.trim();
@@ -197,16 +214,25 @@ function Typeahead({
197
214
  filteredOptions = options;
198
215
  } else {
199
216
  filteredOptions = options.filter(
200
- (option) => option.label.toLocaleLowerCase().includes(normalizedQuery)
217
+ (option) => {
218
+ var _a2, _b, _c, _d;
219
+ return option.label.toLocaleLowerCase().includes(normalizedQuery) || ((_b = (_a2 = option.description) == null ? void 0 : _a2.toLocaleLowerCase().includes(normalizedQuery)) != null ? _b : false) || ((_d = (_c = option.group) == null ? void 0 : _c.toLocaleLowerCase().includes(normalizedQuery)) != null ? _d : false);
220
+ }
201
221
  );
202
222
  }
203
223
  }
224
+ filteredOptions = orderByGroup(filteredOptions);
204
225
  if (typeof maxVisibleOptions === "number" && Number.isFinite(maxVisibleOptions)) {
205
226
  filteredOptions = filteredOptions.slice(0, Math.max(0, maxVisibleOptions));
206
227
  }
228
+ const groupStarts = /* @__PURE__ */ new Map();
229
+ filteredOptions.forEach((option, index) => {
230
+ if (option.group && !groupStarts.has(option.group)) groupStarts.set(option.group, index);
231
+ });
207
232
  const commitSelection = React__namespace.useCallback(
208
233
  (option) => {
209
234
  var _a2, _b;
235
+ if (option == null ? void 0 : option.disabled) return;
210
236
  locallyClearedRef.current = false;
211
237
  setHideSelectedHighlight(false);
212
238
  if (mode === "multi") {
@@ -334,24 +360,21 @@ function Typeahead({
334
360
  }, [selectedOption, selectedSingleText, mode]);
335
361
  const getSelectedIndex = React__namespace.useCallback(
336
362
  (items) => {
337
- if (items.length === 0) return -1;
338
- if (locallyClearedRef.current) return 0;
339
- if (mode === "multi") {
340
- if (!Array.isArray(selectedValue) || selectedValue.length === 0) return 0;
341
- const firstSelectedIndex = items.findIndex((option) => selectedValue.includes(option.value));
342
- return firstSelectedIndex >= 0 ? firstSelectedIndex : 0;
343
- }
344
- const singleSelectedIndex = items.findIndex((option) => option.value === selectedValue);
345
- return singleSelectedIndex >= 0 ? singleSelectedIndex : 0;
363
+ const firstEnabled = findEnabledIndex(items, 0, 1);
364
+ if (firstEnabled < 0 || locallyClearedRef.current) return firstEnabled;
365
+ const selectedIndex = items.findIndex(
366
+ (option) => !option.disabled && (mode === "multi" ? Array.isArray(selectedValue) && selectedValue.includes(option.value) : option.value === selectedValue)
367
+ );
368
+ return selectedIndex >= 0 ? selectedIndex : firstEnabled;
346
369
  },
347
370
  [mode, selectedValue]
348
371
  );
349
372
  React.useEffect(() => {
350
373
  setActiveIndex((current) => {
351
- if (filteredOptions.length === 0) return -1;
352
- if (locallyClearedRef.current) return 0;
353
- if (current < 0) return getSelectedIndex(filteredOptions);
354
- return Math.min(current, filteredOptions.length - 1);
374
+ if (locallyClearedRef.current || current < 0 || !filteredOptions[current] || filteredOptions[current].disabled) {
375
+ return getSelectedIndex(filteredOptions);
376
+ }
377
+ return current;
355
378
  });
356
379
  }, [filteredOptions, getSelectedIndex]);
357
380
  React.useEffect(() => {
@@ -452,7 +475,14 @@ function Typeahead({
452
475
  }
453
476
  const exactLabelMatch = options.find((option) => option.label === inputValue);
454
477
  if (exactLabelMatch) {
455
- commitSelection(exactLabelMatch);
478
+ if (exactLabelMatch.disabled) {
479
+ setInputValue(selectedSingleText);
480
+ setQuery("");
481
+ setOpen(false);
482
+ setActiveIndex(-1);
483
+ } else {
484
+ commitSelection(exactLabelMatch);
485
+ }
456
486
  return;
457
487
  }
458
488
  if (allowCustomValue) {
@@ -484,7 +514,7 @@ function Typeahead({
484
514
  if (justClearedRef.current) return;
485
515
  setQuery("");
486
516
  setOpen(true);
487
- setActiveIndex(getSelectedIndex(options));
517
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
488
518
  }, [getSelectedIndex, options]);
489
519
  const openWithCurrentFilter = React__namespace.useCallback(() => {
490
520
  if (justClearedRef.current) return;
@@ -526,7 +556,7 @@ function Typeahead({
526
556
  if (isAlreadyFocused && mode === "single" && selectedOption && !locallyClearedRef.current) {
527
557
  prepareSingleSearchInput();
528
558
  setOpen(true);
529
- setActiveIndex(getSelectedIndex(options));
559
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
530
560
  } else if (isAlreadyFocused && !open) {
531
561
  setOpen(true);
532
562
  setActiveIndex(getSelectedIndex(filteredOptions));
@@ -573,7 +603,8 @@ function Typeahead({
573
603
  setActiveIndex((index) => {
574
604
  if (filteredOptions.length === 0) return -1;
575
605
  if (index < 0) return getSelectedIndex(filteredOptions);
576
- return Math.min(index + 1, filteredOptions.length - 1);
606
+ const next = findEnabledIndex(filteredOptions, index + 1, 1);
607
+ return next >= 0 ? next : index;
577
608
  });
578
609
  return;
579
610
  case "ArrowUp":
@@ -583,20 +614,23 @@ function Typeahead({
583
614
  return;
584
615
  }
585
616
  setActiveIndex((index) => {
586
- if (filteredOptions.length === 0) return -1;
587
- if (index < 0) return filteredOptions.length - 1;
588
- return Math.max(index - 1, 0);
617
+ const next = findEnabledIndex(
618
+ filteredOptions,
619
+ index < 0 ? filteredOptions.length - 1 : index - 1,
620
+ -1
621
+ );
622
+ return next >= 0 ? next : index;
589
623
  });
590
624
  return;
591
625
  case "Home":
592
626
  if (!open || filteredOptions.length === 0) return;
593
627
  e.preventDefault();
594
- setActiveIndex(0);
628
+ setActiveIndex(findEnabledIndex(filteredOptions, 0, 1));
595
629
  return;
596
630
  case "End":
597
631
  if (!open || filteredOptions.length === 0) return;
598
632
  e.preventDefault();
599
- setActiveIndex(filteredOptions.length - 1);
633
+ setActiveIndex(findEnabledIndex(filteredOptions, filteredOptions.length - 1, -1));
600
634
  return;
601
635
  case "Enter":
602
636
  if (!open) return;
@@ -650,7 +684,7 @@ function Typeahead({
650
684
  if (nextOpen) {
651
685
  if (mode === "single" && selectedOption && !locallyClearedRef.current) {
652
686
  setQuery("");
653
- setActiveIndex(getSelectedIndex(options));
687
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
654
688
  } else {
655
689
  setActiveIndex(getSelectedIndex(filteredOptions));
656
690
  }
@@ -768,41 +802,74 @@ function Typeahead({
768
802
  const isActive = index === activeIndex;
769
803
  const isSelected = mode === "multi" ? Array.isArray(selectedValue) && selectedValue.includes(option.value) : option.value === selectedValue && !hideSelectedHighlight;
770
804
  const optionId = `${listboxId}-option-${index}`;
771
- return mode === "multi" ? /* @__PURE__ */ jsxRuntime.jsx(
805
+ const groupId = option.group ? `${listboxId}-group-${groupStarts.get(option.group)}` : void 0;
806
+ const startsGroup = !!option.group && groupStarts.get(option.group) === index;
807
+ const descriptionId = option.description ? `${optionId}-description` : void 0;
808
+ const describedBy = [groupId, descriptionId].filter(Boolean).join(" ") || void 0;
809
+ const labelContent = /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles__default.default.optionText, children: [
810
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: option.label }),
811
+ option.description && /* @__PURE__ */ jsxRuntime.jsx("span", { id: descriptionId, className: styles__default.default.description, children: option.description })
812
+ ] });
813
+ const item = mode === "multi" ? /* @__PURE__ */ jsxRuntime.jsx(
772
814
  Menu.Menu.CheckItem,
773
815
  {
774
816
  checked: isSelected,
817
+ disabled: option.disabled,
775
818
  active: isActive,
776
819
  interactiveProps: {
777
820
  id: optionId,
778
821
  role: "option",
779
- onMouseEnter: () => setActiveIndex(index),
822
+ "aria-describedby": describedBy,
823
+ "aria-label": option.label,
824
+ "aria-disabled": option.disabled || void 0,
825
+ onMouseEnter: () => {
826
+ if (!option.disabled) setActiveIndex(index);
827
+ },
780
828
  onMouseDown: (e) => {
781
829
  interactingWithOptionsRef.current = true;
782
830
  e.preventDefault();
783
831
  },
784
832
  onKeyDown: (e) => handleTabKeyDown(e)
785
833
  },
786
- label: /* @__PURE__ */ jsxRuntime.jsx("span", { children: option.label }),
834
+ label: labelContent,
787
835
  onCheckedChange: () => commitSelection(option)
788
836
  },
789
837
  option.value
790
- ) : /* @__PURE__ */ jsxRuntime.jsx(Menu.Menu.Item, { active: isActive, selected: isSelected, children: /* @__PURE__ */ jsxRuntime.jsx(
791
- "button",
838
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
839
+ Menu.Menu.Item,
792
840
  {
793
- id: optionId,
794
- type: "button",
795
- role: "option",
796
- "aria-selected": isSelected,
797
- onMouseEnter: () => setActiveIndex(index),
798
- onMouseDown: (e) => {
799
- e.preventDefault();
800
- },
801
- onKeyDown: (e) => handleTabKeyDown(e),
802
- onClick: () => commitSelection(option),
803
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: option.label })
804
- }
805
- ) }, option.value);
841
+ active: isActive,
842
+ selected: isSelected,
843
+ disabled: option.disabled,
844
+ children: /* @__PURE__ */ jsxRuntime.jsx(
845
+ "button",
846
+ {
847
+ id: optionId,
848
+ type: "button",
849
+ role: "option",
850
+ "aria-selected": isSelected,
851
+ "aria-describedby": describedBy,
852
+ "aria-label": option.label,
853
+ "aria-disabled": option.disabled || void 0,
854
+ disabled: option.disabled,
855
+ onMouseEnter: () => {
856
+ if (!option.disabled) setActiveIndex(index);
857
+ },
858
+ onMouseDown: (e) => {
859
+ e.preventDefault();
860
+ },
861
+ onKeyDown: (e) => handleTabKeyDown(e),
862
+ onClick: () => commitSelection(option),
863
+ children: labelContent
864
+ }
865
+ )
866
+ },
867
+ option.value
868
+ );
869
+ return /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Fragment, { children: [
870
+ startsGroup && /* @__PURE__ */ jsxRuntime.jsx(Menu.Menu.Header, { id: groupId, children: option.group }),
871
+ item
872
+ ] }, option.value);
806
873
  }) : /* @__PURE__ */ jsxRuntime.jsx(Menu.Menu.Item, { disabled: true, children: emptyMessage }) })
807
874
  }
808
875
  ),
@@ -3,6 +3,12 @@ import { InputProps, InputVariant } from '../../../components/forms/input/Input'
3
3
  type TypeaheadOption<T> = {
4
4
  label: string;
5
5
  value: T;
6
+ /** Optional section header; groups follow their first occurrence in the list. */
7
+ group?: string;
8
+ /** Secondary option text, included in default search but not the selected input value. */
9
+ description?: string;
10
+ /** Visible but unavailable for selection by pointer, keyboard, or exact-label commit. */
11
+ disabled?: boolean;
6
12
  };
7
13
  type MultiValueDisplayMode = 'chips' | 'count';
8
14
  type MultiSelectedValuesDisplayMode = 'hidden' | 'below-input';
@@ -8,6 +8,23 @@ import { Menu } from '../../../components/menu/Menu';
8
8
  import { Popover } from '../../../components/popover/Popover';
9
9
  import styles from './Typeahead.module.css';
10
10
 
11
+ function orderByGroup(options) {
12
+ if (!options.some((option) => option.group)) return options;
13
+ const groups = /* @__PURE__ */ new Map();
14
+ for (const option of options) {
15
+ const group = option.group || void 0;
16
+ const entries = groups.get(group);
17
+ if (entries) entries.push(option);
18
+ else groups.set(group, [option]);
19
+ }
20
+ return Array.from(groups.values()).flat();
21
+ }
22
+ function findEnabledIndex(items, start, step) {
23
+ for (let index = start; index >= 0 && index < items.length; index += step) {
24
+ if (!items[index].disabled) return index;
25
+ }
26
+ return -1;
27
+ }
11
28
  function parseLengthToPx(value, referenceFontSize = 16) {
12
29
  if (!value) return 0;
13
30
  const trimmed = value.trim();
@@ -173,16 +190,25 @@ function Typeahead({
173
190
  filteredOptions = options;
174
191
  } else {
175
192
  filteredOptions = options.filter(
176
- (option) => option.label.toLocaleLowerCase().includes(normalizedQuery)
193
+ (option) => {
194
+ var _a2, _b, _c, _d;
195
+ return option.label.toLocaleLowerCase().includes(normalizedQuery) || ((_b = (_a2 = option.description) == null ? void 0 : _a2.toLocaleLowerCase().includes(normalizedQuery)) != null ? _b : false) || ((_d = (_c = option.group) == null ? void 0 : _c.toLocaleLowerCase().includes(normalizedQuery)) != null ? _d : false);
196
+ }
177
197
  );
178
198
  }
179
199
  }
200
+ filteredOptions = orderByGroup(filteredOptions);
180
201
  if (typeof maxVisibleOptions === "number" && Number.isFinite(maxVisibleOptions)) {
181
202
  filteredOptions = filteredOptions.slice(0, Math.max(0, maxVisibleOptions));
182
203
  }
204
+ const groupStarts = /* @__PURE__ */ new Map();
205
+ filteredOptions.forEach((option, index) => {
206
+ if (option.group && !groupStarts.has(option.group)) groupStarts.set(option.group, index);
207
+ });
183
208
  const commitSelection = React.useCallback(
184
209
  (option) => {
185
210
  var _a2, _b;
211
+ if (option == null ? void 0 : option.disabled) return;
186
212
  locallyClearedRef.current = false;
187
213
  setHideSelectedHighlight(false);
188
214
  if (mode === "multi") {
@@ -310,24 +336,21 @@ function Typeahead({
310
336
  }, [selectedOption, selectedSingleText, mode]);
311
337
  const getSelectedIndex = React.useCallback(
312
338
  (items) => {
313
- if (items.length === 0) return -1;
314
- if (locallyClearedRef.current) return 0;
315
- if (mode === "multi") {
316
- if (!Array.isArray(selectedValue) || selectedValue.length === 0) return 0;
317
- const firstSelectedIndex = items.findIndex((option) => selectedValue.includes(option.value));
318
- return firstSelectedIndex >= 0 ? firstSelectedIndex : 0;
319
- }
320
- const singleSelectedIndex = items.findIndex((option) => option.value === selectedValue);
321
- return singleSelectedIndex >= 0 ? singleSelectedIndex : 0;
339
+ const firstEnabled = findEnabledIndex(items, 0, 1);
340
+ if (firstEnabled < 0 || locallyClearedRef.current) return firstEnabled;
341
+ const selectedIndex = items.findIndex(
342
+ (option) => !option.disabled && (mode === "multi" ? Array.isArray(selectedValue) && selectedValue.includes(option.value) : option.value === selectedValue)
343
+ );
344
+ return selectedIndex >= 0 ? selectedIndex : firstEnabled;
322
345
  },
323
346
  [mode, selectedValue]
324
347
  );
325
348
  useEffect(() => {
326
349
  setActiveIndex((current) => {
327
- if (filteredOptions.length === 0) return -1;
328
- if (locallyClearedRef.current) return 0;
329
- if (current < 0) return getSelectedIndex(filteredOptions);
330
- return Math.min(current, filteredOptions.length - 1);
350
+ if (locallyClearedRef.current || current < 0 || !filteredOptions[current] || filteredOptions[current].disabled) {
351
+ return getSelectedIndex(filteredOptions);
352
+ }
353
+ return current;
331
354
  });
332
355
  }, [filteredOptions, getSelectedIndex]);
333
356
  useEffect(() => {
@@ -428,7 +451,14 @@ function Typeahead({
428
451
  }
429
452
  const exactLabelMatch = options.find((option) => option.label === inputValue);
430
453
  if (exactLabelMatch) {
431
- commitSelection(exactLabelMatch);
454
+ if (exactLabelMatch.disabled) {
455
+ setInputValue(selectedSingleText);
456
+ setQuery("");
457
+ setOpen(false);
458
+ setActiveIndex(-1);
459
+ } else {
460
+ commitSelection(exactLabelMatch);
461
+ }
432
462
  return;
433
463
  }
434
464
  if (allowCustomValue) {
@@ -460,7 +490,7 @@ function Typeahead({
460
490
  if (justClearedRef.current) return;
461
491
  setQuery("");
462
492
  setOpen(true);
463
- setActiveIndex(getSelectedIndex(options));
493
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
464
494
  }, [getSelectedIndex, options]);
465
495
  const openWithCurrentFilter = React.useCallback(() => {
466
496
  if (justClearedRef.current) return;
@@ -502,7 +532,7 @@ function Typeahead({
502
532
  if (isAlreadyFocused && mode === "single" && selectedOption && !locallyClearedRef.current) {
503
533
  prepareSingleSearchInput();
504
534
  setOpen(true);
505
- setActiveIndex(getSelectedIndex(options));
535
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
506
536
  } else if (isAlreadyFocused && !open) {
507
537
  setOpen(true);
508
538
  setActiveIndex(getSelectedIndex(filteredOptions));
@@ -549,7 +579,8 @@ function Typeahead({
549
579
  setActiveIndex((index) => {
550
580
  if (filteredOptions.length === 0) return -1;
551
581
  if (index < 0) return getSelectedIndex(filteredOptions);
552
- return Math.min(index + 1, filteredOptions.length - 1);
582
+ const next = findEnabledIndex(filteredOptions, index + 1, 1);
583
+ return next >= 0 ? next : index;
553
584
  });
554
585
  return;
555
586
  case "ArrowUp":
@@ -559,20 +590,23 @@ function Typeahead({
559
590
  return;
560
591
  }
561
592
  setActiveIndex((index) => {
562
- if (filteredOptions.length === 0) return -1;
563
- if (index < 0) return filteredOptions.length - 1;
564
- return Math.max(index - 1, 0);
593
+ const next = findEnabledIndex(
594
+ filteredOptions,
595
+ index < 0 ? filteredOptions.length - 1 : index - 1,
596
+ -1
597
+ );
598
+ return next >= 0 ? next : index;
565
599
  });
566
600
  return;
567
601
  case "Home":
568
602
  if (!open || filteredOptions.length === 0) return;
569
603
  e.preventDefault();
570
- setActiveIndex(0);
604
+ setActiveIndex(findEnabledIndex(filteredOptions, 0, 1));
571
605
  return;
572
606
  case "End":
573
607
  if (!open || filteredOptions.length === 0) return;
574
608
  e.preventDefault();
575
- setActiveIndex(filteredOptions.length - 1);
609
+ setActiveIndex(findEnabledIndex(filteredOptions, filteredOptions.length - 1, -1));
576
610
  return;
577
611
  case "Enter":
578
612
  if (!open) return;
@@ -626,7 +660,7 @@ function Typeahead({
626
660
  if (nextOpen) {
627
661
  if (mode === "single" && selectedOption && !locallyClearedRef.current) {
628
662
  setQuery("");
629
- setActiveIndex(getSelectedIndex(options));
663
+ setActiveIndex(getSelectedIndex(orderByGroup(options)));
630
664
  } else {
631
665
  setActiveIndex(getSelectedIndex(filteredOptions));
632
666
  }
@@ -744,41 +778,74 @@ function Typeahead({
744
778
  const isActive = index === activeIndex;
745
779
  const isSelected = mode === "multi" ? Array.isArray(selectedValue) && selectedValue.includes(option.value) : option.value === selectedValue && !hideSelectedHighlight;
746
780
  const optionId = `${listboxId}-option-${index}`;
747
- return mode === "multi" ? /* @__PURE__ */ jsx(
781
+ const groupId = option.group ? `${listboxId}-group-${groupStarts.get(option.group)}` : void 0;
782
+ const startsGroup = !!option.group && groupStarts.get(option.group) === index;
783
+ const descriptionId = option.description ? `${optionId}-description` : void 0;
784
+ const describedBy = [groupId, descriptionId].filter(Boolean).join(" ") || void 0;
785
+ const labelContent = /* @__PURE__ */ jsxs("span", { className: styles.optionText, children: [
786
+ /* @__PURE__ */ jsx("span", { children: option.label }),
787
+ option.description && /* @__PURE__ */ jsx("span", { id: descriptionId, className: styles.description, children: option.description })
788
+ ] });
789
+ const item = mode === "multi" ? /* @__PURE__ */ jsx(
748
790
  Menu.CheckItem,
749
791
  {
750
792
  checked: isSelected,
793
+ disabled: option.disabled,
751
794
  active: isActive,
752
795
  interactiveProps: {
753
796
  id: optionId,
754
797
  role: "option",
755
- onMouseEnter: () => setActiveIndex(index),
798
+ "aria-describedby": describedBy,
799
+ "aria-label": option.label,
800
+ "aria-disabled": option.disabled || void 0,
801
+ onMouseEnter: () => {
802
+ if (!option.disabled) setActiveIndex(index);
803
+ },
756
804
  onMouseDown: (e) => {
757
805
  interactingWithOptionsRef.current = true;
758
806
  e.preventDefault();
759
807
  },
760
808
  onKeyDown: (e) => handleTabKeyDown(e)
761
809
  },
762
- label: /* @__PURE__ */ jsx("span", { children: option.label }),
810
+ label: labelContent,
763
811
  onCheckedChange: () => commitSelection(option)
764
812
  },
765
813
  option.value
766
- ) : /* @__PURE__ */ jsx(Menu.Item, { active: isActive, selected: isSelected, children: /* @__PURE__ */ jsx(
767
- "button",
814
+ ) : /* @__PURE__ */ jsx(
815
+ Menu.Item,
768
816
  {
769
- id: optionId,
770
- type: "button",
771
- role: "option",
772
- "aria-selected": isSelected,
773
- onMouseEnter: () => setActiveIndex(index),
774
- onMouseDown: (e) => {
775
- e.preventDefault();
776
- },
777
- onKeyDown: (e) => handleTabKeyDown(e),
778
- onClick: () => commitSelection(option),
779
- children: /* @__PURE__ */ jsx("span", { children: option.label })
780
- }
781
- ) }, option.value);
817
+ active: isActive,
818
+ selected: isSelected,
819
+ disabled: option.disabled,
820
+ children: /* @__PURE__ */ jsx(
821
+ "button",
822
+ {
823
+ id: optionId,
824
+ type: "button",
825
+ role: "option",
826
+ "aria-selected": isSelected,
827
+ "aria-describedby": describedBy,
828
+ "aria-label": option.label,
829
+ "aria-disabled": option.disabled || void 0,
830
+ disabled: option.disabled,
831
+ onMouseEnter: () => {
832
+ if (!option.disabled) setActiveIndex(index);
833
+ },
834
+ onMouseDown: (e) => {
835
+ e.preventDefault();
836
+ },
837
+ onKeyDown: (e) => handleTabKeyDown(e),
838
+ onClick: () => commitSelection(option),
839
+ children: labelContent
840
+ }
841
+ )
842
+ },
843
+ option.value
844
+ );
845
+ return /* @__PURE__ */ jsxs(React.Fragment, { children: [
846
+ startsGroup && /* @__PURE__ */ jsx(Menu.Header, { id: groupId, children: option.group }),
847
+ item
848
+ ] }, option.value);
782
849
  }) : /* @__PURE__ */ jsx(Menu.Item, { disabled: true, children: emptyMessage }) })
783
850
  }
784
851
  ),
@@ -36,3 +36,19 @@
36
36
  .typeaheadInput {
37
37
  cursor: pointer;
38
38
  }
39
+
40
+ /* Label and description share one row; the description wraps below only when space runs out. */
41
+ .optionText {
42
+ display: flex;
43
+ flex-wrap: wrap;
44
+ align-items: baseline;
45
+ column-gap: var(--spacing-md);
46
+ min-width: 0;
47
+ }
48
+
49
+ .description {
50
+ margin-inline-start: auto;
51
+ font-size: var(--font-size-xs);
52
+ color: var(--color-fg-muted);
53
+ text-align: end;
54
+ }
@@ -7,7 +7,6 @@ var react = require('react');
7
7
  var usePersistentState = require('../../hooks/usePersistentState');
8
8
  var Headline = require('./Headline');
9
9
  var styles = require('./Headline.module.css');
10
- var Button = require('../button/Button');
11
10
 
12
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
12
 
@@ -45,7 +44,7 @@ function CollapsibleHeadline({
45
44
  onToggle == null ? void 0 : onToggle();
46
45
  };
47
46
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.collapsibleRoot, children: [
48
- /* @__PURE__ */ jsxRuntime.jsxs(
47
+ /* @__PURE__ */ jsxRuntime.jsx(
49
48
  Headline.Headline,
50
49
  {
51
50
  ...headlineProps,
@@ -53,28 +52,26 @@ function CollapsibleHeadline({
53
52
  weight,
54
53
  size,
55
54
  addition,
56
- children: [
57
- header,
58
- /* @__PURE__ */ jsxRuntime.jsx(
59
- Button.Button,
60
- {
61
- shape: "round",
62
- type: "button",
63
- variant: "inline",
64
- style: { padding: "var(--spacing-2xs)" },
65
- "aria-expanded": isExpanded,
66
- "aria-controls": panelId,
67
- onClick: handleToggle,
68
- children: /* @__PURE__ */ jsxRuntime.jsx(
55
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
56
+ "button",
57
+ {
58
+ type: "button",
59
+ className: styles__default.default.toggleButton,
60
+ "aria-expanded": isExpanded,
61
+ "aria-controls": panelId,
62
+ onClick: handleToggle,
63
+ children: [
64
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: header }),
65
+ /* @__PURE__ */ jsxRuntime.jsx(
69
66
  lucideReact.ChevronDown,
70
67
  {
71
68
  "aria-hidden": true,
72
69
  className: [styles__default.default.toggleChevron, isExpanded ? styles__default.default.toggleChevronExpanded : ""].filter(Boolean).join(" ")
73
70
  }
74
71
  )
75
- }
76
- )
77
- ]
72
+ ]
73
+ }
74
+ )
78
75
  }
79
76
  ),
80
77
  isExpanded && /* @__PURE__ */ jsxRuntime.jsx("div", { id: panelId, className: styles__default.default.collapsiblePanel, children })
@@ -1,7 +1,7 @@
1
1
  import React, { PropsWithChildren } from 'react';
2
2
  import type { HeadlineProps } from './Headline';
3
3
  export interface CollapsibleHeadlineProps extends Omit<HeadlineProps, 'addition' | 'children'> {
4
- /** The headline text — always visible. */
4
+ /** The headline text — always visible. Use addition for separate interactive content. */
5
5
  header: React.ReactNode;
6
6
  /** Controlled expanded state. When omitted, the component manages its own state. */
7
7
  expanded?: boolean;
@@ -9,7 +9,7 @@ export interface CollapsibleHeadlineProps extends Omit<HeadlineProps, 'addition'
9
9
  onToggle?: () => void;
10
10
  /** id of the panel element, used for aria-controls. Generated automatically if omitted. */
11
11
  controls?: string;
12
- /** Extra content rendered between the headline text and the toggle button. */
12
+ /** Extra content rendered beside the heading, outside the toggle button. */
13
13
  addition?: React.ReactNode;
14
14
  /**
15
15
  * When set in uncontrolled mode, the component persists its expanded