@arun-dev/headless 4.9.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.
- package/dist/combobox/index.cjs +131 -21
- package/dist/combobox/index.d.cts +63 -9
- package/dist/combobox/index.d.ts +63 -9
- package/dist/combobox/index.js +144 -34
- package/package.json +1 -1
package/dist/combobox/index.cjs
CHANGED
|
@@ -32,6 +32,8 @@ __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,
|
|
@@ -169,6 +171,11 @@ function useComboboxRootContext(part) {
|
|
|
169
171
|
|
|
170
172
|
// src/combobox/ComboboxRoot.tsx
|
|
171
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
|
+
}
|
|
172
179
|
function defaultItemToString(item) {
|
|
173
180
|
if (typeof item === "string") return item;
|
|
174
181
|
if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
|
|
@@ -193,6 +200,7 @@ function ComboboxRoot({
|
|
|
193
200
|
onLoadMore,
|
|
194
201
|
loading = false,
|
|
195
202
|
disabled = false,
|
|
203
|
+
required = false,
|
|
196
204
|
name,
|
|
197
205
|
form,
|
|
198
206
|
children
|
|
@@ -229,10 +237,17 @@ function ComboboxRoot({
|
|
|
229
237
|
});
|
|
230
238
|
const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
|
|
231
239
|
const deferredQuery = (0, import_react3.useDeferredValue)(query);
|
|
232
|
-
const filteredItems = (0, import_react3.useMemo)(() => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 };
|
|
236
251
|
}, [items, filter, deferredQuery, itemToString]);
|
|
237
252
|
const indexByKey = (0, import_react3.useMemo)(
|
|
238
253
|
() => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
|
|
@@ -377,9 +392,11 @@ function ComboboxRoot({
|
|
|
377
392
|
open,
|
|
378
393
|
multiple,
|
|
379
394
|
disabled,
|
|
395
|
+
required,
|
|
380
396
|
inputValue,
|
|
381
397
|
loading,
|
|
382
398
|
filteredItems,
|
|
399
|
+
filteredGroups,
|
|
383
400
|
itemToString,
|
|
384
401
|
itemToKey,
|
|
385
402
|
indexByKey,
|
|
@@ -413,9 +430,11 @@ function ComboboxRoot({
|
|
|
413
430
|
open,
|
|
414
431
|
multiple,
|
|
415
432
|
disabled,
|
|
433
|
+
required,
|
|
416
434
|
inputValue,
|
|
417
435
|
loading,
|
|
418
436
|
filteredItems,
|
|
437
|
+
filteredGroups,
|
|
419
438
|
itemToString,
|
|
420
439
|
itemToKey,
|
|
421
440
|
indexByKey,
|
|
@@ -578,6 +597,7 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
578
597
|
open,
|
|
579
598
|
multiple,
|
|
580
599
|
disabled,
|
|
600
|
+
required,
|
|
581
601
|
inputValue,
|
|
582
602
|
filteredItems,
|
|
583
603
|
itemToKey,
|
|
@@ -606,6 +626,20 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
606
626
|
() => highlight.get().index,
|
|
607
627
|
() => -1
|
|
608
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]);
|
|
609
643
|
function enabledIndices() {
|
|
610
644
|
const indices = [];
|
|
611
645
|
filteredItems.forEach((item, index) => {
|
|
@@ -643,6 +677,9 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
643
677
|
"aria-controls": listId,
|
|
644
678
|
"aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
|
|
645
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,
|
|
646
683
|
value: inputValue,
|
|
647
684
|
...comboboxDataAttributes({ open, multiple, disabled }),
|
|
648
685
|
// The anchor, unless an InputGroup around it is.
|
|
@@ -712,6 +749,11 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
712
749
|
consumerProps: rest
|
|
713
750
|
});
|
|
714
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
|
+
}
|
|
715
757
|
|
|
716
758
|
// src/combobox/ComboboxInputGroup.tsx
|
|
717
759
|
var import_react6 = require("react");
|
|
@@ -872,8 +914,10 @@ function ComboboxList({
|
|
|
872
914
|
render,
|
|
873
915
|
...rest
|
|
874
916
|
}) {
|
|
875
|
-
const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
|
|
876
|
-
const content = typeof children === "function" ? filteredItems.map(
|
|
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;
|
|
877
921
|
return useRender({
|
|
878
922
|
render,
|
|
879
923
|
defaultTagName: "div",
|
|
@@ -909,10 +953,52 @@ function LoadMoreSentinel() {
|
|
|
909
953
|
}
|
|
910
954
|
|
|
911
955
|
// src/combobox/ComboboxItem.tsx
|
|
956
|
+
var import_react12 = require("react");
|
|
957
|
+
|
|
958
|
+
// src/combobox/ComboboxGroup.tsx
|
|
912
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
|
|
913
999
|
function ComboboxItem({
|
|
914
1000
|
value,
|
|
915
|
-
disabled = false,
|
|
1001
|
+
disabled: disabledProp = false,
|
|
916
1002
|
className,
|
|
917
1003
|
children,
|
|
918
1004
|
render,
|
|
@@ -928,23 +1014,25 @@ function ComboboxItem({
|
|
|
928
1014
|
select,
|
|
929
1015
|
optionId
|
|
930
1016
|
} = useComboboxRootContext("Item");
|
|
1017
|
+
const group = useComboboxGroupContext();
|
|
1018
|
+
const disabled = disabledProp || (group?.disabled ?? false);
|
|
931
1019
|
const key = itemToKey(value);
|
|
932
1020
|
const index = indexByKey.get(key) ?? -1;
|
|
933
1021
|
const selected = selectedKeys.has(key);
|
|
934
|
-
const highlighted = (0,
|
|
1022
|
+
const highlighted = (0, import_react12.useSyncExternalStore)(
|
|
935
1023
|
highlight.subscribe,
|
|
936
1024
|
() => index >= 0 && highlight.get().index === index,
|
|
937
1025
|
() => false
|
|
938
1026
|
);
|
|
939
|
-
const elementRef = (0,
|
|
940
|
-
(0,
|
|
1027
|
+
const elementRef = (0, import_react12.useRef)(null);
|
|
1028
|
+
(0, import_react12.useLayoutEffect)(() => {
|
|
941
1029
|
if (!disabled) return;
|
|
942
1030
|
disabledKeys.add(key);
|
|
943
1031
|
return () => {
|
|
944
1032
|
disabledKeys.delete(key);
|
|
945
1033
|
};
|
|
946
1034
|
}, [disabled, disabledKeys, key]);
|
|
947
|
-
(0,
|
|
1035
|
+
(0, import_react12.useEffect)(() => {
|
|
948
1036
|
if (highlighted && highlight.get().reason === "keyboard") {
|
|
949
1037
|
elementRef.current?.scrollIntoView?.({ block: "nearest" });
|
|
950
1038
|
}
|
|
@@ -975,6 +1063,28 @@ function ComboboxItem({
|
|
|
975
1063
|
});
|
|
976
1064
|
}
|
|
977
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
|
+
|
|
978
1088
|
// src/combobox/ComboboxEmpty.tsx
|
|
979
1089
|
function ComboboxEmpty({ className, children, render, ...rest }) {
|
|
980
1090
|
const { filteredItems, loading } = useComboboxRootContext("Empty");
|
|
@@ -999,11 +1109,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
|
|
|
999
1109
|
}
|
|
1000
1110
|
|
|
1001
1111
|
// src/combobox/ComboboxChip.tsx
|
|
1002
|
-
var
|
|
1003
|
-
var
|
|
1004
|
-
var ComboboxChipContext = (0,
|
|
1112
|
+
var import_react14 = require("react");
|
|
1113
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1114
|
+
var ComboboxChipContext = (0, import_react14.createContext)(null);
|
|
1005
1115
|
function useComboboxChipContext() {
|
|
1006
|
-
const context = (0,
|
|
1116
|
+
const context = (0, import_react14.useContext)(ComboboxChipContext);
|
|
1007
1117
|
if (context === null) {
|
|
1008
1118
|
throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
|
|
1009
1119
|
}
|
|
@@ -1018,9 +1128,9 @@ function ComboboxChip({
|
|
|
1018
1128
|
}) {
|
|
1019
1129
|
const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
|
|
1020
1130
|
const key = itemToKey(value);
|
|
1021
|
-
const elementRef = (0,
|
|
1022
|
-
(0,
|
|
1023
|
-
const chipContext = (0,
|
|
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]);
|
|
1024
1134
|
function focusAt(index) {
|
|
1025
1135
|
const chip = chips[index];
|
|
1026
1136
|
if (chip) chip.ref.current?.focus();
|
|
@@ -1061,7 +1171,7 @@ function ComboboxChip({
|
|
|
1061
1171
|
},
|
|
1062
1172
|
consumerProps: rest
|
|
1063
1173
|
});
|
|
1064
|
-
return /* @__PURE__ */ (0,
|
|
1174
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ComboboxChipContext.Provider, { value: chipContext, children: element });
|
|
1065
1175
|
}
|
|
1066
1176
|
|
|
1067
1177
|
// src/combobox/ComboboxChipRemove.tsx
|
|
@@ -1096,11 +1206,11 @@ function ComboboxChipRemove({
|
|
|
1096
1206
|
}
|
|
1097
1207
|
|
|
1098
1208
|
// src/combobox/ComboboxValue.tsx
|
|
1099
|
-
var
|
|
1209
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1100
1210
|
function ComboboxValue({ children }) {
|
|
1101
1211
|
const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
|
|
1102
1212
|
const value = multiple ? selectedItems : selectedItems[0] ?? null;
|
|
1103
|
-
return /* @__PURE__ */ (0,
|
|
1213
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: children(value, { itemToString }) });
|
|
1104
1214
|
}
|
|
1105
1215
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1106
1216
|
0 && (module.exports = {
|
|
@@ -24,9 +24,20 @@ type ComboboxState = {
|
|
|
24
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
|
-
/**
|
|
29
|
-
|
|
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.
|
|
@@ -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. */
|
|
@@ -114,6 +130,11 @@ type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'in
|
|
|
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
|
*/
|
|
@@ -194,7 +215,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
|
|
|
194
215
|
type ComboboxListOwnProps<T> = {
|
|
195
216
|
/**
|
|
196
217
|
* A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
|
|
197
|
-
* 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`.
|
|
198
220
|
*/
|
|
199
221
|
children?: ReactNode | ((item: T, index: number) => ReactNode);
|
|
200
222
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
@@ -204,7 +226,8 @@ type ComboboxListOwnProps<T> = {
|
|
|
204
226
|
};
|
|
205
227
|
type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
|
|
206
228
|
/**
|
|
207
|
-
* 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.
|
|
208
231
|
*
|
|
209
232
|
* With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
|
|
210
233
|
* view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
|
|
@@ -214,7 +237,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
|
|
|
214
237
|
type ComboboxItemOwnProps<T> = {
|
|
215
238
|
/** The item this option stands for — one of the Root's `items`. */
|
|
216
239
|
value: T;
|
|
217
|
-
/** 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. */
|
|
218
241
|
disabled?: boolean;
|
|
219
242
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
220
243
|
render?: ReactElement;
|
|
@@ -229,7 +252,38 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
|
|
|
229
252
|
* `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
|
|
230
253
|
* the arrow keys and the pointer. Only the items whose highlight changes re-render.
|
|
231
254
|
*/
|
|
232
|
-
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>>;
|
|
233
287
|
|
|
234
288
|
type ComboboxEmptyOwnProps = {
|
|
235
289
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
@@ -308,7 +362,7 @@ type ComboboxValueProps<T = unknown> = {
|
|
|
308
362
|
declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
|
|
309
363
|
|
|
310
364
|
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 };
|
|
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 };
|
|
312
366
|
}
|
|
313
367
|
|
|
314
368
|
/**
|
|
@@ -319,4 +373,4 @@ declare namespace index_parts {
|
|
|
319
373
|
*/
|
|
320
374
|
declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
|
|
321
375
|
|
|
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 };
|
|
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 };
|
package/dist/combobox/index.d.ts
CHANGED
|
@@ -24,9 +24,20 @@ type ComboboxState = {
|
|
|
24
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
|
-
/**
|
|
29
|
-
|
|
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.
|
|
@@ -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. */
|
|
@@ -114,6 +130,11 @@ type ComboboxInputProps = ComboboxInputOwnProps & Omit<ComponentPropsWithRef<'in
|
|
|
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
|
*/
|
|
@@ -194,7 +215,8 @@ declare function ComboboxPopup({ side, align, className, children, render, ...re
|
|
|
194
215
|
type ComboboxListOwnProps<T> = {
|
|
195
216
|
/**
|
|
196
217
|
* A function rendering one `Combobox.Item` per filtered item, or any nodes. With a
|
|
197
|
-
* 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`.
|
|
198
220
|
*/
|
|
199
221
|
children?: ReactNode | ((item: T, index: number) => ReactNode);
|
|
200
222
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
@@ -204,7 +226,8 @@ type ComboboxListOwnProps<T> = {
|
|
|
204
226
|
};
|
|
205
227
|
type ComboboxListProps<T = unknown> = ComboboxListOwnProps<T> & Omit<ComponentPropsWithRef<'div'>, keyof ComboboxListOwnProps<T>>;
|
|
206
228
|
/**
|
|
207
|
-
* 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.
|
|
208
231
|
*
|
|
209
232
|
* With the Root's `onLoadMore`, an invisible sentinel follows the items; when it scrolls into
|
|
210
233
|
* view and nothing is `loading`, `onLoadMore` is called. The list is `aria-busy` meanwhile.
|
|
@@ -214,7 +237,7 @@ declare function ComboboxList<T = unknown>({ className, children, render, ...res
|
|
|
214
237
|
type ComboboxItemOwnProps<T> = {
|
|
215
238
|
/** The item this option stands for — one of the Root's `items`. */
|
|
216
239
|
value: T;
|
|
217
|
-
/** 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. */
|
|
218
241
|
disabled?: boolean;
|
|
219
242
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
220
243
|
render?: ReactElement;
|
|
@@ -229,7 +252,38 @@ type ComboboxItemProps<T = unknown> = ComboboxItemOwnProps<T> & Omit<ComponentPr
|
|
|
229
252
|
* `data-selected` and `aria-selected` follow the Root's `value`; `data-highlighted` follows
|
|
230
253
|
* the arrow keys and the pointer. Only the items whose highlight changes re-render.
|
|
231
254
|
*/
|
|
232
|
-
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>>;
|
|
233
287
|
|
|
234
288
|
type ComboboxEmptyOwnProps = {
|
|
235
289
|
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
@@ -308,7 +362,7 @@ type ComboboxValueProps<T = unknown> = {
|
|
|
308
362
|
declare function ComboboxValue<T = unknown>({ children }: ComboboxValueProps<T>): react.JSX.Element;
|
|
309
363
|
|
|
310
364
|
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 };
|
|
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 };
|
|
312
366
|
}
|
|
313
367
|
|
|
314
368
|
/**
|
|
@@ -319,4 +373,4 @@ declare namespace index_parts {
|
|
|
319
373
|
*/
|
|
320
374
|
declare function defaultFilter<T>(items: readonly T[], query: string, itemToString: (item: T) => string): T[];
|
|
321
375
|
|
|
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 };
|
|
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 };
|
package/dist/combobox/index.js
CHANGED
|
@@ -22,6 +22,8 @@ __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,
|
|
@@ -75,6 +77,11 @@ function useComboboxRootContext(part) {
|
|
|
75
77
|
|
|
76
78
|
// src/combobox/ComboboxRoot.tsx
|
|
77
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
|
+
}
|
|
78
85
|
function defaultItemToString(item) {
|
|
79
86
|
if (typeof item === "string") return item;
|
|
80
87
|
if (item !== null && typeof item === "object" && "label" in item) return String(item.label);
|
|
@@ -99,6 +106,7 @@ function ComboboxRoot({
|
|
|
99
106
|
onLoadMore,
|
|
100
107
|
loading = false,
|
|
101
108
|
disabled = false,
|
|
109
|
+
required = false,
|
|
102
110
|
name,
|
|
103
111
|
form,
|
|
104
112
|
children
|
|
@@ -135,10 +143,17 @@ function ComboboxRoot({
|
|
|
135
143
|
});
|
|
136
144
|
const query = !multiple && inputValue === selectedLabel ? "" : inputValue;
|
|
137
145
|
const deferredQuery = useDeferredValue(query);
|
|
138
|
-
const filteredItems = useMemo(() => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 };
|
|
142
157
|
}, [items, filter, deferredQuery, itemToString]);
|
|
143
158
|
const indexByKey = useMemo(
|
|
144
159
|
() => new Map(filteredItems.map((item, index) => [itemToKey(item), index])),
|
|
@@ -283,9 +298,11 @@ function ComboboxRoot({
|
|
|
283
298
|
open,
|
|
284
299
|
multiple,
|
|
285
300
|
disabled,
|
|
301
|
+
required,
|
|
286
302
|
inputValue,
|
|
287
303
|
loading,
|
|
288
304
|
filteredItems,
|
|
305
|
+
filteredGroups,
|
|
289
306
|
itemToString,
|
|
290
307
|
itemToKey,
|
|
291
308
|
indexByKey,
|
|
@@ -319,9 +336,11 @@ function ComboboxRoot({
|
|
|
319
336
|
open,
|
|
320
337
|
multiple,
|
|
321
338
|
disabled,
|
|
339
|
+
required,
|
|
322
340
|
inputValue,
|
|
323
341
|
loading,
|
|
324
342
|
filteredItems,
|
|
343
|
+
filteredGroups,
|
|
325
344
|
itemToString,
|
|
326
345
|
itemToKey,
|
|
327
346
|
indexByKey,
|
|
@@ -364,7 +383,7 @@ function ComboboxRoot({
|
|
|
364
383
|
}
|
|
365
384
|
|
|
366
385
|
// src/combobox/ComboboxInput.tsx
|
|
367
|
-
import { useSyncExternalStore } from "react";
|
|
386
|
+
import { useLayoutEffect, useRef as useRef2, useSyncExternalStore } from "react";
|
|
368
387
|
|
|
369
388
|
// src/combobox/comboboxDataAttributes.ts
|
|
370
389
|
function comboboxDataAttributes({ open, disabled }) {
|
|
@@ -395,6 +414,7 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
395
414
|
open,
|
|
396
415
|
multiple,
|
|
397
416
|
disabled,
|
|
417
|
+
required,
|
|
398
418
|
inputValue,
|
|
399
419
|
filteredItems,
|
|
400
420
|
itemToKey,
|
|
@@ -423,6 +443,20 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
423
443
|
() => highlight.get().index,
|
|
424
444
|
() => -1
|
|
425
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]);
|
|
426
460
|
function enabledIndices() {
|
|
427
461
|
const indices = [];
|
|
428
462
|
filteredItems.forEach((item, index) => {
|
|
@@ -460,6 +494,9 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
460
494
|
"aria-controls": listId,
|
|
461
495
|
"aria-activedescendant": open && highlighted >= 0 ? optionId(highlighted) : void 0,
|
|
462
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,
|
|
463
500
|
value: inputValue,
|
|
464
501
|
...comboboxDataAttributes({ open, multiple, disabled }),
|
|
465
502
|
// The anchor, unless an InputGroup around it is.
|
|
@@ -529,9 +566,14 @@ function ComboboxInput({ className, render, ...rest }) {
|
|
|
529
566
|
consumerProps: rest
|
|
530
567
|
});
|
|
531
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
|
+
}
|
|
532
574
|
|
|
533
575
|
// src/combobox/ComboboxInputGroup.tsx
|
|
534
|
-
import { useLayoutEffect, useRef as
|
|
576
|
+
import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
|
|
535
577
|
function ComboboxInputGroup({
|
|
536
578
|
className,
|
|
537
579
|
children,
|
|
@@ -539,9 +581,9 @@ function ComboboxInputGroup({
|
|
|
539
581
|
...rest
|
|
540
582
|
}) {
|
|
541
583
|
const { open, multiple, disabled, anchorName, registerGroup, registerInside, inputRef } = useComboboxRootContext("InputGroup");
|
|
542
|
-
const elementRef =
|
|
543
|
-
|
|
544
|
-
|
|
584
|
+
const elementRef = useRef3(null);
|
|
585
|
+
useLayoutEffect2(() => registerGroup(), [registerGroup]);
|
|
586
|
+
useLayoutEffect2(() => registerInside(elementRef), [registerInside]);
|
|
545
587
|
return useRender({
|
|
546
588
|
render,
|
|
547
589
|
defaultTagName: "div",
|
|
@@ -562,11 +604,11 @@ function ComboboxInputGroup({
|
|
|
562
604
|
}
|
|
563
605
|
|
|
564
606
|
// src/combobox/ComboboxTrigger.tsx
|
|
565
|
-
import { useLayoutEffect as
|
|
607
|
+
import { useLayoutEffect as useLayoutEffect3, useRef as useRef4 } from "react";
|
|
566
608
|
function ComboboxTrigger({ className, children, render, ...rest }) {
|
|
567
609
|
const { open, multiple, disabled, setOpen, close, listId, inputRef, registerInside } = useComboboxRootContext("Trigger");
|
|
568
|
-
const elementRef =
|
|
569
|
-
|
|
610
|
+
const elementRef = useRef4(null);
|
|
611
|
+
useLayoutEffect3(() => registerInside(elementRef), [registerInside]);
|
|
570
612
|
return useRender({
|
|
571
613
|
render,
|
|
572
614
|
defaultTagName: "button",
|
|
@@ -594,11 +636,11 @@ function ComboboxTrigger({ className, children, render, ...rest }) {
|
|
|
594
636
|
}
|
|
595
637
|
|
|
596
638
|
// src/combobox/ComboboxClear.tsx
|
|
597
|
-
import { useLayoutEffect as
|
|
639
|
+
import { useLayoutEffect as useLayoutEffect4, useRef as useRef5 } from "react";
|
|
598
640
|
function ComboboxClear({ className, children, render, ...rest }) {
|
|
599
641
|
const { disabled, selectedItems, clear, inputRef, registerInside } = useComboboxRootContext("Clear");
|
|
600
|
-
const elementRef =
|
|
601
|
-
|
|
642
|
+
const elementRef = useRef5(null);
|
|
643
|
+
useLayoutEffect4(() => registerInside(elementRef), [registerInside]);
|
|
602
644
|
return useRender({
|
|
603
645
|
render,
|
|
604
646
|
defaultTagName: "button",
|
|
@@ -681,7 +723,7 @@ function ComboboxPopup({
|
|
|
681
723
|
}
|
|
682
724
|
|
|
683
725
|
// src/combobox/ComboboxList.tsx
|
|
684
|
-
import { useEffect as useEffect3, useRef as
|
|
726
|
+
import { useEffect as useEffect3, useRef as useRef6 } from "react";
|
|
685
727
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
686
728
|
function ComboboxList({
|
|
687
729
|
className,
|
|
@@ -689,8 +731,10 @@ function ComboboxList({
|
|
|
689
731
|
render,
|
|
690
732
|
...rest
|
|
691
733
|
}) {
|
|
692
|
-
const { multiple, loading, filteredItems, onLoadMore, listId } = useComboboxRootContext("List");
|
|
693
|
-
const content = typeof children === "function" ? filteredItems.map(
|
|
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;
|
|
694
738
|
return useRender({
|
|
695
739
|
render,
|
|
696
740
|
defaultTagName: "div",
|
|
@@ -710,8 +754,8 @@ function ComboboxList({
|
|
|
710
754
|
}
|
|
711
755
|
function LoadMoreSentinel() {
|
|
712
756
|
const { open, loading, filteredItems, onLoadMore } = useComboboxRootContext("List");
|
|
713
|
-
const ref =
|
|
714
|
-
const onLoadMoreRef =
|
|
757
|
+
const ref = useRef6(null);
|
|
758
|
+
const onLoadMoreRef = useRef6(onLoadMore);
|
|
715
759
|
onLoadMoreRef.current = onLoadMore;
|
|
716
760
|
useEffect3(() => {
|
|
717
761
|
const element = ref.current;
|
|
@@ -726,10 +770,52 @@ function LoadMoreSentinel() {
|
|
|
726
770
|
}
|
|
727
771
|
|
|
728
772
|
// src/combobox/ComboboxItem.tsx
|
|
729
|
-
import { useEffect as useEffect4, useLayoutEffect as
|
|
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
|
|
730
816
|
function ComboboxItem({
|
|
731
817
|
value,
|
|
732
|
-
disabled = false,
|
|
818
|
+
disabled: disabledProp = false,
|
|
733
819
|
className,
|
|
734
820
|
children,
|
|
735
821
|
render,
|
|
@@ -745,6 +831,8 @@ function ComboboxItem({
|
|
|
745
831
|
select,
|
|
746
832
|
optionId
|
|
747
833
|
} = useComboboxRootContext("Item");
|
|
834
|
+
const group = useComboboxGroupContext();
|
|
835
|
+
const disabled = disabledProp || (group?.disabled ?? false);
|
|
748
836
|
const key = itemToKey(value);
|
|
749
837
|
const index = indexByKey.get(key) ?? -1;
|
|
750
838
|
const selected = selectedKeys.has(key);
|
|
@@ -753,8 +841,8 @@ function ComboboxItem({
|
|
|
753
841
|
() => index >= 0 && highlight.get().index === index,
|
|
754
842
|
() => false
|
|
755
843
|
);
|
|
756
|
-
const elementRef =
|
|
757
|
-
|
|
844
|
+
const elementRef = useRef7(null);
|
|
845
|
+
useLayoutEffect5(() => {
|
|
758
846
|
if (!disabled) return;
|
|
759
847
|
disabledKeys.add(key);
|
|
760
848
|
return () => {
|
|
@@ -792,6 +880,28 @@ function ComboboxItem({
|
|
|
792
880
|
});
|
|
793
881
|
}
|
|
794
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
|
+
|
|
795
905
|
// src/combobox/ComboboxEmpty.tsx
|
|
796
906
|
function ComboboxEmpty({ className, children, render, ...rest }) {
|
|
797
907
|
const { filteredItems, loading } = useComboboxRootContext("Empty");
|
|
@@ -816,11 +926,11 @@ function ComboboxStatus({ className, children, render, ...rest }) {
|
|
|
816
926
|
}
|
|
817
927
|
|
|
818
928
|
// src/combobox/ComboboxChip.tsx
|
|
819
|
-
import { createContext as
|
|
820
|
-
import { jsx as
|
|
821
|
-
var ComboboxChipContext =
|
|
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);
|
|
822
932
|
function useComboboxChipContext() {
|
|
823
|
-
const context =
|
|
933
|
+
const context = useContext3(ComboboxChipContext);
|
|
824
934
|
if (context === null) {
|
|
825
935
|
throw new Error("<Combobox.ChipRemove> must be rendered inside <Combobox.Chip>.");
|
|
826
936
|
}
|
|
@@ -835,9 +945,9 @@ function ComboboxChip({
|
|
|
835
945
|
}) {
|
|
836
946
|
const { disabled, itemToKey, remove, registerChip, chips, inputRef } = useComboboxRootContext("Chip");
|
|
837
947
|
const key = itemToKey(value);
|
|
838
|
-
const elementRef =
|
|
839
|
-
|
|
840
|
-
const chipContext =
|
|
948
|
+
const elementRef = useRef8(null);
|
|
949
|
+
useLayoutEffect7(() => registerChip({ key, ref: elementRef }), [registerChip, key]);
|
|
950
|
+
const chipContext = useMemo3(() => ({ value }), [value]);
|
|
841
951
|
function focusAt(index) {
|
|
842
952
|
const chip = chips[index];
|
|
843
953
|
if (chip) chip.ref.current?.focus();
|
|
@@ -878,7 +988,7 @@ function ComboboxChip({
|
|
|
878
988
|
},
|
|
879
989
|
consumerProps: rest
|
|
880
990
|
});
|
|
881
|
-
return /* @__PURE__ */
|
|
991
|
+
return /* @__PURE__ */ jsx4(ComboboxChipContext.Provider, { value: chipContext, children: element });
|
|
882
992
|
}
|
|
883
993
|
|
|
884
994
|
// src/combobox/ComboboxChipRemove.tsx
|
|
@@ -913,11 +1023,11 @@ function ComboboxChipRemove({
|
|
|
913
1023
|
}
|
|
914
1024
|
|
|
915
1025
|
// src/combobox/ComboboxValue.tsx
|
|
916
|
-
import { Fragment as Fragment2, jsx as
|
|
1026
|
+
import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
|
|
917
1027
|
function ComboboxValue({ children }) {
|
|
918
1028
|
const { multiple, selectedItems, itemToString } = useComboboxRootContext("Value");
|
|
919
1029
|
const value = multiple ? selectedItems : selectedItems[0] ?? null;
|
|
920
|
-
return /* @__PURE__ */
|
|
1030
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: children(value, { itemToString }) });
|
|
921
1031
|
}
|
|
922
1032
|
export {
|
|
923
1033
|
index_parts_exports as Combobox,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "4.
|
|
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",
|