@sproutsocial/seeds-react-menu 1.10.8 → 1.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.
- package/.turbo/turbo-build.log +21 -15
- package/CHANGELOG.md +22 -0
- package/dist/esm/index.js +45 -24
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v2/index.js +1 -1
- package/dist/esm/v2/index.js.map +1 -1
- package/dist/esm/v3/index.js +800 -0
- package/dist/esm/v3/index.js.map +1 -0
- package/dist/index.d.mts +22 -22
- package/dist/index.d.ts +22 -22
- package/dist/index.js +35 -14
- package/dist/index.js.map +1 -1
- package/dist/v2/index.js +1 -1
- package/dist/v2/index.js.map +1 -1
- package/dist/v3/index.d.mts +295 -0
- package/dist/v3/index.d.ts +295 -0
- package/dist/v3/index.js +868 -0
- package/dist/v3/index.js.map +1 -0
- package/package.json +7 -1
- package/src/MenuGroup/MenuGroup.tsx +16 -3
- package/src/MenuGroup/__tests__/MenuGroup.test.tsx +19 -0
- package/src/MenuItem/MenuItem.tsx +21 -10
- package/src/MenuItem/__tests__/MenuItem.test.tsx +19 -0
- package/src/v2/Common-V2/ComboboxContent.tsx +1 -1
- package/src/v3/Common/ComboboxItem.tsx +82 -0
- package/src/v3/Common/ComboboxStyles.tsx +475 -0
- package/src/v3/Common/SelectGroup.tsx +28 -0
- package/src/v3/Common/SelectIcons.tsx +26 -0
- package/src/v3/Common/SelectItem.tsx +108 -0
- package/src/v3/Common/SelectStyles.tsx +126 -0
- package/src/v3/Common/VirtualizedComboboxList.tsx +200 -0
- package/src/v3/Common/groupItems.ts +12 -0
- package/src/v3/Common/types.ts +3 -0
- package/src/v3/ModalStories.stories.tsx +181 -0
- package/src/v3/MultiCombobox.stories.tsx +322 -0
- package/src/v3/MultiCombobox.tsx +562 -0
- package/src/v3/MultiSearchableSelect.stories.tsx +389 -0
- package/src/v3/MultiSearchableSelect.tsx +545 -0
- package/src/v3/MultiSelect.stories.tsx +300 -0
- package/src/v3/MultiSelect.tsx +498 -0
- package/src/v3/SeedsPortal.tsx +35 -0
- package/src/v3/SingleCombobox.stories.tsx +169 -0
- package/src/v3/SingleCombobox.tsx +304 -0
- package/src/v3/SingleSearchableSelect.stories.tsx +239 -0
- package/src/v3/SingleSearchableSelect.tsx +319 -0
- package/src/v3/SingleSelect.stories.tsx +227 -0
- package/src/v3/SingleSelect.tsx +245 -0
- package/src/v3/index.ts +7 -0
- package/src/v3/storyData.tsx +117 -0
- package/tsup.config.ts +1 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Combobox } from "@base-ui/react/combobox";
|
|
3
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
4
|
+
import VirtualizedComboboxList from "./Common/VirtualizedComboboxList";
|
|
5
|
+
import { useSeedsPortalContainer } from "./SeedsPortal";
|
|
6
|
+
import ComboboxItem from "./Common/ComboboxItem";
|
|
7
|
+
import type { DataWithId } from "./Common/types";
|
|
8
|
+
import { groupItems } from "./Common/groupItems";
|
|
9
|
+
import { Field } from "./Common/SelectStyles";
|
|
10
|
+
import {
|
|
11
|
+
ComboboxTokenInputGroup,
|
|
12
|
+
ComboboxTokenInput,
|
|
13
|
+
ComboboxToken,
|
|
14
|
+
ComboboxActionButtons,
|
|
15
|
+
ComboboxClear,
|
|
16
|
+
ComboboxTrigger,
|
|
17
|
+
ComboboxPositioner,
|
|
18
|
+
ComboboxPopup,
|
|
19
|
+
ComboboxList,
|
|
20
|
+
ComboboxEmpty,
|
|
21
|
+
ComboboxStatus,
|
|
22
|
+
ComboboxGroup,
|
|
23
|
+
ComboboxGroupLabel,
|
|
24
|
+
ComboboxSeparator,
|
|
25
|
+
Token,
|
|
26
|
+
TokenRemove,
|
|
27
|
+
ComboboxGroupHeaderItem,
|
|
28
|
+
ComboboxGroupHeaderCheckbox,
|
|
29
|
+
ComboboxSelectAllItem,
|
|
30
|
+
} from "./Common/ComboboxStyles";
|
|
31
|
+
import { ClearIcon, StyledChevron, ChevronIcon } from "./Common/SelectIcons";
|
|
32
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
33
|
+
|
|
34
|
+
// ─── Sentinels ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface GroupHeaderSentinel {
|
|
37
|
+
__groupHeader: true;
|
|
38
|
+
groupName: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeGroupHeaderSentinel(groupName: string): GroupHeaderSentinel {
|
|
42
|
+
return { __groupHeader: true, groupName };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isGroupHeaderSentinel(v: unknown): v is GroupHeaderSentinel {
|
|
46
|
+
return typeof v === "object" && v !== null && "__groupHeader" in v;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SelectAllSentinel {
|
|
50
|
+
__selectAll: true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SELECT_ALL_SENTINEL: SelectAllSentinel = { __selectAll: true };
|
|
54
|
+
|
|
55
|
+
function isSelectAllSentinel(v: unknown): v is SelectAllSentinel {
|
|
56
|
+
return typeof v === "object" && v !== null && "__selectAll" in v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
interface MultiComboboxBaseProps {
|
|
62
|
+
id?: string;
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
emptyText?: string;
|
|
65
|
+
isLoading?: boolean;
|
|
66
|
+
loadingText?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface MultiComboboxDataProps<T extends DataWithId>
|
|
70
|
+
extends MultiComboboxBaseProps {
|
|
71
|
+
data: T[];
|
|
72
|
+
itemToString: (item: T) => string;
|
|
73
|
+
renderItem?: (item: T) => React.ReactNode;
|
|
74
|
+
renderToken?: (item: T) => React.ReactNode;
|
|
75
|
+
renderSelection?: (items: T[]) => React.ReactNode;
|
|
76
|
+
inputType?: "icon" | "checkbox" | "none";
|
|
77
|
+
selectedItemIds?: Array<T["id"]>;
|
|
78
|
+
defaultSelectedItemIds?: Array<T["id"]>;
|
|
79
|
+
onSelectedItemIdsChange?: (ids: Array<T["id"]>) => void;
|
|
80
|
+
groupBy?: (item: T) => string;
|
|
81
|
+
renderGroupHeading?: (label: string) => React.ReactNode;
|
|
82
|
+
selectHeadings?: boolean;
|
|
83
|
+
selectAll?: boolean;
|
|
84
|
+
isVirtualized?: boolean;
|
|
85
|
+
removeSelectedItems?: boolean;
|
|
86
|
+
maxTokens?: number;
|
|
87
|
+
children?: never;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface MultiComboboxChildrenProps extends MultiComboboxBaseProps {
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
selectedItemIds?: Array<string | number>;
|
|
93
|
+
defaultSelectedItemIds?: Array<string | number>;
|
|
94
|
+
onSelectedItemIdsChange?: (ids: Array<string | number>) => void;
|
|
95
|
+
data?: never;
|
|
96
|
+
itemToString?: never;
|
|
97
|
+
renderItem?: never;
|
|
98
|
+
renderToken?: never;
|
|
99
|
+
renderSelection?: never;
|
|
100
|
+
inputType?: never;
|
|
101
|
+
groupBy?: never;
|
|
102
|
+
renderGroupHeading?: never;
|
|
103
|
+
selectHeadings?: never;
|
|
104
|
+
selectAll?: never;
|
|
105
|
+
removeSelectedItems?: never;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type MultiComboboxProps<T extends DataWithId> =
|
|
109
|
+
| MultiComboboxDataProps<T>
|
|
110
|
+
| MultiComboboxChildrenProps;
|
|
111
|
+
|
|
112
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
// ─── Virtualized list ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export default function MultiCombobox<T extends DataWithId>(
|
|
119
|
+
props: MultiComboboxProps<T>
|
|
120
|
+
) {
|
|
121
|
+
const {
|
|
122
|
+
id,
|
|
123
|
+
placeholder = "Search...",
|
|
124
|
+
emptyText = "No results found.",
|
|
125
|
+
isLoading = false,
|
|
126
|
+
loadingText = "Loading...",
|
|
127
|
+
} = props;
|
|
128
|
+
|
|
129
|
+
const isChildrenMode = "children" in props && props.children != null;
|
|
130
|
+
|
|
131
|
+
const data = isChildrenMode
|
|
132
|
+
? ([] as T[])
|
|
133
|
+
: (props as MultiComboboxDataProps<T>).data;
|
|
134
|
+
const itemToString = isChildrenMode
|
|
135
|
+
? (_item: T) => ""
|
|
136
|
+
: (props as MultiComboboxDataProps<T>).itemToString;
|
|
137
|
+
const renderItem = isChildrenMode
|
|
138
|
+
? undefined
|
|
139
|
+
: (props as MultiComboboxDataProps<T>).renderItem;
|
|
140
|
+
const renderToken = isChildrenMode
|
|
141
|
+
? undefined
|
|
142
|
+
: (props as MultiComboboxDataProps<T>).renderToken;
|
|
143
|
+
const renderSelection = isChildrenMode
|
|
144
|
+
? undefined
|
|
145
|
+
: (props as MultiComboboxDataProps<T>).renderSelection;
|
|
146
|
+
const maxTokens = isChildrenMode
|
|
147
|
+
? undefined
|
|
148
|
+
: (props as MultiComboboxDataProps<T>).maxTokens;
|
|
149
|
+
const removeSelectedItems = isChildrenMode
|
|
150
|
+
? false
|
|
151
|
+
: (props as MultiComboboxDataProps<T>).removeSelectedItems ?? false;
|
|
152
|
+
const inputType = isChildrenMode
|
|
153
|
+
? "checkbox"
|
|
154
|
+
: removeSelectedItems
|
|
155
|
+
? "none"
|
|
156
|
+
: (props as MultiComboboxDataProps<T>).inputType ?? "checkbox";
|
|
157
|
+
const groupBy = isChildrenMode
|
|
158
|
+
? undefined
|
|
159
|
+
: (props as MultiComboboxDataProps<T>).groupBy;
|
|
160
|
+
const renderGroupHeading = isChildrenMode
|
|
161
|
+
? undefined
|
|
162
|
+
: (props as MultiComboboxDataProps<T>).renderGroupHeading;
|
|
163
|
+
const selectHeadings = isChildrenMode
|
|
164
|
+
? false
|
|
165
|
+
: (props as MultiComboboxDataProps<T>).selectHeadings ?? false;
|
|
166
|
+
const selectAll = isChildrenMode
|
|
167
|
+
? false
|
|
168
|
+
: (props as MultiComboboxDataProps<T>).selectAll ?? false;
|
|
169
|
+
const isVirtualized = isChildrenMode
|
|
170
|
+
? false
|
|
171
|
+
: (props as MultiComboboxDataProps<T>).isVirtualized ?? false;
|
|
172
|
+
const children = isChildrenMode
|
|
173
|
+
? (props as MultiComboboxChildrenProps).children
|
|
174
|
+
: undefined;
|
|
175
|
+
|
|
176
|
+
const selectedItemIds = props.selectedItemIds;
|
|
177
|
+
const defaultSelectedItemIds = props.defaultSelectedItemIds;
|
|
178
|
+
const onSelectedItemIdsChange = props.onSelectedItemIdsChange;
|
|
179
|
+
|
|
180
|
+
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
181
|
+
const [open, setOpen] = React.useState(false);
|
|
182
|
+
const virtualizerRef = React.useRef<ReturnType<
|
|
183
|
+
typeof useVirtualizer<HTMLDivElement, Element>
|
|
184
|
+
> | null>(null);
|
|
185
|
+
|
|
186
|
+
const isControlled = selectedItemIds !== undefined;
|
|
187
|
+
|
|
188
|
+
const idsToItems = React.useCallback(
|
|
189
|
+
(ids: Array<T["id"]>): T[] =>
|
|
190
|
+
ids
|
|
191
|
+
.map((id) => data.find((d) => d.id === id))
|
|
192
|
+
.filter((d): d is T => d != null),
|
|
193
|
+
[data]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const [internalValue, setInternalValue] = React.useState<T[]>(() =>
|
|
197
|
+
defaultSelectedItemIds
|
|
198
|
+
? idsToItems(defaultSelectedItemIds as Array<T["id"]>)
|
|
199
|
+
: []
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const value = isControlled
|
|
203
|
+
? idsToItems(selectedItemIds as Array<T["id"]>)
|
|
204
|
+
: internalValue;
|
|
205
|
+
|
|
206
|
+
const visibleData = React.useMemo(
|
|
207
|
+
() =>
|
|
208
|
+
removeSelectedItems
|
|
209
|
+
? data.filter((item) => !value.some((v) => v.id === item.id))
|
|
210
|
+
: data,
|
|
211
|
+
[data, value, removeSelectedItems]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const groups = React.useMemo(
|
|
215
|
+
() => (groupBy ? groupItems(visibleData, groupBy) : null),
|
|
216
|
+
[visibleData, groupBy]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// ─── Sentinel flat list ───────────────────────────────────────────────────
|
|
220
|
+
// Used when selectHeadings or selectAll. selectAll implies selectHeadings —
|
|
221
|
+
// group headers are always selectable when the list is flattened.
|
|
222
|
+
|
|
223
|
+
type FlatRow = T | GroupHeaderSentinel | SelectAllSentinel;
|
|
224
|
+
|
|
225
|
+
const flatItems = React.useMemo<FlatRow[] | null>(() => {
|
|
226
|
+
if (!groups || (!selectHeadings && !selectAll)) return null;
|
|
227
|
+
const rows: FlatRow[] = [];
|
|
228
|
+
if (selectAll) rows.push(SELECT_ALL_SENTINEL);
|
|
229
|
+
for (const group of groups) {
|
|
230
|
+
rows.push(makeGroupHeaderSentinel(group.value));
|
|
231
|
+
rows.push(...group.items);
|
|
232
|
+
}
|
|
233
|
+
return rows;
|
|
234
|
+
}, [groups, selectHeadings, selectAll]);
|
|
235
|
+
|
|
236
|
+
function setSelectedItems(items: T[]) {
|
|
237
|
+
if (!isControlled) setInternalValue(items);
|
|
238
|
+
if (onSelectedItemIdsChange)
|
|
239
|
+
onSelectedItemIdsChange(items.map((item) => item.id));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleValueChange(next: FlatRow[]) {
|
|
243
|
+
const selectAllClicked = next.filter(isSelectAllSentinel);
|
|
244
|
+
const groupSentinels = next.filter(isGroupHeaderSentinel);
|
|
245
|
+
const realItems = next.filter(
|
|
246
|
+
(v): v is T => !isGroupHeaderSentinel(v) && !isSelectAllSentinel(v)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (selectAllClicked.length > 0) {
|
|
250
|
+
const allSelected = data.every((d) =>
|
|
251
|
+
realItems.some((s) => s.id === d.id)
|
|
252
|
+
);
|
|
253
|
+
setSelectedItems(allSelected ? [] : [...data]);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (groupSentinels.length === 0) {
|
|
258
|
+
setSelectedItems(realItems);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let updated = [...realItems];
|
|
263
|
+
for (const sentinel of groupSentinels) {
|
|
264
|
+
const group = groups?.find((g) => g.value === sentinel.groupName);
|
|
265
|
+
if (!group) continue;
|
|
266
|
+
const allSelected = group.items.every((item) =>
|
|
267
|
+
updated.some((s) => s.id === item.id)
|
|
268
|
+
);
|
|
269
|
+
if (allSelected) {
|
|
270
|
+
updated = updated.filter(
|
|
271
|
+
(s) => !group.items.some((item) => item.id === s.id)
|
|
272
|
+
);
|
|
273
|
+
} else {
|
|
274
|
+
const missing = group.items.filter(
|
|
275
|
+
(item) => !updated.some((s) => s.id === item.id)
|
|
276
|
+
);
|
|
277
|
+
updated = [...updated, ...missing];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
setSelectedItems(updated);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleSimpleValueChange(newItems: T[]) {
|
|
284
|
+
setSelectedItems(newItems);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function removeItem(item: T, e: React.MouseEvent) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
e.stopPropagation();
|
|
290
|
+
setSelectedItems(value.filter((v) => v.id !== item.id));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Render sentinel item ─────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function renderSentinelRow(row: FlatRow) {
|
|
296
|
+
if (isSelectAllSentinel(row)) {
|
|
297
|
+
const totalCount = data.length;
|
|
298
|
+
const selectedCount = value.length;
|
|
299
|
+
const state: "none" | "partial" | "all" =
|
|
300
|
+
selectedCount === 0
|
|
301
|
+
? "none"
|
|
302
|
+
: selectedCount === totalCount
|
|
303
|
+
? "all"
|
|
304
|
+
: "partial";
|
|
305
|
+
return (
|
|
306
|
+
<ComboboxSelectAllItem key="__selectAll" value={row}>
|
|
307
|
+
<ComboboxGroupHeaderCheckbox $state={state} id="__selectAll" />
|
|
308
|
+
Select all
|
|
309
|
+
</ComboboxSelectAllItem>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (isGroupHeaderSentinel(row)) {
|
|
314
|
+
const group = groups?.find((g) => g.value === row.groupName);
|
|
315
|
+
const groupItemsList = group?.items ?? [];
|
|
316
|
+
const selectedCount = groupItemsList.filter((item) =>
|
|
317
|
+
value.some((s) => s.id === item.id)
|
|
318
|
+
).length;
|
|
319
|
+
const state: "none" | "partial" | "all" =
|
|
320
|
+
selectedCount === 0
|
|
321
|
+
? "none"
|
|
322
|
+
: selectedCount === groupItemsList.length
|
|
323
|
+
? "all"
|
|
324
|
+
: "partial";
|
|
325
|
+
return (
|
|
326
|
+
<ComboboxGroupHeaderItem key={`__header-${row.groupName}`} value={row}>
|
|
327
|
+
<ComboboxGroupHeaderCheckbox
|
|
328
|
+
$state={state}
|
|
329
|
+
id={`__header-${row.groupName}`}
|
|
330
|
+
/>
|
|
331
|
+
{renderGroupHeading
|
|
332
|
+
? renderGroupHeading(row.groupName)
|
|
333
|
+
: row.groupName}
|
|
334
|
+
</ComboboxGroupHeaderItem>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const item = row as T;
|
|
339
|
+
return (
|
|
340
|
+
<ComboboxItem
|
|
341
|
+
key={item.id}
|
|
342
|
+
value={item}
|
|
343
|
+
itemId={String(item.id)}
|
|
344
|
+
label={itemToString(item)}
|
|
345
|
+
inputType={inputType}
|
|
346
|
+
renderItem={
|
|
347
|
+
renderItem ? () => renderItem(item) : () => itemToString(item)
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Items passed to Combobox.Root ────────────────────────────────────────
|
|
354
|
+
// When selectHeadings: flatItems (flat array of group header sentinels + items).
|
|
355
|
+
// When selectAll only: prepend SELECT_ALL_SENTINEL to data so Base UI tracks
|
|
356
|
+
// it, while Combobox.List still uses the normal grouped render.
|
|
357
|
+
// The sentinel is never in flatItems — it is always rendered manually above
|
|
358
|
+
// Combobox.List, so we must not prepend it when flatItems is used.
|
|
359
|
+
|
|
360
|
+
const rootItems =
|
|
361
|
+
flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
|
|
362
|
+
|
|
363
|
+
const hasSentinels = selectAll || selectHeadings;
|
|
364
|
+
|
|
365
|
+
const rootValue = hasSentinels
|
|
366
|
+
? ([...value] as FlatRow[])
|
|
367
|
+
: (value as unknown as T[]);
|
|
368
|
+
|
|
369
|
+
return (
|
|
370
|
+
<Field>
|
|
371
|
+
<div ref={containerRef} style={{ position: "relative" }} />
|
|
372
|
+
<Combobox.Root
|
|
373
|
+
multiple
|
|
374
|
+
id={id}
|
|
375
|
+
virtualized={isVirtualized}
|
|
376
|
+
open={open}
|
|
377
|
+
onOpenChange={setOpen}
|
|
378
|
+
onItemHighlighted={
|
|
379
|
+
isVirtualized
|
|
380
|
+
? (item, { reason, index }) => {
|
|
381
|
+
const virt = virtualizerRef.current;
|
|
382
|
+
if (!item || !virt) return;
|
|
383
|
+
const isStart = index === 0;
|
|
384
|
+
const isEnd = index === virt.options.count - 1;
|
|
385
|
+
if (
|
|
386
|
+
reason === "none" ||
|
|
387
|
+
(reason === "keyboard" && (isStart || isEnd))
|
|
388
|
+
) {
|
|
389
|
+
queueMicrotask(() => {
|
|
390
|
+
virt.scrollToIndex(index, {
|
|
391
|
+
align: isEnd ? "start" : "end",
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
: undefined
|
|
397
|
+
}
|
|
398
|
+
value={rootValue}
|
|
399
|
+
onValueChange={
|
|
400
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
401
|
+
(hasSentinels ? handleValueChange : handleSimpleValueChange) as any
|
|
402
|
+
}
|
|
403
|
+
itemToStringLabel={(item: FlatRow | null) => {
|
|
404
|
+
if (!item || isGroupHeaderSentinel(item) || isSelectAllSentinel(item))
|
|
405
|
+
return "";
|
|
406
|
+
return itemToString(item as T);
|
|
407
|
+
}}
|
|
408
|
+
isItemEqualToValue={(a: FlatRow, b: FlatRow) => {
|
|
409
|
+
if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
|
|
410
|
+
return a.groupName === b.groupName;
|
|
411
|
+
if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
|
|
412
|
+
if (
|
|
413
|
+
!isGroupHeaderSentinel(a) &&
|
|
414
|
+
!isSelectAllSentinel(a) &&
|
|
415
|
+
!isGroupHeaderSentinel(b) &&
|
|
416
|
+
!isSelectAllSentinel(b)
|
|
417
|
+
)
|
|
418
|
+
return (a as T).id === (b as T).id;
|
|
419
|
+
return false;
|
|
420
|
+
}}
|
|
421
|
+
items={rootItems}
|
|
422
|
+
>
|
|
423
|
+
<ComboboxTokenInputGroup>
|
|
424
|
+
<ComboboxToken>
|
|
425
|
+
{renderSelection
|
|
426
|
+
? renderSelection(value)
|
|
427
|
+
: (() => {
|
|
428
|
+
const visibleTokens =
|
|
429
|
+
maxTokens != null ? value.slice(0, maxTokens) : value;
|
|
430
|
+
const overflow =
|
|
431
|
+
maxTokens != null ? value.length - maxTokens : 0;
|
|
432
|
+
return (
|
|
433
|
+
<>
|
|
434
|
+
{visibleTokens.map((item) => (
|
|
435
|
+
<Token key={item.id}>
|
|
436
|
+
{renderToken ? renderToken(item) : itemToString(item)}
|
|
437
|
+
<TokenRemove
|
|
438
|
+
aria-label={`Remove ${itemToString(item)}`}
|
|
439
|
+
onPointerDown={(e) => e.preventDefault()}
|
|
440
|
+
onClick={(e) => removeItem(item, e)}
|
|
441
|
+
>
|
|
442
|
+
<Icon name="x-outline" size="mini" />
|
|
443
|
+
</TokenRemove>
|
|
444
|
+
</Token>
|
|
445
|
+
))}
|
|
446
|
+
{overflow > 0 && (
|
|
447
|
+
<Token key="__overflow">+{overflow}</Token>
|
|
448
|
+
)}
|
|
449
|
+
</>
|
|
450
|
+
);
|
|
451
|
+
})()}
|
|
452
|
+
<ComboboxTokenInput
|
|
453
|
+
id={id}
|
|
454
|
+
placeholder={value.length > 0 ? "" : placeholder}
|
|
455
|
+
/>
|
|
456
|
+
<ComboboxActionButtons>
|
|
457
|
+
<ComboboxClear aria-label="Clear all">
|
|
458
|
+
<ClearIcon />
|
|
459
|
+
</ComboboxClear>
|
|
460
|
+
<ComboboxTrigger aria-label="Open popup">
|
|
461
|
+
<StyledChevron $isOpen={open}>
|
|
462
|
+
<ChevronIcon />
|
|
463
|
+
</StyledChevron>
|
|
464
|
+
</ComboboxTrigger>
|
|
465
|
+
</ComboboxActionButtons>
|
|
466
|
+
</ComboboxToken>
|
|
467
|
+
</ComboboxTokenInputGroup>
|
|
468
|
+
|
|
469
|
+
<Combobox.Portal container={portalContainer}>
|
|
470
|
+
<ComboboxPositioner sideOffset={4}>
|
|
471
|
+
<ComboboxPopup
|
|
472
|
+
aria-busy={isLoading || undefined}
|
|
473
|
+
style={
|
|
474
|
+
isVirtualized
|
|
475
|
+
? { display: "flex", flexDirection: "column" }
|
|
476
|
+
: undefined
|
|
477
|
+
}
|
|
478
|
+
>
|
|
479
|
+
<ComboboxStatus>{isLoading ? loadingText : null}</ComboboxStatus>
|
|
480
|
+
<ComboboxEmpty>{isLoading ? null : emptyText}</ComboboxEmpty>
|
|
481
|
+
<ComboboxList
|
|
482
|
+
style={
|
|
483
|
+
isVirtualized
|
|
484
|
+
? {
|
|
485
|
+
padding: 0,
|
|
486
|
+
flex: 1,
|
|
487
|
+
minHeight: 0,
|
|
488
|
+
display: "flex",
|
|
489
|
+
flexDirection: "column",
|
|
490
|
+
}
|
|
491
|
+
: undefined
|
|
492
|
+
}
|
|
493
|
+
>
|
|
494
|
+
{isVirtualized ? (
|
|
495
|
+
<VirtualizedComboboxList
|
|
496
|
+
open={open}
|
|
497
|
+
virtualizerRef={virtualizerRef}
|
|
498
|
+
value={value}
|
|
499
|
+
groups={groups}
|
|
500
|
+
data={visibleData}
|
|
501
|
+
itemToString={itemToString}
|
|
502
|
+
inputType={inputType}
|
|
503
|
+
renderItem={renderItem}
|
|
504
|
+
renderGroupHeading={renderGroupHeading}
|
|
505
|
+
/>
|
|
506
|
+
) : children ? (
|
|
507
|
+
children
|
|
508
|
+
) : flatItems ? (
|
|
509
|
+
(row: FlatRow) => renderSentinelRow(row)
|
|
510
|
+
) : groups ? (
|
|
511
|
+
(group: { value: string; items: T[] }, index: number) => (
|
|
512
|
+
<React.Fragment key={group.value}>
|
|
513
|
+
<ComboboxGroup items={group.items}>
|
|
514
|
+
<ComboboxGroupLabel>
|
|
515
|
+
{renderGroupHeading
|
|
516
|
+
? renderGroupHeading(group.value)
|
|
517
|
+
: group.value}
|
|
518
|
+
</ComboboxGroupLabel>
|
|
519
|
+
<Combobox.Collection>
|
|
520
|
+
{(item: T) => (
|
|
521
|
+
<ComboboxItem
|
|
522
|
+
key={item.id}
|
|
523
|
+
value={item}
|
|
524
|
+
itemId={String(item.id)}
|
|
525
|
+
label={itemToString(item)}
|
|
526
|
+
inputType={inputType}
|
|
527
|
+
renderItem={
|
|
528
|
+
renderItem
|
|
529
|
+
? () => renderItem(item)
|
|
530
|
+
: () => itemToString(item)
|
|
531
|
+
}
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
534
|
+
</Combobox.Collection>
|
|
535
|
+
</ComboboxGroup>
|
|
536
|
+
{index < groups.length - 1 && <ComboboxSeparator />}
|
|
537
|
+
</React.Fragment>
|
|
538
|
+
)
|
|
539
|
+
) : (
|
|
540
|
+
(item: T) => (
|
|
541
|
+
<ComboboxItem
|
|
542
|
+
key={item.id}
|
|
543
|
+
value={item}
|
|
544
|
+
itemId={String(item.id)}
|
|
545
|
+
label={itemToString(item)}
|
|
546
|
+
inputType={inputType}
|
|
547
|
+
renderItem={
|
|
548
|
+
renderItem
|
|
549
|
+
? () => renderItem(item)
|
|
550
|
+
: () => itemToString(item)
|
|
551
|
+
}
|
|
552
|
+
/>
|
|
553
|
+
)
|
|
554
|
+
)}
|
|
555
|
+
</ComboboxList>
|
|
556
|
+
</ComboboxPopup>
|
|
557
|
+
</ComboboxPositioner>
|
|
558
|
+
</Combobox.Portal>
|
|
559
|
+
</Combobox.Root>
|
|
560
|
+
</Field>
|
|
561
|
+
);
|
|
562
|
+
}
|