@arun-dev/headless 4.9.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,12 +32,15 @@ __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,
36
38
  InputGroup: () => ComboboxInputGroup,
37
39
  Item: () => ComboboxItem,
38
40
  List: () => ComboboxList,
39
41
  Popup: () => ComboboxPopup,
40
42
  Root: () => ComboboxRoot,
43
+ Separator: () => ComboboxSeparator,
41
44
  Status: () => ComboboxStatus,
42
45
  Trigger: () => ComboboxTrigger,
43
46
  Value: () => ComboboxValue
@@ -169,6 +172,11 @@ function useComboboxRootContext(part) {
169
172
 
170
173
  // src/combobox/ComboboxRoot.tsx
171
174
  var import_jsx_runtime = require("react/jsx-runtime");
175
+ function isGroupList(items) {
176
+ return items.length > 0 && items.every(
177
+ (entry) => entry !== null && typeof entry === "object" && Array.isArray(entry.items)
178
+ );
179
+ }
172
180
  function defaultItemToString(item) {
173
181
  if (typeof item === "string") return item;
174
182
  if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
@@ -193,6 +201,7 @@ function ComboboxRoot({
193
201
  onLoadMore,
194
202
  loading = false,
195
203
  disabled = false,
204
+ required = false,
196
205
  name,
197
206
  form,
198
207
  children
@@ -229,10 +238,17 @@ function ComboboxRoot({
229
238
  });
230
239
  const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
231
240
  const deferredQuery = (0, import_react3.useDeferredValue)(query);
232
- const filteredItems = (0, import_react3.useMemo)(() => {
233
- if (filter === null) return items;
234
- const run = filter ?? defaultFilter;
235
- return run(items, deferredQuery, itemToString);
241
+ const { filteredItems, filteredGroups } = (0, import_react3.useMemo)(() => {
242
+ const run = filter === null ? (list) => list : (list) => (filter ?? defaultFilter)(
243
+ list,
244
+ deferredQuery,
245
+ itemToString
246
+ );
247
+ if (!isGroupList(items)) {
248
+ return { filteredItems: run(items), filteredGroups: null };
249
+ }
250
+ const groups = items.map((group) => ({ ...group, items: run(group.items) })).filter((group) => group.items.length > 0);
251
+ return { filteredItems: groups.flatMap((group) => group.items), filteredGroups: groups };
236
252
  }, [items, filter, deferredQuery, itemToString]);
237
253
  const indexByKey = (0, import_react3.useMemo)(
238
254
  () => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
@@ -326,7 +342,7 @@ function ComboboxRoot({
326
342
  (0, import_react3.useEffect)(() => {
327
343
  if (lastSelectedKeyRef.current === selectedKey) return;
328
344
  lastSelectedKeyRef.current = selectedKey;
329
- setInputValueState(selectedLabel);
345
+ setInputValue(selectedLabel, "value-change");
330
346
  }, [selectedKey]);
331
347
  const [chips, setChips] = (0, import_react3.useState)([]);
332
348
  const registerChip = (0, import_react3.useCallback)((entry) => {
@@ -377,9 +393,11 @@ function ComboboxRoot({
377
393
  open,
378
394
  multiple,
379
395
  disabled,
396
+ required,
380
397
  inputValue,
381
398
  loading,
382
399
  filteredItems,
400
+ filteredGroups,
383
401
  itemToString,
384
402
  itemToKey,
385
403
  indexByKey,
@@ -413,9 +431,11 @@ function ComboboxRoot({
413
431
  open,
414
432
  multiple,
415
433
  disabled,
434
+ required,
416
435
  inputValue,
417
436
  loading,
418
437
  filteredItems,
438
+ filteredGroups,
419
439
  itemToString,
420
440
  itemToKey,
421
441
  indexByKey,
@@ -578,6 +598,7 @@ function ComboboxInput({ className, render, ...rest }) {
578
598
  open,
579
599
  multiple,
580
600
  disabled,
601
+ required,
581
602
  inputValue,
582
603
  filteredItems,
583
604
  itemToKey,
@@ -606,6 +627,20 @@ function ComboboxInput({ className, render, ...rest }) {
606
627
  () => highlight.get().index,
607
628
  () => -1
608
629
  );
630
+ const missing = required && selectedItems.length === 0;
631
+ const textOnly = missing && inputValue !== "";
632
+ const customValidityRef = (0, import_react5.useRef)(false);
633
+ (0, import_react5.useLayoutEffect)(() => {
634
+ const input = inputRef.current;
635
+ if (!input || typeof input.setCustomValidity !== "function") return;
636
+ if (textOnly) {
637
+ input.setCustomValidity(valueMissingMessage());
638
+ customValidityRef.current = true;
639
+ } else if (customValidityRef.current) {
640
+ input.setCustomValidity("");
641
+ customValidityRef.current = false;
642
+ }
643
+ }, [textOnly, inputRef]);
609
644
  function enabledIndices() {
610
645
  const indices = [];
611
646
  filteredItems.forEach((item, index) => {
@@ -643,6 +678,9 @@ function ComboboxInput({ className, render, ...rest }) {
643
678
  "aria-controls": listId,
644
679
  "aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
645
680
  disabled: disabled || void 0,
681
+ required: missing || void 0,
682
+ // Stays when a selection lifts `required`: the field is still required.
683
+ "aria-required": required || void 0,
646
684
  value: inputValue,
647
685
  ...comboboxDataAttributes({ open, multiple, disabled }),
648
686
  // The anchor, unless an InputGroup around it is.
@@ -712,6 +750,11 @@ function ComboboxInput({ className, render, ...rest }) {
712
750
  consumerProps: rest
713
751
  });
714
752
  }
753
+ function valueMissingMessage() {
754
+ const probe = document.createElement("input");
755
+ probe.required = true;
756
+ return probe.validationMessage || "Please select an item in the list.";
757
+ }
715
758
 
716
759
  // src/combobox/ComboboxInputGroup.tsx
717
760
  var import_react6 = require("react");
@@ -789,7 +832,8 @@ function ComboboxClear({ className, children, render, ...rest }) {
789
832
  type: "button",
790
833
  tabIndex: -1,
791
834
  disabled: disabled || void 0,
792
- ...comboboxClearDataAttributes(selectedItems.length > 0),
835
+ // Nothing to clear while disabled: a disabled field hides it, as it has no effect.
836
+ ...comboboxClearDataAttributes(!disabled && selectedItems.length > 0),
793
837
  className,
794
838
  children,
795
839
  ref: elementRef,
@@ -872,8 +916,10 @@ function ComboboxList({
872
916
  render,
873
917
  ...rest
874
918
  }) {
875
- const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
876
- const content = typeof children === "function" ? filteredItems.map((item, index) => children(item, index)) : children;
919
+ const { multiple, loading, filteredItems, filteredGroups, onLoadMore, listId } = useComboboxRootContext("List");
920
+ const content = typeof children === "function" ? (filteredGroups ?? filteredItems).map(
921
+ (entry, index) => children(entry, index)
922
+ ) : children;
877
923
  return useRender({
878
924
  render,
879
925
  defaultTagName: "div",
@@ -909,10 +955,52 @@ function LoadMoreSentinel() {
909
955
  }
910
956
 
911
957
  // src/combobox/ComboboxItem.tsx
958
+ var import_react12 = require("react");
959
+
960
+ // src/combobox/ComboboxGroup.tsx
912
961
  var import_react11 = require("react");
962
+ var import_jsx_runtime3 = require("react/jsx-runtime");
963
+ var ComboboxGroupContext = (0, import_react11.createContext)(null);
964
+ function useComboboxGroupContext() {
965
+ return (0, import_react11.useContext)(ComboboxGroupContext);
966
+ }
967
+ function ComboboxGroup({
968
+ disabled = false,
969
+ className,
970
+ children,
971
+ render,
972
+ ...rest
973
+ }) {
974
+ useComboboxRootContext("Group");
975
+ const labelId = `${(0, import_react11.useId)()}-label`;
976
+ const [hasLabel, setHasLabel] = (0, import_react11.useState)(false);
977
+ const registerLabel = (0, import_react11.useCallback)(() => {
978
+ setHasLabel(true);
979
+ return () => setHasLabel(false);
980
+ }, []);
981
+ const context = (0, import_react11.useMemo)(
982
+ () => ({ disabled, labelId, registerLabel }),
983
+ [disabled, labelId, registerLabel]
984
+ );
985
+ const element = useRender({
986
+ render,
987
+ defaultTagName: "div",
988
+ props: {
989
+ role: "group",
990
+ "aria-labelledby": hasLabel ? labelId : void 0,
991
+ "data-disabled": disabled ? "" : void 0,
992
+ className,
993
+ children
994
+ },
995
+ consumerProps: rest
996
+ });
997
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComboboxGroupContext.Provider, { value: context, children: element });
998
+ }
999
+
1000
+ // src/combobox/ComboboxItem.tsx
913
1001
  function ComboboxItem({
914
1002
  value,
915
- disabled = false,
1003
+ disabled: disabledProp = false,
916
1004
  className,
917
1005
  children,
918
1006
  render,
@@ -928,23 +1016,25 @@ function ComboboxItem({
928
1016
  select,
929
1017
  optionId
930
1018
  } = useComboboxRootContext("Item");
1019
+ const group = useComboboxGroupContext();
1020
+ const disabled = disabledProp || (group?.disabled ?? false);
931
1021
  const key = itemToKey(value);
932
1022
  const index = indexByKey.get(key) ?? -1;
933
1023
  const selected = selectedKeys.has(key);
934
- const highlighted = (0, import_react11.useSyncExternalStore)(
1024
+ const highlighted = (0, import_react12.useSyncExternalStore)(
935
1025
  highlight.subscribe,
936
1026
  () => index >= 0 && highlight.get().index === index,
937
1027
  () => false
938
1028
  );
939
- const elementRef = (0, import_react11.useRef)(null);
940
- (0, import_react11.useLayoutEffect)(() => {
1029
+ const elementRef = (0, import_react12.useRef)(null);
1030
+ (0, import_react12.useLayoutEffect)(() => {
941
1031
  if (!disabled) return;
942
1032
  disabledKeys.add(key);
943
1033
  return () => {
944
1034
  disabledKeys.delete(key);
945
1035
  };
946
1036
  }, [disabled, disabledKeys, key]);
947
- (0, import_react11.useEffect)(() => {
1037
+ (0, import_react12.useEffect)(() => {
948
1038
  if (highlighted && highlight.get().reason === "keyboard") {
949
1039
  elementRef.current?.scrollIntoView?.({ block: "nearest" });
950
1040
  }
@@ -975,6 +1065,39 @@ function ComboboxItem({
975
1065
  });
976
1066
  }
977
1067
 
1068
+ // src/combobox/ComboboxGroupLabel.tsx
1069
+ var import_react13 = require("react");
1070
+ function ComboboxGroupLabel({
1071
+ className,
1072
+ children,
1073
+ render,
1074
+ ...rest
1075
+ }) {
1076
+ const group = useComboboxGroupContext();
1077
+ if (group === null) {
1078
+ throw new Error("<Combobox.GroupLabel> must be rendered inside <Combobox.Group>.");
1079
+ }
1080
+ const { labelId, registerLabel } = group;
1081
+ (0, import_react13.useLayoutEffect)(registerLabel, [registerLabel]);
1082
+ return useRender({
1083
+ render,
1084
+ defaultTagName: "div",
1085
+ props: { id: labelId, className, children },
1086
+ consumerProps: rest
1087
+ });
1088
+ }
1089
+
1090
+ // src/combobox/ComboboxSeparator.tsx
1091
+ function ComboboxSeparator({ className, render, ...rest }) {
1092
+ useComboboxRootContext("Separator");
1093
+ return useRender({
1094
+ render,
1095
+ defaultTagName: "div",
1096
+ props: { "aria-hidden": true, className },
1097
+ consumerProps: rest
1098
+ });
1099
+ }
1100
+
978
1101
  // src/combobox/ComboboxEmpty.tsx
979
1102
  function ComboboxEmpty({ className, children, render, ...rest }) {
980
1103
  const { filteredItems, loading } = useComboboxRootContext("Empty");
@@ -999,11 +1122,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
999
1122
  }
1000
1123
 
1001
1124
  // src/combobox/ComboboxChip.tsx
1002
- var import_react12 = require("react");
1003
- var import_jsx_runtime3 = require("react/jsx-runtime");
1004
- var ComboboxChipContext = (0, import_react12.createContext)(null);
1125
+ var import_react14 = require("react");
1126
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1127
+ var ComboboxChipContext = (0, import_react14.createContext)(null);
1005
1128
  function useComboboxChipContext() {
1006
- const context = (0, import_react12.useContext)(ComboboxChipContext);
1129
+ const context = (0, import_react14.useContext)(ComboboxChipContext);
1007
1130
  if (context === null) {
1008
1131
  throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
1009
1132
  }
@@ -1018,9 +1141,9 @@ function ComboboxChip({
1018
1141
  }) {
1019
1142
  const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
1020
1143
  const key = itemToKey(value);
1021
- const elementRef = (0, import_react12.useRef)(null);
1022
- (0, import_react12.useLayoutEffect)(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
1023
- const chipContext = (0, import_react12.useMemo)(() => ({ value }), [value]);
1144
+ const elementRef = (0, import_react14.useRef)(null);
1145
+ (0, import_react14.useLayoutEffect)(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
1146
+ const chipContext = (0, import_react14.useMemo)(() => ({ value }), [value]);
1024
1147
  function focusAt(index) {
1025
1148
  const chip = chips[index];
1026
1149
  if (chip) chip.ref.current?.focus();
@@ -1061,7 +1184,7 @@ function ComboboxChip({
1061
1184
  },
1062
1185
  consumerProps: rest
1063
1186
  });
1064
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComboboxChipContext.Provider, { value: chipContext, children: element });
1187
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ComboboxChipContext.Provider, { value: chipContext, children: element });
1065
1188
  }
1066
1189
 
1067
1190
  // src/combobox/ComboboxChipRemove.tsx
@@ -1096,11 +1219,11 @@ function ComboboxChipRemove({
1096
1219
  }
1097
1220
 
1098
1221
  // src/combobox/ComboboxValue.tsx
1099
- var import_jsx_runtime4 = require("react/jsx-runtime");
1222
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1100
1223
  function ComboboxValue({ children }) {
1101
1224
  const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
1102
1225
  const value = multiple ? selectedItems : selectedItems[0] ?? null;
1103
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: children(value, { itemToString }) });
1226
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children(value, { itemToString }) });
1104
1227
  }
1105
1228
  // Annotate the CommonJS export names for ESM import in node:
1106
1229
  0 && (module.exports = {
@@ -3,7 +3,9 @@ import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
  import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.cjs';
4
4
 
5
5
  /** Why a value, the input's text or the open state changed — the second callback argument. */
6
- type ComboboxChangeReason = 'input' | 'input-press' | 'trigger-press' | 'item-press' | 'keyboard' | 'clear' | 'chip-remove' | 'escape' | 'outside-press' | 'blur' | 'form-reset';
6
+ type ComboboxChangeReason = 'input' | 'input-press' | 'trigger-press' | 'item-press' | 'keyboard' | 'clear' | 'chip-remove' | 'escape' | 'outside-press' | 'blur' | 'form-reset'
7
+ /** The input's text followed a change of `value` — the parent's, not the user's. */
8
+ | 'value-change';
7
9
  type ComboboxChangeDetails = {
8
10
  reason: ComboboxChangeReason;
9
11
  };
@@ -24,9 +26,20 @@ type ComboboxState = {
24
26
  type ComboboxValue$1<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
25
27
  /** Takes the whole list, so a filter can rank as well as narrow — a fuzzy search, say. */
26
28
  type ComboboxFilter<T> = (items: readonly T[], query: string, itemToString: (item: T) => string) => T[];
29
+ /**
30
+ * Items under a label, as an `<optgroup>`: any object with an `items` array — the rest, a
31
+ * label or an id, is yours to render.
32
+ */
33
+ type ComboboxGroup$1<T> = {
34
+ readonly items: readonly T[];
35
+ };
27
36
  type ComboboxRootProps<T, Multiple extends boolean = false> = {
28
- /** Every item the list can show. With server search, the current results. */
29
- items: readonly T[];
37
+ /**
38
+ * Every item the list can show. With server search, the current results. Or groups of
39
+ * them: each an object with an `items` array, which the filter searches and `List` renders
40
+ * one by one, leaving out the groups with no match.
41
+ */
42
+ items: readonly T[] | readonly ComboboxGroup$1<T>[];
30
43
  /**
31
44
  * An item's label: what the input shows once it is picked, and what the default filter
32
45
  * matches. Defaults to the item itself for strings, else its `label` property.
@@ -78,6 +91,11 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
78
91
  loading?: boolean;
79
92
  /** Disables the input and every button. */
80
93
  disabled?: boolean;
94
+ /**
95
+ * Something must be selected before the form submits. The input is `required` while the
96
+ * selection is empty, so the browser's own validation reports it.
97
+ */
98
+ required?: boolean;
81
99
  /** Submits each selected item's key under this name, from hidden inputs. */
82
100
  name?: string;
83
101
  /** Associates the hidden inputs with a `<form>` by id, when rendered outside it. */
@@ -96,7 +114,7 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
96
114
  *
97
115
  * Focus stays in the input; the highlighted option is its `aria-activedescendant`.
98
116
  */
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;
117
+ 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
118
 
101
119
  type ComboboxInputOwnProps = {
102
120
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -114,6 +132,11 @@ type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'in
114
132
  * Enter picks the highlighted item; Esc closes. With `multiple`, Backspace in an empty input
115
133
  * removes the last selected item, and Left at the start of the text moves into the chips.
116
134
  *
135
+ * With the Root's `required`, the input is `required` while nothing is selected, so the
136
+ * browser's validation reports it, `:invalid` matches it, and `form.checkValidity()` fails.
137
+ * Text typed but not picked is still nothing selected: the input reports `valueMissing`'s
138
+ * message then, through `setCustomValidity`.
139
+ *
117
140
  * Leaving the input closes the list and puts the text back: the selected label, or empty
118
141
  * with `multiple`.
119
142
  */
@@ -161,7 +184,8 @@ type ComboboxClearProps = ComboboxClearOwnProps & Omit<ComponentPropsWithRef<'bu
161
184
  * Empties the input and the selection — every selected item, with `multiple`. Give it an
162
185
  * `aria-label`.
163
186
  *
164
- * `data-visible` is present while there is a selection to clear; style it hidden otherwise.
187
+ * `data-visible` is present while there is a selection to clear and the Root is not disabled;
188
+ * style it hidden otherwise.
165
189
  * Out of the tab order, and a press keeps focus in the input.
166
190
  */
167
191
  declare function ComboboxClear({ className, children, render, ...rest }: ComboboxClearProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
@@ -194,7 +218,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
194
218
  type ComboboxListOwnProps<T> = {
195
219
  /**
196
220
  * A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
197
- * virtualizer, render only the rows it asks for.
221
+ * virtualizer, render only the rows it asks for. With groups, it is called once per group
222
+ * the filter left an item in, with that group, and renders a `Combobox.Group`.
198
223
  */
199
224
  children?: ReactNode | ((item: T, index: number) => ReactNode);
200
225
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -204,7 +229,8 @@ type ComboboxListOwnProps<T> = {
204
229
  };
205
230
  type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
206
231
  /**
207
- * The `role="listbox"`. Given a function, renders it once per item the filter left.
232
+ * The `role="listbox"`. Given a function, renders it once per item the filter left — or, when
233
+ * the Root's `items` are groups, once per group.
208
234
  *
209
235
  * With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
210
236
  * view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
@@ -214,7 +240,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
214
240
  type ComboboxItemOwnProps<T> = {
215
241
  /** The item this option stands for — one of the Root's `items`. */
216
242
  value: T;
217
- /** Cannot be picked, and the arrow keys skip it. */
243
+ /** Cannot be picked, and the arrow keys skip it. A disabled Group disables it too. */
218
244
  disabled?: boolean;
219
245
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
220
246
  render?: ReactElement;
@@ -229,7 +255,55 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
229
255
  * `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
230
256
  * the arrow keys and the pointer. Only the items whose highlight changes re-render.
231
257
  */
232
- declare function ComboboxItem<T = unknown>({ value, disabled, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
258
+ declare function ComboboxItem<T = unknown>({ value, disabled: disabledProp, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
259
+
260
+ type ComboboxGroupOwnProps = {
261
+ /** Disables every Item inside, as `disabled` on an `<optgroup>` does. */
262
+ disabled?: boolean;
263
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
264
+ render?: ReactElement;
265
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
266
+ ref?: Ref<HTMLElement>;
267
+ };
268
+ type ComboboxGroupProps = ComboboxGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupOwnProps>;
269
+ /**
270
+ * Items under a label, as an `<optgroup>`: `role="group"`, named by its GroupLabel. Render one
271
+ * from the List's function for each group of the Root's `items`, with an Item for each of the
272
+ * group's `items` — the filter has already narrowed them, and left out a group with none.
273
+ *
274
+ * The arrow keys run through every group's items as one list, past the labels.
275
+ */
276
+ declare function ComboboxGroup({ disabled, className, children, render, ...rest }: ComboboxGroupProps): react.JSX.Element;
277
+
278
+ type ComboboxGroupLabelOwnProps = {
279
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
280
+ render?: ReactElement;
281
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
282
+ ref?: Ref<HTMLElement>;
283
+ };
284
+ type ComboboxGroupLabelProps = ComboboxGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupLabelOwnProps>;
285
+ /**
286
+ * Names its Group, as an `<optgroup>`'s `label` does. Shown in the list, but not an option:
287
+ * it cannot be picked, and the arrow keys pass it.
288
+ */
289
+ declare function ComboboxGroupLabel({ className, children, render, ...rest }: ComboboxGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
290
+
291
+ type ComboboxSeparatorOwnProps = {
292
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
293
+ render?: ReactElement;
294
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
295
+ ref?: Ref<HTMLElement>;
296
+ };
297
+ type ComboboxSeparatorProps = ComboboxSeparatorOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxSeparatorOwnProps>;
298
+ /**
299
+ * A line between items or groups in the List, as an `<hr>` in a `<select>`. The arrow keys pass
300
+ * it.
301
+ *
302
+ * Visual only: `aria-hidden`, with no `role="separator"`. A listbox may own only options and
303
+ * groups, so a separator role inside one fails `aria-required-children`; a Group, named by its
304
+ * label, is what tells a screen reader where one set ends.
305
+ */
306
+ declare function ComboboxSeparator({ className, render, ...rest }: ComboboxSeparatorProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
233
307
 
234
308
  type ComboboxEmptyOwnProps = {
235
309
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -308,7 +382,7 @@ type ComboboxValueProps<T = unknown> = {
308
382
  declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
309
383
 
310
384
  declare namespace index_parts {
311
- export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, 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 };
385
+ 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, ComboboxSeparator as Separator, ComboboxStatus as Status, ComboboxTrigger as Trigger, ComboboxValue as Value };
312
386
  }
313
387
 
314
388
  /**
@@ -319,4 +393,4 @@ declare namespace index_parts {
319
393
  */
320
394
  declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
321
395
 
322
- 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 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 };
396
+ 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 ComboboxSeparatorProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue$1 as ComboboxValue, type ComboboxValueProps, defaultFilter };
@@ -3,7 +3,9 @@ import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
3
  import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.js';
4
4
 
5
5
  /** Why a value, the input's text or the open state changed — the second callback argument. */
6
- type ComboboxChangeReason = 'input' | 'input-press' | 'trigger-press' | 'item-press' | 'keyboard' | 'clear' | 'chip-remove' | 'escape' | 'outside-press' | 'blur' | 'form-reset';
6
+ type ComboboxChangeReason = 'input' | 'input-press' | 'trigger-press' | 'item-press' | 'keyboard' | 'clear' | 'chip-remove' | 'escape' | 'outside-press' | 'blur' | 'form-reset'
7
+ /** The input's text followed a change of `value` — the parent's, not the user's. */
8
+ | 'value-change';
7
9
  type ComboboxChangeDetails = {
8
10
  reason: ComboboxChangeReason;
9
11
  };
@@ -24,9 +26,20 @@ type ComboboxState = {
24
26
  type ComboboxValue$1<T, Multiple extends boolean> = Multiple extends true ? T[] : T | null;
25
27
  /** Takes the whole list, so a filter can rank as well as narrow — a fuzzy search, say. */
26
28
  type ComboboxFilter<T> = (items: readonly T[], query: string, itemToString: (item: T) => string) => T[];
29
+ /**
30
+ * Items under a label, as an `<optgroup>`: any object with an `items` array — the rest, a
31
+ * label or an id, is yours to render.
32
+ */
33
+ type ComboboxGroup$1<T> = {
34
+ readonly items: readonly T[];
35
+ };
27
36
  type ComboboxRootProps<T, Multiple extends boolean = false> = {
28
- /** Every item the list can show. With server search, the current results. */
29
- items: readonly T[];
37
+ /**
38
+ * Every item the list can show. With server search, the current results. Or groups of
39
+ * them: each an object with an `items` array, which the filter searches and `List` renders
40
+ * one by one, leaving out the groups with no match.
41
+ */
42
+ items: readonly T[] | readonly ComboboxGroup$1<T>[];
30
43
  /**
31
44
  * An item's label: what the input shows once it is picked, and what the default filter
32
45
  * matches. Defaults to the item itself for strings, else its `label` property.
@@ -78,6 +91,11 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
78
91
  loading?: boolean;
79
92
  /** Disables the input and every button. */
80
93
  disabled?: boolean;
94
+ /**
95
+ * Something must be selected before the form submits. The input is `required` while the
96
+ * selection is empty, so the browser's own validation reports it.
97
+ */
98
+ required?: boolean;
81
99
  /** Submits each selected item's key under this name, from hidden inputs. */
82
100
  name?: string;
83
101
  /** Associates the hidden inputs with a `<form>` by id, when rendered outside it. */
@@ -96,7 +114,7 @@ type ComboboxRootProps<T, Multiple extends boolean = false> = {
96
114
  *
97
115
  * Focus stays in the input; the highlighted option is its `aria-activedescendant`.
98
116
  */
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;
117
+ 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
118
 
101
119
  type ComboboxInputOwnProps = {
102
120
  /** Element to render instead of the default `<input>`. Props and ref are merged onto it. */
@@ -114,6 +132,11 @@ type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'in
114
132
  * Enter picks the highlighted item; Esc closes. With `multiple`, Backspace in an empty input
115
133
  * removes the last selected item, and Left at the start of the text moves into the chips.
116
134
  *
135
+ * With the Root's `required`, the input is `required` while nothing is selected, so the
136
+ * browser's validation reports it, `:invalid` matches it, and `form.checkValidity()` fails.
137
+ * Text typed but not picked is still nothing selected: the input reports `valueMissing`'s
138
+ * message then, through `setCustomValidity`.
139
+ *
117
140
  * Leaving the input closes the list and puts the text back: the selected label, or empty
118
141
  * with `multiple`.
119
142
  */
@@ -161,7 +184,8 @@ type ComboboxClearProps = ComboboxClearOwnProps & Omit<ComponentPropsWithRef<'bu
161
184
  * Empties the input and the selection — every selected item, with `multiple`. Give it an
162
185
  * `aria-label`.
163
186
  *
164
- * `data-visible` is present while there is a selection to clear; style it hidden otherwise.
187
+ * `data-visible` is present while there is a selection to clear and the Root is not disabled;
188
+ * style it hidden otherwise.
165
189
  * Out of the tab order, and a press keeps focus in the input.
166
190
  */
167
191
  declare function ComboboxClear({ className, children, render, ...rest }: ComboboxClearProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
@@ -194,7 +218,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
194
218
  type ComboboxListOwnProps<T> = {
195
219
  /**
196
220
  * A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
197
- * virtualizer, render only the rows it asks for.
221
+ * virtualizer, render only the rows it asks for. With groups, it is called once per group
222
+ * the filter left an item in, with that group, and renders a `Combobox.Group`.
198
223
  */
199
224
  children?: ReactNode | ((item: T, index: number) => ReactNode);
200
225
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -204,7 +229,8 @@ type ComboboxListOwnProps<T> = {
204
229
  };
205
230
  type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
206
231
  /**
207
- * The `role="listbox"`. Given a function, renders it once per item the filter left.
232
+ * The `role="listbox"`. Given a function, renders it once per item the filter left — or, when
233
+ * the Root's `items` are groups, once per group.
208
234
  *
209
235
  * With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
210
236
  * view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
@@ -214,7 +240,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
214
240
  type ComboboxItemOwnProps<T> = {
215
241
  /** The item this option stands for — one of the Root's `items`. */
216
242
  value: T;
217
- /** Cannot be picked, and the arrow keys skip it. */
243
+ /** Cannot be picked, and the arrow keys skip it. A disabled Group disables it too. */
218
244
  disabled?: boolean;
219
245
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
220
246
  render?: ReactElement;
@@ -229,7 +255,55 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
229
255
  * `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
230
256
  * the arrow keys and the pointer. Only the items whose highlight changes re-render.
231
257
  */
232
- declare function ComboboxItem<T = unknown>({ value, disabled, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
258
+ declare function ComboboxItem<T = unknown>({ value, disabled: disabledProp, className, children, render, ...rest }: ComboboxItemProps<T>): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
259
+
260
+ type ComboboxGroupOwnProps = {
261
+ /** Disables every Item inside, as `disabled` on an `<optgroup>` does. */
262
+ disabled?: boolean;
263
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
264
+ render?: ReactElement;
265
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
266
+ ref?: Ref<HTMLElement>;
267
+ };
268
+ type ComboboxGroupProps = ComboboxGroupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupOwnProps>;
269
+ /**
270
+ * Items under a label, as an `<optgroup>`: `role="group"`, named by its GroupLabel. Render one
271
+ * from the List's function for each group of the Root's `items`, with an Item for each of the
272
+ * group's `items` — the filter has already narrowed them, and left out a group with none.
273
+ *
274
+ * The arrow keys run through every group's items as one list, past the labels.
275
+ */
276
+ declare function ComboboxGroup({ disabled, className, children, render, ...rest }: ComboboxGroupProps): react.JSX.Element;
277
+
278
+ type ComboboxGroupLabelOwnProps = {
279
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
280
+ render?: ReactElement;
281
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
282
+ ref?: Ref<HTMLElement>;
283
+ };
284
+ type ComboboxGroupLabelProps = ComboboxGroupLabelOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxGroupLabelOwnProps>;
285
+ /**
286
+ * Names its Group, as an `<optgroup>`'s `label` does. Shown in the list, but not an option:
287
+ * it cannot be picked, and the arrow keys pass it.
288
+ */
289
+ declare function ComboboxGroupLabel({ className, children, render, ...rest }: ComboboxGroupLabelProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
290
+
291
+ type ComboboxSeparatorOwnProps = {
292
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
293
+ render?: ReactElement;
294
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
295
+ ref?: Ref<HTMLElement>;
296
+ };
297
+ type ComboboxSeparatorProps = ComboboxSeparatorOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxSeparatorOwnProps>;
298
+ /**
299
+ * A line between items or groups in the List, as an `<hr>` in a `<select>`. The arrow keys pass
300
+ * it.
301
+ *
302
+ * Visual only: `aria-hidden`, with no `role="separator"`. A listbox may own only options and
303
+ * groups, so a separator role inside one fails `aria-required-children`; a Group, named by its
304
+ * label, is what tells a screen reader where one set ends.
305
+ */
306
+ declare function ComboboxSeparator({ className, render, ...rest }: ComboboxSeparatorProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
233
307
 
234
308
  type ComboboxEmptyOwnProps = {
235
309
  /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
@@ -308,7 +382,7 @@ type ComboboxValueProps<T = unknown> = {
308
382
  declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
309
383
 
310
384
  declare namespace index_parts {
311
- export { ComboboxChip as Chip, ComboboxChipRemove as ChipRemove, ComboboxClear as Clear, ComboboxEmpty as Empty, 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 };
385
+ 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, ComboboxSeparator as Separator, ComboboxStatus as Status, ComboboxTrigger as Trigger, ComboboxValue as Value };
312
386
  }
313
387
 
314
388
  /**
@@ -319,4 +393,4 @@ declare namespace index_parts {
319
393
  */
320
394
  declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
321
395
 
322
- 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 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 };
396
+ 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 ComboboxSeparatorProps, type ComboboxSide, type ComboboxState, type ComboboxStatusProps, type ComboboxTriggerProps, type ComboboxValue$1 as ComboboxValue, type ComboboxValueProps, defaultFilter };
@@ -22,12 +22,15 @@ __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,
26
28
  InputGroup: () => ComboboxInputGroup,
27
29
  Item: () => ComboboxItem,
28
30
  List: () => ComboboxList,
29
31
  Popup: () => ComboboxPopup,
30
32
  Root: () => ComboboxRoot,
33
+ Separator: () => ComboboxSeparator,
31
34
  Status: () => ComboboxStatus,
32
35
  Trigger: () => ComboboxTrigger,
33
36
  Value: () => ComboboxValue
@@ -75,6 +78,11 @@ function useComboboxRootContext(part) {
75
78
 
76
79
  // src/combobox/ComboboxRoot.tsx
77
80
  import { jsx, jsxs } from "react/jsx-runtime";
81
+ function isGroupList(items) {
82
+ return items.length > 0 && items.every(
83
+ (entry) => entry !== null && typeof entry === "object" && Array.isArray(entry.items)
84
+ );
85
+ }
78
86
  function defaultItemToString(item) {
79
87
  if (typeof item === "string") return item;
80
88
  if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
@@ -99,6 +107,7 @@ function ComboboxRoot({
99
107
  onLoadMore,
100
108
  loading = false,
101
109
  disabled = false,
110
+ required = false,
102
111
  name,
103
112
  form,
104
113
  children
@@ -135,10 +144,17 @@ function ComboboxRoot({
135
144
  });
136
145
  const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
137
146
  const deferredQuery = useDeferredValue(query);
138
- const filteredItems = useMemo(() => {
139
- if (filter === null) return items;
140
- const run = filter ?? defaultFilter;
141
- return run(items, deferredQuery, itemToString);
147
+ const { filteredItems, filteredGroups } = useMemo(() => {
148
+ const run = filter === null ? (list) => list : (list) => (filter ?? defaultFilter)(
149
+ list,
150
+ deferredQuery,
151
+ itemToString
152
+ );
153
+ if (!isGroupList(items)) {
154
+ return { filteredItems: run(items), filteredGroups: null };
155
+ }
156
+ const groups = items.map((group) => ({ ...group, items: run(group.items) })).filter((group) => group.items.length > 0);
157
+ return { filteredItems: groups.flatMap((group) => group.items), filteredGroups: groups };
142
158
  }, [items, filter, deferredQuery, itemToString]);
143
159
  const indexByKey = useMemo(
144
160
  () => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
@@ -232,7 +248,7 @@ function ComboboxRoot({
232
248
  useEffect(() => {
233
249
  if (lastSelectedKeyRef.current === selectedKey) return;
234
250
  lastSelectedKeyRef.current = selectedKey;
235
- setInputValueState(selectedLabel);
251
+ setInputValue(selectedLabel, "value-change");
236
252
  }, [selectedKey]);
237
253
  const [chips, setChips] = useState([]);
238
254
  const registerChip = useCallback((entry) => {
@@ -283,9 +299,11 @@ function ComboboxRoot({
283
299
  open,
284
300
  multiple,
285
301
  disabled,
302
+ required,
286
303
  inputValue,
287
304
  loading,
288
305
  filteredItems,
306
+ filteredGroups,
289
307
  itemToString,
290
308
  itemToKey,
291
309
  indexByKey,
@@ -319,9 +337,11 @@ function ComboboxRoot({
319
337
  open,
320
338
  multiple,
321
339
  disabled,
340
+ required,
322
341
  inputValue,
323
342
  loading,
324
343
  filteredItems,
344
+ filteredGroups,
325
345
  itemToString,
326
346
  itemToKey,
327
347
  indexByKey,
@@ -364,7 +384,7 @@ function ComboboxRoot({
364
384
  }
365
385
 
366
386
  // src/combobox/ComboboxInput.tsx
367
- import { useSyncExternalStore } from "react";
387
+ import { useLayoutEffect, useRef as useRef2, useSyncExternalStore } from "react";
368
388
 
369
389
  // src/combobox/comboboxDataAttributes.ts
370
390
  function comboboxDataAttributes({ open, disabled }) {
@@ -395,6 +415,7 @@ function ComboboxInput({ className, render, ...rest }) {
395
415
  open,
396
416
  multiple,
397
417
  disabled,
418
+ required,
398
419
  inputValue,
399
420
  filteredItems,
400
421
  itemToKey,
@@ -423,6 +444,20 @@ function ComboboxInput({ className, render, ...rest }) {
423
444
  () => highlight.get().index,
424
445
  () => -1
425
446
  );
447
+ const missing = required && selectedItems.length === 0;
448
+ const textOnly = missing && inputValue !== "";
449
+ const customValidityRef = useRef2(false);
450
+ useLayoutEffect(() => {
451
+ const input = inputRef.current;
452
+ if (!input || typeof input.setCustomValidity !== "function") return;
453
+ if (textOnly) {
454
+ input.setCustomValidity(valueMissingMessage());
455
+ customValidityRef.current = true;
456
+ } else if (customValidityRef.current) {
457
+ input.setCustomValidity("");
458
+ customValidityRef.current = false;
459
+ }
460
+ }, [textOnly, inputRef]);
426
461
  function enabledIndices() {
427
462
  const indices = [];
428
463
  filteredItems.forEach((item, index) => {
@@ -460,6 +495,9 @@ function ComboboxInput({ className, render, ...rest }) {
460
495
  "aria-controls": listId,
461
496
  "aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
462
497
  disabled: disabled || void 0,
498
+ required: missing || void 0,
499
+ // Stays when a selection lifts `required`: the field is still required.
500
+ "aria-required": required || void 0,
463
501
  value: inputValue,
464
502
  ...comboboxDataAttributes({ open, multiple, disabled }),
465
503
  // The anchor, unless an InputGroup around it is.
@@ -529,9 +567,14 @@ function ComboboxInput({ className, render, ...rest }) {
529
567
  consumerProps: rest
530
568
  });
531
569
  }
570
+ function valueMissingMessage() {
571
+ const probe = document.createElement("input");
572
+ probe.required = true;
573
+ return probe.validationMessage || "Please select an item in the list.";
574
+ }
532
575
 
533
576
  // src/combobox/ComboboxInputGroup.tsx
534
- import { useLayoutEffect, useRef as useRef2 } from "react";
577
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
535
578
  function ComboboxInputGroup({
536
579
  className,
537
580
  children,
@@ -539,9 +582,9 @@ function ComboboxInputGroup({
539
582
  ...rest
540
583
  }) {
541
584
  const { open, multiple, disabled, anchorName, registerGroup, registerInside, inputRef } = useComboboxRootContext("InputGroup");
542
- const elementRef = useRef2(null);
543
- useLayoutEffect(() => registerGroup(), [registerGroup]);
544
- useLayoutEffect(() => registerInside(elementRef), [registerInside]);
585
+ const elementRef = useRef3(null);
586
+ useLayoutEffect2(() => registerGroup(), [registerGroup]);
587
+ useLayoutEffect2(() => registerInside(elementRef), [registerInside]);
545
588
  return useRender({
546
589
  render,
547
590
  defaultTagName: "div",
@@ -562,11 +605,11 @@ function ComboboxInputGroup({
562
605
  }
563
606
 
564
607
  // src/combobox/ComboboxTrigger.tsx
565
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
608
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef4 } from "react";
566
609
  function ComboboxTrigger({ className, children, render, ...rest }) {
567
610
  const { open, multiple, disabled, setOpen, close, listId, inputRef, registerInside } = useComboboxRootContext("Trigger");
568
- const elementRef = useRef3(null);
569
- useLayoutEffect2(() => registerInside(elementRef), [registerInside]);
611
+ const elementRef = useRef4(null);
612
+ useLayoutEffect3(() => registerInside(elementRef), [registerInside]);
570
613
  return useRender({
571
614
  render,
572
615
  defaultTagName: "button",
@@ -594,11 +637,11 @@ function ComboboxTrigger({ className, children, render, ...rest }) {
594
637
  }
595
638
 
596
639
  // src/combobox/ComboboxClear.tsx
597
- import { useLayoutEffect as useLayoutEffect3, useRef as useRef4 } from "react";
640
+ import { useLayoutEffect as useLayoutEffect4, useRef as useRef5 } from "react";
598
641
  function ComboboxClear({ className, children, render, ...rest }) {
599
642
  const { disabled, selectedItems, clear, inputRef, registerInside } = useComboboxRootContext("Clear");
600
- const elementRef = useRef4(null);
601
- useLayoutEffect3(() => registerInside(elementRef), [registerInside]);
643
+ const elementRef = useRef5(null);
644
+ useLayoutEffect4(() => registerInside(elementRef), [registerInside]);
602
645
  return useRender({
603
646
  render,
604
647
  defaultTagName: "button",
@@ -606,7 +649,8 @@ function ComboboxClear({ className, children, render, ...rest }) {
606
649
  type: "button",
607
650
  tabIndex: -1,
608
651
  disabled: disabled || void 0,
609
- ...comboboxClearDataAttributes(selectedItems.length > 0),
652
+ // Nothing to clear while disabled: a disabled field hides it, as it has no effect.
653
+ ...comboboxClearDataAttributes(!disabled && selectedItems.length > 0),
610
654
  className,
611
655
  children,
612
656
  ref: elementRef,
@@ -681,7 +725,7 @@ function ComboboxPopup({
681
725
  }
682
726
 
683
727
  // src/combobox/ComboboxList.tsx
684
- import { useEffect as useEffect3, useRef as useRef5 } from "react";
728
+ import { useEffect as useEffect3, useRef as useRef6 } from "react";
685
729
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
686
730
  function ComboboxList({
687
731
  className,
@@ -689,8 +733,10 @@ function ComboboxList({
689
733
  render,
690
734
  ...rest
691
735
  }) {
692
- const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
693
- const content = typeof children === "function" ? filteredItems.map((item, index) => children(item, index)) : children;
736
+ const { multiple, loading, filteredItems, filteredGroups, onLoadMore, listId } = useComboboxRootContext("List");
737
+ const content = typeof children === "function" ? (filteredGroups ?? filteredItems).map(
738
+ (entry, index) => children(entry, index)
739
+ ) : children;
694
740
  return useRender({
695
741
  render,
696
742
  defaultTagName: "div",
@@ -710,8 +756,8 @@ function ComboboxList({
710
756
  }
711
757
  function LoadMoreSentinel() {
712
758
  const { open, loading, filteredItems, onLoadMore } = useComboboxRootContext("List");
713
- const ref = useRef5(null);
714
- const onLoadMoreRef = useRef5(onLoadMore);
759
+ const ref = useRef6(null);
760
+ const onLoadMoreRef = useRef6(onLoadMore);
715
761
  onLoadMoreRef.current = onLoadMore;
716
762
  useEffect3(() => {
717
763
  const element = ref.current;
@@ -726,10 +772,52 @@ function LoadMoreSentinel() {
726
772
  }
727
773
 
728
774
  // src/combobox/ComboboxItem.tsx
729
- import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useSyncExternalStore as useSyncExternalStore2 } from "react";
775
+ import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect5, useRef as useRef7, useSyncExternalStore as useSyncExternalStore2 } from "react";
776
+
777
+ // src/combobox/ComboboxGroup.tsx
778
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useId as useId2, useMemo as useMemo2, useState as useState2 } from "react";
779
+ import { jsx as jsx3 } from "react/jsx-runtime";
780
+ var ComboboxGroupContext = createContext2(null);
781
+ function useComboboxGroupContext() {
782
+ return useContext2(ComboboxGroupContext);
783
+ }
784
+ function ComboboxGroup({
785
+ disabled = false,
786
+ className,
787
+ children,
788
+ render,
789
+ ...rest
790
+ }) {
791
+ useComboboxRootContext("Group");
792
+ const labelId = `${useId2()}-label`;
793
+ const [hasLabel, setHasLabel] = useState2(false);
794
+ const registerLabel = useCallback2(() => {
795
+ setHasLabel(true);
796
+ return () => setHasLabel(false);
797
+ }, []);
798
+ const context = useMemo2(
799
+ () => ({ disabled, labelId, registerLabel }),
800
+ [disabled, labelId, registerLabel]
801
+ );
802
+ const element = useRender({
803
+ render,
804
+ defaultTagName: "div",
805
+ props: {
806
+ role: "group",
807
+ "aria-labelledby": hasLabel ? labelId : void 0,
808
+ "data-disabled": disabled ? "" : void 0,
809
+ className,
810
+ children
811
+ },
812
+ consumerProps: rest
813
+ });
814
+ return /* @__PURE__ */ jsx3(ComboboxGroupContext.Provider, { value: context, children: element });
815
+ }
816
+
817
+ // src/combobox/ComboboxItem.tsx
730
818
  function ComboboxItem({
731
819
  value,
732
- disabled = false,
820
+ disabled: disabledProp = false,
733
821
  className,
734
822
  children,
735
823
  render,
@@ -745,6 +833,8 @@ function ComboboxItem({
745
833
  select,
746
834
  optionId
747
835
  } = useComboboxRootContext("Item");
836
+ const group = useComboboxGroupContext();
837
+ const disabled = disabledProp || (group?.disabled ?? false);
748
838
  const key = itemToKey(value);
749
839
  const index = indexByKey.get(key) ?? -1;
750
840
  const selected = selectedKeys.has(key);
@@ -753,8 +843,8 @@ function ComboboxItem({
753
843
  () => index >= 0 && highlight.get().index === index,
754
844
  () => false
755
845
  );
756
- const elementRef = useRef6(null);
757
- useLayoutEffect4(() => {
846
+ const elementRef = useRef7(null);
847
+ useLayoutEffect5(() => {
758
848
  if (!disabled) return;
759
849
  disabledKeys.add(key);
760
850
  return () => {
@@ -792,6 +882,39 @@ function ComboboxItem({
792
882
  });
793
883
  }
794
884
 
885
+ // src/combobox/ComboboxGroupLabel.tsx
886
+ import { useLayoutEffect as useLayoutEffect6 } from "react";
887
+ function ComboboxGroupLabel({
888
+ className,
889
+ children,
890
+ render,
891
+ ...rest
892
+ }) {
893
+ const group = useComboboxGroupContext();
894
+ if (group === null) {
895
+ throw new Error("<Combobox.GroupLabel> must be rendered inside <Combobox.Group>.");
896
+ }
897
+ const { labelId, registerLabel } = group;
898
+ useLayoutEffect6(registerLabel, [registerLabel]);
899
+ return useRender({
900
+ render,
901
+ defaultTagName: "div",
902
+ props: { id: labelId, className, children },
903
+ consumerProps: rest
904
+ });
905
+ }
906
+
907
+ // src/combobox/ComboboxSeparator.tsx
908
+ function ComboboxSeparator({ className, render, ...rest }) {
909
+ useComboboxRootContext("Separator");
910
+ return useRender({
911
+ render,
912
+ defaultTagName: "div",
913
+ props: { "aria-hidden": true, className },
914
+ consumerProps: rest
915
+ });
916
+ }
917
+
795
918
  // src/combobox/ComboboxEmpty.tsx
796
919
  function ComboboxEmpty({ className, children, render, ...rest }) {
797
920
  const { filteredItems, loading } = useComboboxRootContext("Empty");
@@ -816,11 +939,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
816
939
  }
817
940
 
818
941
  // src/combobox/ComboboxChip.tsx
819
- import { createContext as createContext2, useContext as useContext2, useLayoutEffect as useLayoutEffect5, useMemo as useMemo2, useRef as useRef7 } from "react";
820
- import { jsx as jsx3 } from "react/jsx-runtime";
821
- var ComboboxChipContext = createContext2(null);
942
+ import { createContext as createContext3, useContext as useContext3, useLayoutEffect as useLayoutEffect7, useMemo as useMemo3, useRef as useRef8 } from "react";
943
+ import { jsx as jsx4 } from "react/jsx-runtime";
944
+ var ComboboxChipContext = createContext3(null);
822
945
  function useComboboxChipContext() {
823
- const context = useContext2(ComboboxChipContext);
946
+ const context = useContext3(ComboboxChipContext);
824
947
  if (context === null) {
825
948
  throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
826
949
  }
@@ -835,9 +958,9 @@ function ComboboxChip({
835
958
  }) {
836
959
  const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
837
960
  const key = itemToKey(value);
838
- const elementRef = useRef7(null);
839
- useLayoutEffect5(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
840
- const chipContext = useMemo2(() => ({ value }), [value]);
961
+ const elementRef = useRef8(null);
962
+ useLayoutEffect7(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
963
+ const chipContext = useMemo3(() => ({ value }), [value]);
841
964
  function focusAt(index) {
842
965
  const chip = chips[index];
843
966
  if (chip) chip.ref.current?.focus();
@@ -878,7 +1001,7 @@ function ComboboxChip({
878
1001
  },
879
1002
  consumerProps: rest
880
1003
  });
881
- return /* @__PURE__ */ jsx3(ComboboxChipContext.Provider, { value: chipContext, children: element });
1004
+ return /* @__PURE__ */ jsx4(ComboboxChipContext.Provider, { value: chipContext, children: element });
882
1005
  }
883
1006
 
884
1007
  // src/combobox/ComboboxChipRemove.tsx
@@ -913,11 +1036,11 @@ function ComboboxChipRemove({
913
1036
  }
914
1037
 
915
1038
  // src/combobox/ComboboxValue.tsx
916
- import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
1039
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
917
1040
  function ComboboxValue({ children }) {
918
1041
  const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
919
1042
  const value = multiple ? selectedItems : selectedItems[0] ?? null;
920
- return /* @__PURE__ */ jsx4(Fragment2, { children: children(value, { itemToString }) });
1043
+ return /* @__PURE__ */ jsx5(Fragment2, { children: children(value, { itemToString }) });
921
1044
  }
922
1045
  export {
923
1046
  index_parts_exports as Combobox,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.9.0",
3
+ "version": "4.11.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",