@arun-dev/headless 4.8.0 → 4.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.
@@ -32,13 +32,17 @@ __export(index_parts_exports, {
32
32
  ChipRemove: () => ComboboxChipRemove,
33
33
  Clear: () => ComboboxClear,
34
34
  Empty: () => ComboboxEmpty,
35
+ Group: () => ComboboxGroup,
36
+ GroupLabel: () => ComboboxGroupLabel,
35
37
  Input: () => ComboboxInput,
38
+ InputGroup: () => ComboboxInputGroup,
36
39
  Item: () => ComboboxItem,
37
40
  List: () => ComboboxList,
38
41
  Popup: () => ComboboxPopup,
39
42
  Root: () => ComboboxRoot,
40
43
  Status: () => ComboboxStatus,
41
- Trigger: () => ComboboxTrigger
44
+ Trigger: () => ComboboxTrigger,
45
+ Value: () => ComboboxValue
42
46
  });
43
47
 
44
48
  // src/combobox/ComboboxRoot.tsx
@@ -167,6 +171,11 @@ function useComboboxRootContext(part) {
167
171
 
168
172
  // src/combobox/ComboboxRoot.tsx
169
173
  var import_jsx_runtime = require("react/jsx-runtime");
174
+ function isGroupList(items) {
175
+ return items.length > 0 && items.every(
176
+ (entry) => entry !== null && typeof entry === "object" && Array.isArray(entry.items)
177
+ );
178
+ }
170
179
  function defaultItemToString(item) {
171
180
  if (typeof item === "string") return item;
172
181
  if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
@@ -191,6 +200,7 @@ function ComboboxRoot({
191
200
  onLoadMore,
192
201
  loading = false,
193
202
  disabled = false,
203
+ required = false,
194
204
  name,
195
205
  form,
196
206
  children
@@ -227,10 +237,17 @@ function ComboboxRoot({
227
237
  });
228
238
  const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
229
239
  const deferredQuery = (0, import_react3.useDeferredValue)(query);
230
- const filteredItems = (0, import_react3.useMemo)(() => {
231
- if (filter === null) return items;
232
- const run = filter ?? defaultFilter;
233
- return run(items, deferredQuery, itemToString);
240
+ const { filteredItems, filteredGroups } = (0, import_react3.useMemo)(() => {
241
+ const run = filter === null ? (list) => list : (list) => (filter ?? defaultFilter)(
242
+ list,
243
+ deferredQuery,
244
+ itemToString
245
+ );
246
+ if (!isGroupList(items)) {
247
+ return { filteredItems: run(items), filteredGroups: null };
248
+ }
249
+ const groups = items.map((group) => ({ ...group, items: run(group.items) })).filter((group) => group.items.length > 0);
250
+ return { filteredItems: groups.flatMap((group) => group.items), filteredGroups: groups };
234
251
  }, [items, filter, deferredQuery, itemToString]);
235
252
  const indexByKey = (0, import_react3.useMemo)(
236
253
  () => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
@@ -342,6 +359,11 @@ function ComboboxRoot({
342
359
  },
343
360
  [insideRefs]
344
361
  );
362
+ const [hasGroup, setHasGroup] = (0, import_react3.useState)(false);
363
+ const registerGroup = (0, import_react3.useCallback)(() => {
364
+ setHasGroup(true);
365
+ return () => setHasGroup(false);
366
+ }, []);
345
367
  const inputRef = (0, import_react3.useRef)(null);
346
368
  const popupRef = (0, import_react3.useRef)(null);
347
369
  const { current: initial } = (0, import_react3.useRef)({ value, inputValue });
@@ -370,9 +392,11 @@ function ComboboxRoot({
370
392
  open,
371
393
  multiple,
372
394
  disabled,
395
+ required,
373
396
  inputValue,
374
397
  loading,
375
398
  filteredItems,
399
+ filteredGroups,
376
400
  itemToString,
377
401
  itemToKey,
378
402
  indexByKey,
@@ -392,6 +416,8 @@ function ComboboxRoot({
392
416
  chips,
393
417
  registerInside,
394
418
  isInside,
419
+ hasGroup,
420
+ registerGroup,
395
421
  onLoadMore,
396
422
  inputId: `${id}-input`,
397
423
  listId: `${id}-list`,
@@ -404,9 +430,11 @@ function ComboboxRoot({
404
430
  open,
405
431
  multiple,
406
432
  disabled,
433
+ required,
407
434
  inputValue,
408
435
  loading,
409
436
  filteredItems,
437
+ filteredGroups,
410
438
  itemToString,
411
439
  itemToKey,
412
440
  indexByKey,
@@ -426,6 +454,8 @@ function ComboboxRoot({
426
454
  chips,
427
455
  registerInside,
428
456
  isInside,
457
+ hasGroup,
458
+ registerGroup,
429
459
  onLoadMore,
430
460
  id
431
461
  ]
@@ -567,6 +597,7 @@ function ComboboxInput({ className, render, ...rest }) {
567
597
  open,
568
598
  multiple,
569
599
  disabled,
600
+ required,
570
601
  inputValue,
571
602
  filteredItems,
572
603
  itemToKey,
@@ -586,6 +617,7 @@ function ComboboxInput({ className, render, ...rest }) {
586
617
  listId,
587
618
  optionId,
588
619
  anchorName,
620
+ hasGroup,
589
621
  inputRef,
590
622
  popupRef
591
623
  } = useComboboxRootContext("Input");
@@ -594,6 +626,20 @@ function ComboboxInput({ className, render, ...rest }) {
594
626
  () => highlight.get().index,
595
627
  () => -1
596
628
  );
629
+ const missing = required && selectedItems.length === 0;
630
+ const textOnly = missing && inputValue !== "";
631
+ const customValidityRef = (0, import_react5.useRef)(false);
632
+ (0, import_react5.useLayoutEffect)(() => {
633
+ const input = inputRef.current;
634
+ if (!input || typeof input.setCustomValidity !== "function") return;
635
+ if (textOnly) {
636
+ input.setCustomValidity(valueMissingMessage());
637
+ customValidityRef.current = true;
638
+ } else if (customValidityRef.current) {
639
+ input.setCustomValidity("");
640
+ customValidityRef.current = false;
641
+ }
642
+ }, [textOnly, inputRef]);
597
643
  function enabledIndices() {
598
644
  const indices = [];
599
645
  filteredItems.forEach((item, index) => {
@@ -631,9 +677,13 @@ function ComboboxInput({ className, render, ...rest }) {
631
677
  "aria-controls": listId,
632
678
  "aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
633
679
  disabled: disabled || void 0,
680
+ required: missing || void 0,
681
+ // Stays when a selection lifts `required`: the field is still required.
682
+ "aria-required": required || void 0,
634
683
  value: inputValue,
635
684
  ...comboboxDataAttributes({ open, multiple, disabled }),
636
- style: { anchorName },
685
+ // The anchor, unless an InputGroup around it is.
686
+ style: hasGroup ? void 0 : { anchorName },
637
687
  className,
638
688
  ref: inputRef,
639
689
  onChange(event) {
@@ -699,13 +749,49 @@ function ComboboxInput({ className, render, ...rest }) {
699
749
  consumerProps: rest
700
750
  });
701
751
  }
752
+ function valueMissingMessage() {
753
+ const probe = document.createElement("input");
754
+ probe.required = true;
755
+ return probe.validationMessage || "Please select an item in the list.";
756
+ }
702
757
 
703
- // src/combobox/ComboboxTrigger.tsx
758
+ // src/combobox/ComboboxInputGroup.tsx
704
759
  var import_react6 = require("react");
705
- function ComboboxTrigger({ className, children, render, ...rest }) {
706
- const { open, multiple, disabled, setOpen, close, listId, inputRef, registerInside } = useComboboxRootContext("Trigger");
760
+ function ComboboxInputGroup({
761
+ className,
762
+ children,
763
+ render,
764
+ ...rest
765
+ }) {
766
+ const { open, multiple, disabled, anchorName, registerGroup, registerInside, inputRef } = useComboboxRootContext("InputGroup");
707
767
  const elementRef = (0, import_react6.useRef)(null);
768
+ (0, import_react6.useLayoutEffect)(() => registerGroup(), [registerGroup]);
708
769
  (0, import_react6.useLayoutEffect)(() => registerInside(elementRef), [registerInside]);
770
+ return useRender({
771
+ render,
772
+ defaultTagName: "div",
773
+ props: {
774
+ ...comboboxDataAttributes({ open, multiple, disabled }),
775
+ style: { anchorName },
776
+ className,
777
+ children,
778
+ ref: elementRef,
779
+ onMouseDown(event) {
780
+ if (event.target !== event.currentTarget || disabled) return;
781
+ event.preventDefault();
782
+ inputRef.current?.focus();
783
+ }
784
+ },
785
+ consumerProps: rest
786
+ });
787
+ }
788
+
789
+ // src/combobox/ComboboxTrigger.tsx
790
+ var import_react7 = require("react");
791
+ function ComboboxTrigger({ className, children, render, ...rest }) {
792
+ const { open, multiple, disabled, setOpen, close, listId, inputRef, registerInside } = useComboboxRootContext("Trigger");
793
+ const elementRef = (0, import_react7.useRef)(null);
794
+ (0, import_react7.useLayoutEffect)(() => registerInside(elementRef), [registerInside]);
709
795
  return useRender({
710
796
  render,
711
797
  defaultTagName: "button",
@@ -733,11 +819,11 @@ function ComboboxTrigger({ className, children, render, ...rest }) {
733
819
  }
734
820
 
735
821
  // src/combobox/ComboboxClear.tsx
736
- var import_react7 = require("react");
822
+ var import_react8 = require("react");
737
823
  function ComboboxClear({ className, children, render, ...rest }) {
738
824
  const { disabled, selectedItems, clear, inputRef, registerInside } = useComboboxRootContext("Clear");
739
- const elementRef = (0, import_react7.useRef)(null);
740
- (0, import_react7.useLayoutEffect)(() => registerInside(elementRef), [registerInside]);
825
+ const elementRef = (0, import_react8.useRef)(null);
826
+ (0, import_react8.useLayoutEffect)(() => registerInside(elementRef), [registerInside]);
741
827
  return useRender({
742
828
  render,
743
829
  defaultTagName: "button",
@@ -763,7 +849,7 @@ function ComboboxClear({ className, children, render, ...rest }) {
763
849
  }
764
850
 
765
851
  // src/combobox/ComboboxPopup.tsx
766
- var import_react8 = require("react");
852
+ var import_react9 = require("react");
767
853
  function ComboboxPopup({
768
854
  side = "bottom",
769
855
  align = "start",
@@ -773,7 +859,7 @@ function ComboboxPopup({
773
859
  ...rest
774
860
  }) {
775
861
  const { open, multiple, disabled, setOpen, close, isInside, anchorName, inputRef, popupRef } = useComboboxRootContext("Popup");
776
- (0, import_react8.useEffect)(() => {
862
+ (0, import_react9.useEffect)(() => {
777
863
  if (!open) return;
778
864
  function onPointerDown(event) {
779
865
  if (event.target instanceof Node && isInside(event.target)) return;
@@ -782,8 +868,8 @@ function ComboboxPopup({
782
868
  document.addEventListener("pointerdown", onPointerDown, true);
783
869
  return () => document.removeEventListener("pointerdown", onPointerDown, true);
784
870
  }, [open, isInside, close]);
785
- const [, resync] = (0, import_react8.useReducer)((n) => n + 1, 0);
786
- (0, import_react8.useEffect)(() => {
871
+ const [, resync] = (0, import_react9.useReducer)((n) => n + 1, 0);
872
+ (0, import_react9.useEffect)(() => {
787
873
  const popup = popupRef.current;
788
874
  if (!popup || typeof popup.showPopover !== "function") return;
789
875
  const shown = popup.matches(":popover-open");
@@ -820,7 +906,7 @@ function ComboboxPopup({
820
906
  }
821
907
 
822
908
  // src/combobox/ComboboxList.tsx
823
- var import_react9 = require("react");
909
+ var import_react10 = require("react");
824
910
  var import_jsx_runtime2 = require("react/jsx-runtime");
825
911
  function ComboboxList({
826
912
  className,
@@ -828,8 +914,10 @@ function ComboboxList({
828
914
  render,
829
915
  ...rest
830
916
  }) {
831
- const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
832
- const content = typeof children === "function" ? filteredItems.map((item, index) => children(item, index)) : children;
917
+ const { multiple, loading, filteredItems, filteredGroups, onLoadMore, listId } = useComboboxRootContext("List");
918
+ const content = typeof children === "function" ? (filteredGroups ?? filteredItems).map(
919
+ (entry, index) => children(entry, index)
920
+ ) : children;
833
921
  return useRender({
834
922
  render,
835
923
  defaultTagName: "div",
@@ -849,10 +937,10 @@ function ComboboxList({
849
937
  }
850
938
  function LoadMoreSentinel() {
851
939
  const { open, loading, filteredItems, onLoadMore } = useComboboxRootContext("List");
852
- const ref = (0, import_react9.useRef)(null);
853
- const onLoadMoreRef = (0, import_react9.useRef)(onLoadMore);
940
+ const ref = (0, import_react10.useRef)(null);
941
+ const onLoadMoreRef = (0, import_react10.useRef)(onLoadMore);
854
942
  onLoadMoreRef.current = onLoadMore;
855
- (0, import_react9.useEffect)(() => {
943
+ (0, import_react10.useEffect)(() => {
856
944
  const element = ref.current;
857
945
  if (!element || !open || loading || typeof IntersectionObserver === "undefined") return;
858
946
  const observer = new IntersectionObserver((entries) => {
@@ -865,10 +953,52 @@ function LoadMoreSentinel() {
865
953
  }
866
954
 
867
955
  // src/combobox/ComboboxItem.tsx
868
- var import_react10 = require("react");
956
+ var import_react12 = require("react");
957
+
958
+ // src/combobox/ComboboxGroup.tsx
959
+ var import_react11 = require("react");
960
+ var import_jsx_runtime3 = require("react/jsx-runtime");
961
+ var ComboboxGroupContext = (0, import_react11.createContext)(null);
962
+ function useComboboxGroupContext() {
963
+ return (0, import_react11.useContext)(ComboboxGroupContext);
964
+ }
965
+ function ComboboxGroup({
966
+ disabled = false,
967
+ className,
968
+ children,
969
+ render,
970
+ ...rest
971
+ }) {
972
+ useComboboxRootContext("Group");
973
+ const labelId = `${(0, import_react11.useId)()}-label`;
974
+ const [hasLabel, setHasLabel] = (0, import_react11.useState)(false);
975
+ const registerLabel = (0, import_react11.useCallback)(() => {
976
+ setHasLabel(true);
977
+ return () => setHasLabel(false);
978
+ }, []);
979
+ const context = (0, import_react11.useMemo)(
980
+ () => ({ disabled, labelId, registerLabel }),
981
+ [disabled, labelId, registerLabel]
982
+ );
983
+ const element = useRender({
984
+ render,
985
+ defaultTagName: "div",
986
+ props: {
987
+ role: "group",
988
+ "aria-labelledby": hasLabel ? labelId : void 0,
989
+ "data-disabled": disabled ? "" : void 0,
990
+ className,
991
+ children
992
+ },
993
+ consumerProps: rest
994
+ });
995
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComboboxGroupContext.Provider, { value: context, children: element });
996
+ }
997
+
998
+ // src/combobox/ComboboxItem.tsx
869
999
  function ComboboxItem({
870
1000
  value,
871
- disabled = false,
1001
+ disabled: disabledProp = false,
872
1002
  className,
873
1003
  children,
874
1004
  render,
@@ -884,23 +1014,25 @@ function ComboboxItem({
884
1014
  select,
885
1015
  optionId
886
1016
  } = useComboboxRootContext("Item");
1017
+ const group = useComboboxGroupContext();
1018
+ const disabled = disabledProp || (group?.disabled ?? false);
887
1019
  const key = itemToKey(value);
888
1020
  const index = indexByKey.get(key) ?? -1;
889
1021
  const selected = selectedKeys.has(key);
890
- const highlighted = (0, import_react10.useSyncExternalStore)(
1022
+ const highlighted = (0, import_react12.useSyncExternalStore)(
891
1023
  highlight.subscribe,
892
1024
  () => index >= 0 && highlight.get().index === index,
893
1025
  () => false
894
1026
  );
895
- const elementRef = (0, import_react10.useRef)(null);
896
- (0, import_react10.useLayoutEffect)(() => {
1027
+ const elementRef = (0, import_react12.useRef)(null);
1028
+ (0, import_react12.useLayoutEffect)(() => {
897
1029
  if (!disabled) return;
898
1030
  disabledKeys.add(key);
899
1031
  return () => {
900
1032
  disabledKeys.delete(key);
901
1033
  };
902
1034
  }, [disabled, disabledKeys, key]);
903
- (0, import_react10.useEffect)(() => {
1035
+ (0, import_react12.useEffect)(() => {
904
1036
  if (highlighted && highlight.get().reason === "keyboard") {
905
1037
  elementRef.current?.scrollIntoView?.({ block: "nearest" });
906
1038
  }
@@ -931,6 +1063,28 @@ function ComboboxItem({
931
1063
  });
932
1064
  }
933
1065
 
1066
+ // src/combobox/ComboboxGroupLabel.tsx
1067
+ var import_react13 = require("react");
1068
+ function ComboboxGroupLabel({
1069
+ className,
1070
+ children,
1071
+ render,
1072
+ ...rest
1073
+ }) {
1074
+ const group = useComboboxGroupContext();
1075
+ if (group === null) {
1076
+ throw new Error("<Combobox.GroupLabel> must be rendered inside <Combobox.Group>.");
1077
+ }
1078
+ const { labelId, registerLabel } = group;
1079
+ (0, import_react13.useLayoutEffect)(registerLabel, [registerLabel]);
1080
+ return useRender({
1081
+ render,
1082
+ defaultTagName: "div",
1083
+ props: { id: labelId, className, children },
1084
+ consumerProps: rest
1085
+ });
1086
+ }
1087
+
934
1088
  // src/combobox/ComboboxEmpty.tsx
935
1089
  function ComboboxEmpty({ className, children, render, ...rest }) {
936
1090
  const { filteredItems, loading } = useComboboxRootContext("Empty");
@@ -955,11 +1109,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
955
1109
  }
956
1110
 
957
1111
  // src/combobox/ComboboxChip.tsx
958
- var import_react11 = require("react");
959
- var import_jsx_runtime3 = require("react/jsx-runtime");
960
- var ComboboxChipContext = (0, import_react11.createContext)(null);
1112
+ var import_react14 = require("react");
1113
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1114
+ var ComboboxChipContext = (0, import_react14.createContext)(null);
961
1115
  function useComboboxChipContext() {
962
- const context = (0, import_react11.useContext)(ComboboxChipContext);
1116
+ const context = (0, import_react14.useContext)(ComboboxChipContext);
963
1117
  if (context === null) {
964
1118
  throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
965
1119
  }
@@ -974,9 +1128,9 @@ function ComboboxChip({
974
1128
  }) {
975
1129
  const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
976
1130
  const key = itemToKey(value);
977
- const elementRef = (0, import_react11.useRef)(null);
978
- (0, import_react11.useLayoutEffect)(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
979
- const chipContext = (0, import_react11.useMemo)(() => ({ value }), [value]);
1131
+ const elementRef = (0, import_react14.useRef)(null);
1132
+ (0, import_react14.useLayoutEffect)(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
1133
+ const chipContext = (0, import_react14.useMemo)(() => ({ value }), [value]);
980
1134
  function focusAt(index) {
981
1135
  const chip = chips[index];
982
1136
  if (chip) chip.ref.current?.focus();
@@ -1017,7 +1171,7 @@ function ComboboxChip({
1017
1171
  },
1018
1172
  consumerProps: rest
1019
1173
  });
1020
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComboboxChipContext.Provider, { value: chipContext, children: element });
1174
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ComboboxChipContext.Provider, { value: chipContext, children: element });
1021
1175
  }
1022
1176
 
1023
1177
  // src/combobox/ComboboxChipRemove.tsx
@@ -1050,6 +1204,14 @@ function ComboboxChipRemove({
1050
1204
  consumerProps: rest
1051
1205
  });
1052
1206
  }
1207
+
1208
+ // src/combobox/ComboboxValue.tsx
1209
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1210
+ function ComboboxValue({ children }) {
1211
+ const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
1212
+ const value = multiple ? selectedItems : selectedItems[0] ?? null;
1213
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children(value, { itemToString }) });
1214
+ }
1053
1215
  // Annotate the CommonJS export names for ESM import in node:
1054
1216
  0 && (module.exports = {
1055
1217
  Combobox,
@@ -21,12 +21,23 @@ type ComboboxState = {
21
21
  };
22
22
 
23
23
  /** The selection: one item or `null`, or an array with `multiple`. */
24
- type ComboboxValue<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
24
+ type ComboboxValue$1<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
25
25
  /** Takes the whole list, so a filter can rank as well as narrow — a fuzzy search, say. */
26
26
  type ComboboxFilter<T> = (items: readonly T[], query: string, itemToString: (item: T) => string) => T[];
27
+ /**
28
+ * Items under a label, as an `<optgroup>`: any object with an `items` array — the rest, a
29
+ * label or an id, is yours to render.
30
+ */
31
+ type ComboboxGroup$1<T> = {
32
+ readonly items: readonly T[];
33
+ };
27
34
  type ComboboxRootProps<T, Multiple extends boolean = false> = {
28
- /** Every item the list can show. With server search, the current results. */
29
- items: readonly T[];
35
+ /**
36
+ * Every item the list can show. With server search, the current results. Or groups of
37
+ * them: each an object with an `items` array, which the filter searches and `List` renders
38
+ * one by one, leaving out the groups with no match.
39
+ */
40
+ items: readonly T[] | readonly ComboboxGroup$1<T>[];
30
41
  /**
31
42
  * An item's label: what the input shows once it is picked, and what the default filter
32
43
  * matches. Defaults to the item itself for strings, else its `label` property.
@@ -51,10 +62,10 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
51
62
  * Never `undefined` once mounted: the mode is latched at mount. Use `null` — or `[]` with
52
63
  * `multiple` — for nothing selected.
53
64
  */
54
- value?: ComboboxValue<T, Multiple>;
65
+ value?: ComboboxValue$1<T, Multiple>;
55
66
  /** Initial selection when uncontrolled. Read once, at mount. */
56
- defaultValue?: ComboboxValue<T, Multiple>;
57
- onValueChange?: (value: ComboboxValue<T, Multiple>, details: ComboboxChangeDetails) => void;
67
+ defaultValue?: ComboboxValue$1<T, Multiple>;
68
+ onValueChange?: (value: ComboboxValue$1<T, Multiple>, details: ComboboxChangeDetails) => void;
58
69
  /** Controlled text of the input. Provide `onInputValueChange` alongside it. */
59
70
  inputValue?: string;
60
71
  /** Initial text when uncontrolled. Defaults to the selected item's label, without `multiple`. */
@@ -78,6 +89,11 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
78
89
  loading?: boolean;
79
90
  /** Disables the input and every button. */
80
91
  disabled?: boolean;
92
+ /**
93
+ * Something must be selected before the form submits. The input is `required` while the
94
+ * selection is empty, so the browser's own validation reports it.
95
+ */
96
+ required?: boolean;
81
97
  /** Submits each selected item's key under this name, from hidden inputs. */
82
98
  name?: string;
83
99
  /** Associates the hidden inputs with a `<form>` by id, when rendered outside it. */
@@ -96,7 +112,7 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
96
112
  *
97
113
  * Focus stays in the input; the highlighted option is its `aria-activedescendant`.
98
114
  */
99
- declare function ComboboxRoot<T, Multiple extends boolean = false>({ items, itemToString: itemToStringProp, itemToKey: itemToKeyProp, filter, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, inputValue: inputValueProp, defaultInputValue, onInputValueChange, open: openProp, defaultOpen, onOpenChange, onItemHighlighted, onLoadMore, loading, disabled, name, form, children, }: ComboboxRootProps<T, Multiple>): react.JSX.Element;
115
+ declare function ComboboxRoot<T, Multiple extends boolean = false>({ items, itemToString: itemToStringProp, itemToKey: itemToKeyProp, filter, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, inputValue: inputValueProp, defaultInputValue, onInputValueChange, open: openProp, defaultOpen, onOpenChange, onItemHighlighted, onLoadMore, loading, disabled, required, name, form, children, }: ComboboxRootProps<T, Multiple>): react.JSX.Element;
100
116
 
101
117
  type ComboboxInputOwnProps = {
102
118
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -107,18 +123,39 @@ type ComboboxInputOwnProps = {
107
123
  type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'input'>, keyof ComboboxInputOwnProps | 'value' | 'defaultValue'>;
108
124
  /**
109
125
  * The text input: `role="combobox"`, and the element focus stays on. Its text is the Root's
110
- * `inputValue`. It is what the popup is anchored to.
126
+ * `inputValue`. It is what the popup is anchored to, unless an InputGroup wraps it.
111
127
  *
112
128
  * Typing opens the list and filters it. Up and Down open it, or move the highlight, wrapping;
113
129
  * Home and End jump to the ends while it is open; Alt+Down opens it without highlighting.
114
130
  * Enter picks the highlighted item; Esc closes. With `multiple`, Backspace in an empty input
115
131
  * removes the last selected item, and Left at the start of the text moves into the chips.
116
132
  *
133
+ * With the Root's `required`, the input is `required` while nothing is selected, so the
134
+ * browser's validation reports it, `:invalid` matches it, and `form.checkValidity()` fails.
135
+ * Text typed but not picked is still nothing selected: the input reports `valueMissing`'s
136
+ * message then, through `setCustomValidity`.
137
+ *
117
138
  * Leaving the input closes the list and puts the text back: the selected label, or empty
118
139
  * with `multiple`.
119
140
  */
120
141
  declare function ComboboxInput({ className, render, ...rest }: ComboboxInputProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
121
142
 
143
+ type ComboboxInputGroupOwnProps = {
144
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
145
+ render?: ReactElement;
146
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
147
+ ref?: Ref<HTMLElement>;
148
+ };
149
+ type ComboboxInputGroupProps = ComboboxInputGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxInputGroupOwnProps>;
150
+ /**
151
+ * The field around the Input — chips, Clear and Trigger with it — when they are drawn as one
152
+ * box. It takes over as the popup's anchor, so the list lines up with the whole field rather
153
+ * than the text alone, and a press on it is not a press outside.
154
+ *
155
+ * A press on the box itself, between its children, focuses the input.
156
+ */
157
+ declare function ComboboxInputGroup({ className, children, render, ...rest }: ComboboxInputGroupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
158
+
122
159
  type ComboboxTriggerOwnProps = {
123
160
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
124
161
  render?: ReactElement;
@@ -178,7 +215,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
178
215
  type ComboboxListOwnProps<T> = {
179
216
  /**
180
217
  * A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
181
- * virtualizer, render only the rows it asks for.
218
+ * virtualizer, render only the rows it asks for. With groups, it is called once per group
219
+ * the filter left an item in, with that group, and renders a `Combobox.Group`.
182
220
  */
183
221
  children?: ReactNode | ((item: T, index: number) => ReactNode);
184
222
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -188,7 +226,8 @@ type ComboboxListOwnProps<T> = {
188
226
  };
189
227
  type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
190
228
  /**
191
- * The `role="listbox"`. Given a function, renders it once per item the filter left.
229
+ * The `role="listbox"`. Given a function, renders it once per item the filter left — or, when
230
+ * the Root's `items` are groups, once per group.
192
231
  *
193
232
  * With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
194
233
  * view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
@@ -198,7 +237,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
198
237
  type ComboboxItemOwnProps<T> = {
199
238
  /** The item this option stands for — one of the Root's `items`. */
200
239
  value: T;
201
- /** Cannot be picked, and the arrow keys skip it. */
240
+ /** Cannot be picked, and the arrow keys skip it. A disabled Group disables it too. */
202
241
  disabled?: boolean;
203
242
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
204
243
  render?: ReactElement;
@@ -213,7 +252,38 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
213
252
  * `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
214
253
  * the arrow keys and the pointer. Only the items whose highlight changes re-render.
215
254
  */
216
- declare function ComboboxItem<T = unknown>({ value, disabled, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
255
+ declare function ComboboxItem<T = unknown>({ value, disabled: disabledProp, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
256
+
257
+ type ComboboxGroupOwnProps = {
258
+ /** Disables every Item inside, as `disabled` on an `<optgroup>` does. */
259
+ disabled?: boolean;
260
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
261
+ render?: ReactElement;
262
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
263
+ ref?: Ref<HTMLElement>;
264
+ };
265
+ type ComboboxGroupProps = ComboboxGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupOwnProps>;
266
+ /**
267
+ * Items under a label, as an `<optgroup>`: `role="group"`, named by its GroupLabel. Render one
268
+ * from the List's function for each group of the Root's `items`, with an Item for each of the
269
+ * group's `items` — the filter has already narrowed them, and left out a group with none.
270
+ *
271
+ * The arrow keys run through every group's items as one list, past the labels.
272
+ */
273
+ declare function ComboboxGroup({ disabled, className, children, render, ...rest }: ComboboxGroupProps): react.JSX.Element;
274
+
275
+ type ComboboxGroupLabelOwnProps = {
276
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
277
+ render?: ReactElement;
278
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
279
+ ref?: Ref<HTMLElement>;
280
+ };
281
+ type ComboboxGroupLabelProps = ComboboxGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupLabelOwnProps>;
282
+ /**
283
+ * Names its Group, as an `<optgroup>`'s `label` does. Shown in the list, but not an option:
284
+ * it cannot be picked, and the arrow keys pass it.
285
+ */
286
+ declare function ComboboxGroupLabel({ className, children, render, ...rest }: ComboboxGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
217
287
 
218
288
  type ComboboxEmptyOwnProps = {
219
289
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -273,8 +343,26 @@ type ComboboxChipRemoveProps = ComboboxChipRemoveOwnProps & Omit<ComponentPropsW
273
343
  */
274
344
  declare function ComboboxChipRemove({ className, children, render, ...rest }: ComboboxChipRemoveProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
275
345
 
346
+ type ComboboxValueProps<T = unknown> = {
347
+ /**
348
+ * Renders the selection: `T | null`, or `T[]` with `multiple`. The Root's `itemToString` is
349
+ * passed along for labels — a chip's text, say.
350
+ */
351
+ children: (value: T, details: {
352
+ itemToString: (item: unknown) => string;
353
+ }) => ReactNode;
354
+ };
355
+ /**
356
+ * The current selection, for whatever shows it outside the input — chips, most often. Renders
357
+ * no element of its own.
358
+ *
359
+ * It exists so that a separate styling package can show the selection without owning a copy
360
+ * of it (decision 9): the value stays the Root's.
361
+ */
362
+ declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
363
+
276
364
  declare namespace index_parts {
277
- export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, ComboboxInput as Input, ComboboxItem as Item, ComboboxList as List, ComboboxPopup as Popup, ComboboxRoot as Root, ComboboxStatus as Status, ComboboxTrigger as Trigger };
365
+ export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, ComboboxGroup as Group, ComboboxGroupLabel as GroupLabel, ComboboxInput as Input, ComboboxInputGroup as InputGroup, ComboboxItem as Item, ComboboxList as List, ComboboxPopup as Popup, ComboboxRoot as Root, ComboboxStatus as Status, ComboboxTrigger as Trigger, ComboboxValue as Value };
278
366
  }
279
367
 
280
368
  /**
@@ -285,4 +373,4 @@ declare namespace index_parts {
285
373
  */
286
374
  declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
287
375
 
288
- export { index_parts as Combobox, type ComboboxAlign, type ComboboxChangeDetails, type ComboboxChangeReason, type ComboboxChipProps, type ComboboxChipRemoveProps, type ComboboxClearProps, type ComboboxEmptyProps, type ComboboxFilter, type ComboboxHighlightDetails, type ComboboxHighlightReason, type ComboboxInputProps, type ComboboxItemProps, type ComboboxListProps, type ComboboxPopupProps, type ComboboxRootProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue, defaultFilter };
376
+ export { index_parts as Combobox, type ComboboxAlign, type ComboboxChangeDetails, type ComboboxChangeReason, type ComboboxChipProps, type ComboboxChipRemoveProps, type ComboboxClearProps, type ComboboxEmptyProps, type ComboboxFilter, type ComboboxGroup$1 as ComboboxGroup, type ComboboxGroupLabelProps, type ComboboxGroupProps, type ComboboxHighlightDetails, type ComboboxHighlightReason, type ComboboxInputGroupProps, type ComboboxInputProps, type ComboboxItemProps, type ComboboxListProps, type ComboboxPopupProps, type ComboboxRootProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue$1 as ComboboxValue, type ComboboxValueProps, defaultFilter };
@@ -21,12 +21,23 @@ type ComboboxState = {
21
21
  };
22
22
 
23
23
  /** The selection: one item or `null`, or an array with `multiple`. */
24
- type ComboboxValue<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
24
+ type ComboboxValue$1<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
25
25
  /** Takes the whole list, so a filter can rank as well as narrow — a fuzzy search, say. */
26
26
  type ComboboxFilter<T> = (items: readonly T[], query: string, itemToString: (item: T) => string) => T[];
27
+ /**
28
+ * Items under a label, as an `<optgroup>`: any object with an `items` array — the rest, a
29
+ * label or an id, is yours to render.
30
+ */
31
+ type ComboboxGroup$1<T> = {
32
+ readonly items: readonly T[];
33
+ };
27
34
  type ComboboxRootProps<T, Multiple extends boolean = false> = {
28
- /** Every item the list can show. With server search, the current results. */
29
- items: readonly T[];
35
+ /**
36
+ * Every item the list can show. With server search, the current results. Or groups of
37
+ * them: each an object with an `items` array, which the filter searches and `List` renders
38
+ * one by one, leaving out the groups with no match.
39
+ */
40
+ items: readonly T[] | readonly ComboboxGroup$1<T>[];
30
41
  /**
31
42
  * An item's label: what the input shows once it is picked, and what the default filter
32
43
  * matches. Defaults to the item itself for strings, else its `label` property.
@@ -51,10 +62,10 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
51
62
  * Never `undefined` once mounted: the mode is latched at mount. Use `null` — or `[]` with
52
63
  * `multiple` — for nothing selected.
53
64
  */
54
- value?: ComboboxValue<T, Multiple>;
65
+ value?: ComboboxValue$1<T, Multiple>;
55
66
  /** Initial selection when uncontrolled. Read once, at mount. */
56
- defaultValue?: ComboboxValue<T, Multiple>;
57
- onValueChange?: (value: ComboboxValue<T, Multiple>, details: ComboboxChangeDetails) => void;
67
+ defaultValue?: ComboboxValue$1<T, Multiple>;
68
+ onValueChange?: (value: ComboboxValue$1<T, Multiple>, details: ComboboxChangeDetails) => void;
58
69
  /** Controlled text of the input. Provide `onInputValueChange` alongside it. */
59
70
  inputValue?: string;
60
71
  /** Initial text when uncontrolled. Defaults to the selected item's label, without `multiple`. */
@@ -78,6 +89,11 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
78
89
  loading?: boolean;
79
90
  /** Disables the input and every button. */
80
91
  disabled?: boolean;
92
+ /**
93
+ * Something must be selected before the form submits. The input is `required` while the
94
+ * selection is empty, so the browser's own validation reports it.
95
+ */
96
+ required?: boolean;
81
97
  /** Submits each selected item's key under this name, from hidden inputs. */
82
98
  name?: string;
83
99
  /** Associates the hidden inputs with a `<form>` by id, when rendered outside it. */
@@ -96,7 +112,7 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
96
112
  *
97
113
  * Focus stays in the input; the highlighted option is its `aria-activedescendant`.
98
114
  */
99
- declare function ComboboxRoot<T, Multiple extends boolean = false>({ items, itemToString: itemToStringProp, itemToKey: itemToKeyProp, filter, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, inputValue: inputValueProp, defaultInputValue, onInputValueChange, open: openProp, defaultOpen, onOpenChange, onItemHighlighted, onLoadMore, loading, disabled, name, form, children, }: ComboboxRootProps<T, Multiple>): react.JSX.Element;
115
+ declare function ComboboxRoot<T, Multiple extends boolean = false>({ items, itemToString: itemToStringProp, itemToKey: itemToKeyProp, filter, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, inputValue: inputValueProp, defaultInputValue, onInputValueChange, open: openProp, defaultOpen, onOpenChange, onItemHighlighted, onLoadMore, loading, disabled, required, name, form, children, }: ComboboxRootProps<T, Multiple>): react.JSX.Element;
100
116
 
101
117
  type ComboboxInputOwnProps = {
102
118
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -107,18 +123,39 @@ type ComboboxInputOwnProps = {
107
123
  type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'input'>, keyof ComboboxInputOwnProps | 'value' | 'defaultValue'>;
108
124
  /**
109
125
  * The text input: `role="combobox"`, and the element focus stays on. Its text is the Root's
110
- * `inputValue`. It is what the popup is anchored to.
126
+ * `inputValue`. It is what the popup is anchored to, unless an InputGroup wraps it.
111
127
  *
112
128
  * Typing opens the list and filters it. Up and Down open it, or move the highlight, wrapping;
113
129
  * Home and End jump to the ends while it is open; Alt+Down opens it without highlighting.
114
130
  * Enter picks the highlighted item; Esc closes. With `multiple`, Backspace in an empty input
115
131
  * removes the last selected item, and Left at the start of the text moves into the chips.
116
132
  *
133
+ * With the Root's `required`, the input is `required` while nothing is selected, so the
134
+ * browser's validation reports it, `:invalid` matches it, and `form.checkValidity()` fails.
135
+ * Text typed but not picked is still nothing selected: the input reports `valueMissing`'s
136
+ * message then, through `setCustomValidity`.
137
+ *
117
138
  * Leaving the input closes the list and puts the text back: the selected label, or empty
118
139
  * with `multiple`.
119
140
  */
120
141
  declare function ComboboxInput({ className, render, ...rest }: ComboboxInputProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
121
142
 
143
+ type ComboboxInputGroupOwnProps = {
144
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
145
+ render?: ReactElement;
146
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
147
+ ref?: Ref<HTMLElement>;
148
+ };
149
+ type ComboboxInputGroupProps = ComboboxInputGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxInputGroupOwnProps>;
150
+ /**
151
+ * The field around the Input — chips, Clear and Trigger with it — when they are drawn as one
152
+ * box. It takes over as the popup's anchor, so the list lines up with the whole field rather
153
+ * than the text alone, and a press on it is not a press outside.
154
+ *
155
+ * A press on the box itself, between its children, focuses the input.
156
+ */
157
+ declare function ComboboxInputGroup({ className, children, render, ...rest }: ComboboxInputGroupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
158
+
122
159
  type ComboboxTriggerOwnProps = {
123
160
  /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
124
161
  render?: ReactElement;
@@ -178,7 +215,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
178
215
  type ComboboxListOwnProps<T> = {
179
216
  /**
180
217
  * A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
181
- * virtualizer, render only the rows it asks for.
218
+ * virtualizer, render only the rows it asks for. With groups, it is called once per group
219
+ * the filter left an item in, with that group, and renders a `Combobox.Group`.
182
220
  */
183
221
  children?: ReactNode | ((item: T, index: number) => ReactNode);
184
222
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -188,7 +226,8 @@ type ComboboxListOwnProps<T> = {
188
226
  };
189
227
  type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
190
228
  /**
191
- * The `role="listbox"`. Given a function, renders it once per item the filter left.
229
+ * The `role="listbox"`. Given a function, renders it once per item the filter left — or, when
230
+ * the Root's `items` are groups, once per group.
192
231
  *
193
232
  * With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
194
233
  * view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
@@ -198,7 +237,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
198
237
  type ComboboxItemOwnProps<T> = {
199
238
  /** The item this option stands for — one of the Root's `items`. */
200
239
  value: T;
201
- /** Cannot be picked, and the arrow keys skip it. */
240
+ /** Cannot be picked, and the arrow keys skip it. A disabled Group disables it too. */
202
241
  disabled?: boolean;
203
242
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
204
243
  render?: ReactElement;
@@ -213,7 +252,38 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
213
252
  * `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
214
253
  * the arrow keys and the pointer. Only the items whose highlight changes re-render.
215
254
  */
216
- declare function ComboboxItem<T = unknown>({ value, disabled, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
255
+ declare function ComboboxItem<T = unknown>({ value, disabled: disabledProp, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
256
+
257
+ type ComboboxGroupOwnProps = {
258
+ /** Disables every Item inside, as `disabled` on an `<optgroup>` does. */
259
+ disabled?: boolean;
260
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
261
+ render?: ReactElement;
262
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
263
+ ref?: Ref<HTMLElement>;
264
+ };
265
+ type ComboboxGroupProps = ComboboxGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupOwnProps>;
266
+ /**
267
+ * Items under a label, as an `<optgroup>`: `role="group"`, named by its GroupLabel. Render one
268
+ * from the List's function for each group of the Root's `items`, with an Item for each of the
269
+ * group's `items` — the filter has already narrowed them, and left out a group with none.
270
+ *
271
+ * The arrow keys run through every group's items as one list, past the labels.
272
+ */
273
+ declare function ComboboxGroup({ disabled, className, children, render, ...rest }: ComboboxGroupProps): react.JSX.Element;
274
+
275
+ type ComboboxGroupLabelOwnProps = {
276
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
277
+ render?: ReactElement;
278
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
279
+ ref?: Ref<HTMLElement>;
280
+ };
281
+ type ComboboxGroupLabelProps = ComboboxGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupLabelOwnProps>;
282
+ /**
283
+ * Names its Group, as an `<optgroup>`'s `label` does. Shown in the list, but not an option:
284
+ * it cannot be picked, and the arrow keys pass it.
285
+ */
286
+ declare function ComboboxGroupLabel({ className, children, render, ...rest }: ComboboxGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
217
287
 
218
288
  type ComboboxEmptyOwnProps = {
219
289
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -273,8 +343,26 @@ type ComboboxChipRemoveProps = ComboboxChipRemoveOwnProps & Omit<ComponentPropsW
273
343
  */
274
344
  declare function ComboboxChipRemove({ className, children, render, ...rest }: ComboboxChipRemoveProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
275
345
 
346
+ type ComboboxValueProps<T = unknown> = {
347
+ /**
348
+ * Renders the selection: `T | null`, or `T[]` with `multiple`. The Root's `itemToString` is
349
+ * passed along for labels — a chip's text, say.
350
+ */
351
+ children: (value: T, details: {
352
+ itemToString: (item: unknown) => string;
353
+ }) => ReactNode;
354
+ };
355
+ /**
356
+ * The current selection, for whatever shows it outside the input — chips, most often. Renders
357
+ * no element of its own.
358
+ *
359
+ * It exists so that a separate styling package can show the selection without owning a copy
360
+ * of it (decision 9): the value stays the Root's.
361
+ */
362
+ declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
363
+
276
364
  declare namespace index_parts {
277
- export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, ComboboxInput as Input, ComboboxItem as Item, ComboboxList as List, ComboboxPopup as Popup, ComboboxRoot as Root, ComboboxStatus as Status, ComboboxTrigger as Trigger };
365
+ export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, ComboboxGroup as Group, ComboboxGroupLabel as GroupLabel, ComboboxInput as Input, ComboboxInputGroup as InputGroup, ComboboxItem as Item, ComboboxList as List, ComboboxPopup as Popup, ComboboxRoot as Root, ComboboxStatus as Status, ComboboxTrigger as Trigger, ComboboxValue as Value };
278
366
  }
279
367
 
280
368
  /**
@@ -285,4 +373,4 @@ declare namespace index_parts {
285
373
  */
286
374
  declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
287
375
 
288
- export { index_parts as Combobox, type ComboboxAlign, type ComboboxChangeDetails, type ComboboxChangeReason, type ComboboxChipProps, type ComboboxChipRemoveProps, type ComboboxClearProps, type ComboboxEmptyProps, type ComboboxFilter, type ComboboxHighlightDetails, type ComboboxHighlightReason, type ComboboxInputProps, type ComboboxItemProps, type ComboboxListProps, type ComboboxPopupProps, type ComboboxRootProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue, defaultFilter };
376
+ export { index_parts as Combobox, type ComboboxAlign, type ComboboxChangeDetails, type ComboboxChangeReason, type ComboboxChipProps, type ComboboxChipRemoveProps, type ComboboxClearProps, type ComboboxEmptyProps, type ComboboxFilter, type ComboboxGroup$1 as ComboboxGroup, type ComboboxGroupLabelProps, type ComboboxGroupProps, type ComboboxHighlightDetails, type ComboboxHighlightReason, type ComboboxInputGroupProps, type ComboboxInputProps, type ComboboxItemProps, type ComboboxListProps, type ComboboxPopupProps, type ComboboxRootProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue$1 as ComboboxValue, type ComboboxValueProps, defaultFilter };
@@ -22,13 +22,17 @@ __export(index_parts_exports, {
22
22
  ChipRemove: () => ComboboxChipRemove,
23
23
  Clear: () => ComboboxClear,
24
24
  Empty: () => ComboboxEmpty,
25
+ Group: () => ComboboxGroup,
26
+ GroupLabel: () => ComboboxGroupLabel,
25
27
  Input: () => ComboboxInput,
28
+ InputGroup: () => ComboboxInputGroup,
26
29
  Item: () => ComboboxItem,
27
30
  List: () => ComboboxList,
28
31
  Popup: () => ComboboxPopup,
29
32
  Root: () => ComboboxRoot,
30
33
  Status: () => ComboboxStatus,
31
- Trigger: () => ComboboxTrigger
34
+ Trigger: () => ComboboxTrigger,
35
+ Value: () => ComboboxValue
32
36
  });
33
37
 
34
38
  // src/combobox/ComboboxRoot.tsx
@@ -73,6 +77,11 @@ function useComboboxRootContext(part) {
73
77
 
74
78
  // src/combobox/ComboboxRoot.tsx
75
79
  import { jsx, jsxs } from "react/jsx-runtime";
80
+ function isGroupList(items) {
81
+ return items.length > 0 && items.every(
82
+ (entry) => entry !== null && typeof entry === "object" && Array.isArray(entry.items)
83
+ );
84
+ }
76
85
  function defaultItemToString(item) {
77
86
  if (typeof item === "string") return item;
78
87
  if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
@@ -97,6 +106,7 @@ function ComboboxRoot({
97
106
  onLoadMore,
98
107
  loading = false,
99
108
  disabled = false,
109
+ required = false,
100
110
  name,
101
111
  form,
102
112
  children
@@ -133,10 +143,17 @@ function ComboboxRoot({
133
143
  });
134
144
  const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
135
145
  const deferredQuery = useDeferredValue(query);
136
- const filteredItems = useMemo(() => {
137
- if (filter === null) return items;
138
- const run = filter ?? defaultFilter;
139
- return run(items, deferredQuery, itemToString);
146
+ const { filteredItems, filteredGroups } = useMemo(() => {
147
+ const run = filter === null ? (list) => list : (list) => (filter ?? defaultFilter)(
148
+ list,
149
+ deferredQuery,
150
+ itemToString
151
+ );
152
+ if (!isGroupList(items)) {
153
+ return { filteredItems: run(items), filteredGroups: null };
154
+ }
155
+ const groups = items.map((group) => ({ ...group, items: run(group.items) })).filter((group) => group.items.length > 0);
156
+ return { filteredItems: groups.flatMap((group) => group.items), filteredGroups: groups };
140
157
  }, [items, filter, deferredQuery, itemToString]);
141
158
  const indexByKey = useMemo(
142
159
  () => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
@@ -248,6 +265,11 @@ function ComboboxRoot({
248
265
  },
249
266
  [insideRefs]
250
267
  );
268
+ const [hasGroup, setHasGroup] = useState(false);
269
+ const registerGroup = useCallback(() => {
270
+ setHasGroup(true);
271
+ return () => setHasGroup(false);
272
+ }, []);
251
273
  const inputRef = useRef(null);
252
274
  const popupRef = useRef(null);
253
275
  const { current: initial } = useRef({ value, inputValue });
@@ -276,9 +298,11 @@ function ComboboxRoot({
276
298
  open,
277
299
  multiple,
278
300
  disabled,
301
+ required,
279
302
  inputValue,
280
303
  loading,
281
304
  filteredItems,
305
+ filteredGroups,
282
306
  itemToString,
283
307
  itemToKey,
284
308
  indexByKey,
@@ -298,6 +322,8 @@ function ComboboxRoot({
298
322
  chips,
299
323
  registerInside,
300
324
  isInside,
325
+ hasGroup,
326
+ registerGroup,
301
327
  onLoadMore,
302
328
  inputId: `${id}-input`,
303
329
  listId: `${id}-list`,
@@ -310,9 +336,11 @@ function ComboboxRoot({
310
336
  open,
311
337
  multiple,
312
338
  disabled,
339
+ required,
313
340
  inputValue,
314
341
  loading,
315
342
  filteredItems,
343
+ filteredGroups,
316
344
  itemToString,
317
345
  itemToKey,
318
346
  indexByKey,
@@ -332,6 +360,8 @@ function ComboboxRoot({
332
360
  chips,
333
361
  registerInside,
334
362
  isInside,
363
+ hasGroup,
364
+ registerGroup,
335
365
  onLoadMore,
336
366
  id
337
367
  ]
@@ -353,7 +383,7 @@ function ComboboxRoot({
353
383
  }
354
384
 
355
385
  // src/combobox/ComboboxInput.tsx
356
- import { useSyncExternalStore } from "react";
386
+ import { useLayoutEffect, useRef as useRef2, useSyncExternalStore } from "react";
357
387
 
358
388
  // src/combobox/comboboxDataAttributes.ts
359
389
  function comboboxDataAttributes({ open, disabled }) {
@@ -384,6 +414,7 @@ function ComboboxInput({ className, render, ...rest }) {
384
414
  open,
385
415
  multiple,
386
416
  disabled,
417
+ required,
387
418
  inputValue,
388
419
  filteredItems,
389
420
  itemToKey,
@@ -403,6 +434,7 @@ function ComboboxInput({ className, render, ...rest }) {
403
434
  listId,
404
435
  optionId,
405
436
  anchorName,
437
+ hasGroup,
406
438
  inputRef,
407
439
  popupRef
408
440
  } = useComboboxRootContext("Input");
@@ -411,6 +443,20 @@ function ComboboxInput({ className, render, ...rest }) {
411
443
  () => highlight.get().index,
412
444
  () => -1
413
445
  );
446
+ const missing = required && selectedItems.length === 0;
447
+ const textOnly = missing && inputValue !== "";
448
+ const customValidityRef = useRef2(false);
449
+ useLayoutEffect(() => {
450
+ const input = inputRef.current;
451
+ if (!input || typeof input.setCustomValidity !== "function") return;
452
+ if (textOnly) {
453
+ input.setCustomValidity(valueMissingMessage());
454
+ customValidityRef.current = true;
455
+ } else if (customValidityRef.current) {
456
+ input.setCustomValidity("");
457
+ customValidityRef.current = false;
458
+ }
459
+ }, [textOnly, inputRef]);
414
460
  function enabledIndices() {
415
461
  const indices = [];
416
462
  filteredItems.forEach((item, index) => {
@@ -448,9 +494,13 @@ function ComboboxInput({ className, render, ...rest }) {
448
494
  "aria-controls": listId,
449
495
  "aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
450
496
  disabled: disabled || void 0,
497
+ required: missing || void 0,
498
+ // Stays when a selection lifts `required`: the field is still required.
499
+ "aria-required": required || void 0,
451
500
  value: inputValue,
452
501
  ...comboboxDataAttributes({ open, multiple, disabled }),
453
- style: { anchorName },
502
+ // The anchor, unless an InputGroup around it is.
503
+ style: hasGroup ? void 0 : { anchorName },
454
504
  className,
455
505
  ref: inputRef,
456
506
  onChange(event) {
@@ -516,13 +566,49 @@ function ComboboxInput({ className, render, ...rest }) {
516
566
  consumerProps: rest
517
567
  });
518
568
  }
569
+ function valueMissingMessage() {
570
+ const probe = document.createElement("input");
571
+ probe.required = true;
572
+ return probe.validationMessage || "Please select an item in the list.";
573
+ }
574
+
575
+ // src/combobox/ComboboxInputGroup.tsx
576
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
577
+ function ComboboxInputGroup({
578
+ className,
579
+ children,
580
+ render,
581
+ ...rest
582
+ }) {
583
+ const { open, multiple, disabled, anchorName, registerGroup, registerInside, inputRef } = useComboboxRootContext("InputGroup");
584
+ const elementRef = useRef3(null);
585
+ useLayoutEffect2(() => registerGroup(), [registerGroup]);
586
+ useLayoutEffect2(() => registerInside(elementRef), [registerInside]);
587
+ return useRender({
588
+ render,
589
+ defaultTagName: "div",
590
+ props: {
591
+ ...comboboxDataAttributes({ open, multiple, disabled }),
592
+ style: { anchorName },
593
+ className,
594
+ children,
595
+ ref: elementRef,
596
+ onMouseDown(event) {
597
+ if (event.target !== event.currentTarget || disabled) return;
598
+ event.preventDefault();
599
+ inputRef.current?.focus();
600
+ }
601
+ },
602
+ consumerProps: rest
603
+ });
604
+ }
519
605
 
520
606
  // src/combobox/ComboboxTrigger.tsx
521
- import { useLayoutEffect, useRef as useRef2 } from "react";
607
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef4 } from "react";
522
608
  function ComboboxTrigger({ className, children, render, ...rest }) {
523
609
  const { open, multiple, disabled, setOpen, close, listId, inputRef, registerInside } = useComboboxRootContext("Trigger");
524
- const elementRef = useRef2(null);
525
- useLayoutEffect(() => registerInside(elementRef), [registerInside]);
610
+ const elementRef = useRef4(null);
611
+ useLayoutEffect3(() => registerInside(elementRef), [registerInside]);
526
612
  return useRender({
527
613
  render,
528
614
  defaultTagName: "button",
@@ -550,11 +636,11 @@ function ComboboxTrigger({ className, children, render, ...rest }) {
550
636
  }
551
637
 
552
638
  // src/combobox/ComboboxClear.tsx
553
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
639
+ import { useLayoutEffect as useLayoutEffect4, useRef as useRef5 } from "react";
554
640
  function ComboboxClear({ className, children, render, ...rest }) {
555
641
  const { disabled, selectedItems, clear, inputRef, registerInside } = useComboboxRootContext("Clear");
556
- const elementRef = useRef3(null);
557
- useLayoutEffect2(() => registerInside(elementRef), [registerInside]);
642
+ const elementRef = useRef5(null);
643
+ useLayoutEffect4(() => registerInside(elementRef), [registerInside]);
558
644
  return useRender({
559
645
  render,
560
646
  defaultTagName: "button",
@@ -637,7 +723,7 @@ function ComboboxPopup({
637
723
  }
638
724
 
639
725
  // src/combobox/ComboboxList.tsx
640
- import { useEffect as useEffect3, useRef as useRef4 } from "react";
726
+ import { useEffect as useEffect3, useRef as useRef6 } from "react";
641
727
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
642
728
  function ComboboxList({
643
729
  className,
@@ -645,8 +731,10 @@ function ComboboxList({
645
731
  render,
646
732
  ...rest
647
733
  }) {
648
- const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
649
- const content = typeof children === "function" ? filteredItems.map((item, index) => children(item, index)) : children;
734
+ const { multiple, loading, filteredItems, filteredGroups, onLoadMore, listId } = useComboboxRootContext("List");
735
+ const content = typeof children === "function" ? (filteredGroups ?? filteredItems).map(
736
+ (entry, index) => children(entry, index)
737
+ ) : children;
650
738
  return useRender({
651
739
  render,
652
740
  defaultTagName: "div",
@@ -666,8 +754,8 @@ function ComboboxList({
666
754
  }
667
755
  function LoadMoreSentinel() {
668
756
  const { open, loading, filteredItems, onLoadMore } = useComboboxRootContext("List");
669
- const ref = useRef4(null);
670
- const onLoadMoreRef = useRef4(onLoadMore);
757
+ const ref = useRef6(null);
758
+ const onLoadMoreRef = useRef6(onLoadMore);
671
759
  onLoadMoreRef.current = onLoadMore;
672
760
  useEffect3(() => {
673
761
  const element = ref.current;
@@ -682,10 +770,52 @@ function LoadMoreSentinel() {
682
770
  }
683
771
 
684
772
  // src/combobox/ComboboxItem.tsx
685
- import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore2 } from "react";
773
+ import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect5, useRef as useRef7, useSyncExternalStore as useSyncExternalStore2 } from "react";
774
+
775
+ // src/combobox/ComboboxGroup.tsx
776
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useId as useId2, useMemo as useMemo2, useState as useState2 } from "react";
777
+ import { jsx as jsx3 } from "react/jsx-runtime";
778
+ var ComboboxGroupContext = createContext2(null);
779
+ function useComboboxGroupContext() {
780
+ return useContext2(ComboboxGroupContext);
781
+ }
782
+ function ComboboxGroup({
783
+ disabled = false,
784
+ className,
785
+ children,
786
+ render,
787
+ ...rest
788
+ }) {
789
+ useComboboxRootContext("Group");
790
+ const labelId = `${useId2()}-label`;
791
+ const [hasLabel, setHasLabel] = useState2(false);
792
+ const registerLabel = useCallback2(() => {
793
+ setHasLabel(true);
794
+ return () => setHasLabel(false);
795
+ }, []);
796
+ const context = useMemo2(
797
+ () => ({ disabled, labelId, registerLabel }),
798
+ [disabled, labelId, registerLabel]
799
+ );
800
+ const element = useRender({
801
+ render,
802
+ defaultTagName: "div",
803
+ props: {
804
+ role: "group",
805
+ "aria-labelledby": hasLabel ? labelId : void 0,
806
+ "data-disabled": disabled ? "" : void 0,
807
+ className,
808
+ children
809
+ },
810
+ consumerProps: rest
811
+ });
812
+ return /* @__PURE__ */ jsx3(ComboboxGroupContext.Provider, { value: context, children: element });
813
+ }
814
+
815
+ // src/combobox/ComboboxItem.tsx
686
816
  function ComboboxItem({
687
817
  value,
688
- disabled = false,
818
+ disabled: disabledProp = false,
689
819
  className,
690
820
  children,
691
821
  render,
@@ -701,6 +831,8 @@ function ComboboxItem({
701
831
  select,
702
832
  optionId
703
833
  } = useComboboxRootContext("Item");
834
+ const group = useComboboxGroupContext();
835
+ const disabled = disabledProp || (group?.disabled ?? false);
704
836
  const key = itemToKey(value);
705
837
  const index = indexByKey.get(key) ?? -1;
706
838
  const selected = selectedKeys.has(key);
@@ -709,8 +841,8 @@ function ComboboxItem({
709
841
  () => index >= 0 && highlight.get().index === index,
710
842
  () => false
711
843
  );
712
- const elementRef = useRef5(null);
713
- useLayoutEffect3(() => {
844
+ const elementRef = useRef7(null);
845
+ useLayoutEffect5(() => {
714
846
  if (!disabled) return;
715
847
  disabledKeys.add(key);
716
848
  return () => {
@@ -748,6 +880,28 @@ function ComboboxItem({
748
880
  });
749
881
  }
750
882
 
883
+ // src/combobox/ComboboxGroupLabel.tsx
884
+ import { useLayoutEffect as useLayoutEffect6 } from "react";
885
+ function ComboboxGroupLabel({
886
+ className,
887
+ children,
888
+ render,
889
+ ...rest
890
+ }) {
891
+ const group = useComboboxGroupContext();
892
+ if (group === null) {
893
+ throw new Error("<Combobox.GroupLabel> must be rendered inside <Combobox.Group>.");
894
+ }
895
+ const { labelId, registerLabel } = group;
896
+ useLayoutEffect6(registerLabel, [registerLabel]);
897
+ return useRender({
898
+ render,
899
+ defaultTagName: "div",
900
+ props: { id: labelId, className, children },
901
+ consumerProps: rest
902
+ });
903
+ }
904
+
751
905
  // src/combobox/ComboboxEmpty.tsx
752
906
  function ComboboxEmpty({ className, children, render, ...rest }) {
753
907
  const { filteredItems, loading } = useComboboxRootContext("Empty");
@@ -772,11 +926,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
772
926
  }
773
927
 
774
928
  // src/combobox/ComboboxChip.tsx
775
- import { createContext as createContext2, useContext as useContext2, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef6 } from "react";
776
- import { jsx as jsx3 } from "react/jsx-runtime";
777
- var ComboboxChipContext = createContext2(null);
929
+ import { createContext as createContext3, useContext as useContext3, useLayoutEffect as useLayoutEffect7, useMemo as useMemo3, useRef as useRef8 } from "react";
930
+ import { jsx as jsx4 } from "react/jsx-runtime";
931
+ var ComboboxChipContext = createContext3(null);
778
932
  function useComboboxChipContext() {
779
- const context = useContext2(ComboboxChipContext);
933
+ const context = useContext3(ComboboxChipContext);
780
934
  if (context === null) {
781
935
  throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
782
936
  }
@@ -791,9 +945,9 @@ function ComboboxChip({
791
945
  }) {
792
946
  const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
793
947
  const key = itemToKey(value);
794
- const elementRef = useRef6(null);
795
- useLayoutEffect4(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
796
- const chipContext = useMemo2(() => ({ value }), [value]);
948
+ const elementRef = useRef8(null);
949
+ useLayoutEffect7(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
950
+ const chipContext = useMemo3(() => ({ value }), [value]);
797
951
  function focusAt(index) {
798
952
  const chip = chips[index];
799
953
  if (chip) chip.ref.current?.focus();
@@ -834,7 +988,7 @@ function ComboboxChip({
834
988
  },
835
989
  consumerProps: rest
836
990
  });
837
- return /* @__PURE__ */ jsx3(ComboboxChipContext.Provider, { value: chipContext, children: element });
991
+ return /* @__PURE__ */ jsx4(ComboboxChipContext.Provider, { value: chipContext, children: element });
838
992
  }
839
993
 
840
994
  // src/combobox/ComboboxChipRemove.tsx
@@ -867,6 +1021,14 @@ function ComboboxChipRemove({
867
1021
  consumerProps: rest
868
1022
  });
869
1023
  }
1024
+
1025
+ // src/combobox/ComboboxValue.tsx
1026
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
1027
+ function ComboboxValue({ children }) {
1028
+ const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
1029
+ const value = multiple ? selectedItems : selectedItems[0] ?? null;
1030
+ return /* @__PURE__ */ jsx5(Fragment2, { children: children(value, { itemToString }) });
1031
+ }
870
1032
  export {
871
1033
  index_parts_exports as Combobox,
872
1034
  defaultFilter
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.8.0",
3
+ "version": "4.10.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",