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