@sproutsocial/seeds-react-menu 1.12.1 → 1.12.3
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 +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/esm/v3/index.js +332 -92
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +14 -0
- package/dist/v3/index.d.ts +14 -0
- package/dist/v3/index.js +331 -91
- package/dist/v3/index.js.map +1 -1
- package/package.json +9 -9
- package/src/v3/Common/VirtualizedComboboxList.tsx +29 -2
- package/src/v3/Common/sentinels.ts +13 -0
- package/src/v3/MultiCombobox.stories.tsx +34 -0
- package/src/v3/MultiCombobox.tsx +223 -55
- package/src/v3/MultiSearchableSelect.tsx +217 -55
- package/src/v3/MultiSelect.tsx +3 -1
- package/src/v3/SingleCombobox.tsx +7 -1
- package/src/v3/SingleSearchableSelect.tsx +3 -1
- package/src/v3/SingleSelect.stories.tsx +19 -0
- package/src/v3/SingleSelect.tsx +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.3",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@radix-ui/react-popover": "^1.1.2",
|
|
39
39
|
"@sproutsocial/seeds-react-box": "^1.1.16",
|
|
40
|
-
"@sproutsocial/seeds-react-button": "^2.0.
|
|
41
|
-
"@sproutsocial/seeds-react-checkbox": "^1.3.
|
|
42
|
-
"@sproutsocial/seeds-react-icon": "^2.3.
|
|
43
|
-
"@sproutsocial/seeds-react-input": "^1.5.
|
|
40
|
+
"@sproutsocial/seeds-react-button": "^2.0.5",
|
|
41
|
+
"@sproutsocial/seeds-react-checkbox": "^1.3.33",
|
|
42
|
+
"@sproutsocial/seeds-react-icon": "^2.3.7",
|
|
43
|
+
"@sproutsocial/seeds-react-input": "^1.5.16",
|
|
44
44
|
"@sproutsocial/seeds-react-label": "^1.0.16",
|
|
45
|
-
"@sproutsocial/seeds-react-popout": "^2.4.
|
|
45
|
+
"@sproutsocial/seeds-react-popout": "^2.4.38",
|
|
46
46
|
"@sproutsocial/seeds-react-radio": "^1.3.16",
|
|
47
|
-
"@sproutsocial/seeds-react-switch": "^1.2.
|
|
47
|
+
"@sproutsocial/seeds-react-switch": "^1.2.32",
|
|
48
48
|
"@sproutsocial/seeds-react-system-props": "^3.0.2",
|
|
49
49
|
"@sproutsocial/seeds-react-theme": "^3.6.2",
|
|
50
|
-
"@sproutsocial/seeds-react-token-input": "^1.4.
|
|
50
|
+
"@sproutsocial/seeds-react-token-input": "^1.4.38",
|
|
51
51
|
"@sproutsocial/seeds-react-utilities": "^4.3.0",
|
|
52
52
|
"@base-ui/react": "^1.3.0",
|
|
53
53
|
"@tanstack/react-virtual": "^3.13.12",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"styled-system": "^5.1.5"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@sproutsocial/seeds-react-tooltip": "^1.1.
|
|
60
|
+
"@sproutsocial/seeds-react-tooltip": "^1.1.20",
|
|
61
61
|
"@sproutsocial/eslint-config-seeds": "*",
|
|
62
62
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
63
63
|
"@sproutsocial/seeds-testing": "*",
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
ComboboxSelectAllItem,
|
|
9
9
|
} from "./ComboboxStyles";
|
|
10
10
|
import type { DataWithId } from "./types";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isPlaceholderSentinel,
|
|
13
|
+
isCreatableSentinel,
|
|
14
|
+
type CreatableSentinel,
|
|
15
|
+
} from "./sentinels";
|
|
12
16
|
|
|
13
17
|
export const ITEM_HEIGHT = 32;
|
|
14
18
|
|
|
@@ -29,7 +33,11 @@ export function isSelectAllSentinel(v: unknown): v is SelectAllSentinel {
|
|
|
29
33
|
return typeof v === "object" && v !== null && "__selectAll" in v;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
export type FlatSentinelRow<T> =
|
|
36
|
+
export type FlatSentinelRow<T> =
|
|
37
|
+
| T
|
|
38
|
+
| GroupHeaderSentinel
|
|
39
|
+
| SelectAllSentinel
|
|
40
|
+
| CreatableSentinel;
|
|
33
41
|
|
|
34
42
|
interface VirtualizedComboboxListProps<T extends DataWithId> {
|
|
35
43
|
open: boolean;
|
|
@@ -47,6 +55,12 @@ interface VirtualizedComboboxListProps<T extends DataWithId> {
|
|
|
47
55
|
inputType: "icon" | "radio" | "checkbox" | "none";
|
|
48
56
|
renderItem?: (item: T) => React.ReactNode;
|
|
49
57
|
renderGroupHeading?: (label: string) => React.ReactNode;
|
|
58
|
+
renderCreatableItem?: (
|
|
59
|
+
sentinel: CreatableSentinel,
|
|
60
|
+
style: React.CSSProperties,
|
|
61
|
+
index: number,
|
|
62
|
+
measureRef: (el: Element | null) => void
|
|
63
|
+
) => React.ReactNode;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
@@ -59,6 +73,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
59
73
|
inputType,
|
|
60
74
|
renderItem,
|
|
61
75
|
renderGroupHeading,
|
|
76
|
+
renderCreatableItem,
|
|
62
77
|
}: VirtualizedComboboxListProps<T>) {
|
|
63
78
|
type Row = FlatSentinelRow<T>;
|
|
64
79
|
|
|
@@ -197,6 +212,18 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
197
212
|
);
|
|
198
213
|
}
|
|
199
214
|
|
|
215
|
+
if (isCreatableSentinel(row)) {
|
|
216
|
+
if (renderCreatableItem) {
|
|
217
|
+
return renderCreatableItem(
|
|
218
|
+
row,
|
|
219
|
+
baseStyle,
|
|
220
|
+
virtualItem.index,
|
|
221
|
+
virtualizer.measureElement
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
200
227
|
const item = row as T;
|
|
201
228
|
return (
|
|
202
229
|
<ComboboxItem
|
|
@@ -15,3 +15,16 @@ export function makePlaceholderSentinel(label: string): PlaceholderSentinel {
|
|
|
15
15
|
export function isPlaceholderSentinel(v: unknown): v is PlaceholderSentinel {
|
|
16
16
|
return typeof v === "object" && v !== null && "__placeholder" in v;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export interface CreatableSentinel {
|
|
20
|
+
__creatable: true;
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeCreatableSentinel(label: string): CreatableSentinel {
|
|
25
|
+
return { __creatable: true, label };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isCreatableSentinel(v: unknown): v is CreatableSentinel {
|
|
29
|
+
return typeof v === "object" && v !== null && "__creatable" in v;
|
|
30
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
largeBookSet,
|
|
9
9
|
itemToString,
|
|
10
10
|
renderItem,
|
|
11
|
+
type Book,
|
|
11
12
|
} from "./storyData";
|
|
12
13
|
|
|
13
14
|
const meta: Meta<typeof MultiCombobox> = {
|
|
@@ -421,3 +422,36 @@ export const CustomSearch: Story = {
|
|
|
421
422
|
</div>
|
|
422
423
|
),
|
|
423
424
|
};
|
|
425
|
+
|
|
426
|
+
export const Creatable: Story = {
|
|
427
|
+
name: "Creatable",
|
|
428
|
+
render: () => {
|
|
429
|
+
const [bookList, setBookList] = React.useState<Book[]>(books);
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div style={{ width: "300px" }}>
|
|
433
|
+
<FormField label="Pick or create books">
|
|
434
|
+
{({ id }) => (
|
|
435
|
+
<MultiCombobox
|
|
436
|
+
id={id}
|
|
437
|
+
data={bookList}
|
|
438
|
+
itemToString={itemToString}
|
|
439
|
+
placeholder="Search or create..."
|
|
440
|
+
isVirtualized
|
|
441
|
+
creatable
|
|
442
|
+
onCreate={(value) => {
|
|
443
|
+
const newBook: Book = {
|
|
444
|
+
id: `book-custom-${value.toLowerCase().replace(/\s+/g, "-")}`,
|
|
445
|
+
title: value,
|
|
446
|
+
author: "Unknown",
|
|
447
|
+
};
|
|
448
|
+
setBookList((prev) => [...prev, newBook]);
|
|
449
|
+
return newBook;
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
</FormField>
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
};
|
package/src/v3/MultiCombobox.tsx
CHANGED
|
@@ -6,6 +6,11 @@ import { useSeedsPortalContainer } from "./SeedsPortal";
|
|
|
6
6
|
import ComboboxItem from "./Common/ComboboxItem";
|
|
7
7
|
import type { DataWithId } from "./Common/types";
|
|
8
8
|
import { groupItems } from "./Common/groupItems";
|
|
9
|
+
import {
|
|
10
|
+
isCreatableSentinel,
|
|
11
|
+
makeCreatableSentinel,
|
|
12
|
+
type CreatableSentinel,
|
|
13
|
+
} from "./Common/sentinels";
|
|
9
14
|
import { Field } from "./Common/SelectStyles";
|
|
10
15
|
import {
|
|
11
16
|
ComboboxTokenInputGroup,
|
|
@@ -68,6 +73,7 @@ interface MultiComboboxBaseProps {
|
|
|
68
73
|
filter?: ((item: unknown, query: string) => boolean) | null;
|
|
69
74
|
inputValue?: string;
|
|
70
75
|
onInputValueChange?: (value: string) => void;
|
|
76
|
+
"aria-describedby"?: string;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
interface MultiComboboxDataProps<T extends DataWithId>
|
|
@@ -88,6 +94,8 @@ interface MultiComboboxDataProps<T extends DataWithId>
|
|
|
88
94
|
isVirtualized?: boolean;
|
|
89
95
|
removeSelectedItems?: boolean;
|
|
90
96
|
maxTokens?: number;
|
|
97
|
+
creatable?: boolean;
|
|
98
|
+
onCreate?: (value: string) => T;
|
|
91
99
|
children?: never;
|
|
92
100
|
}
|
|
93
101
|
|
|
@@ -107,6 +115,8 @@ interface MultiComboboxChildrenProps extends MultiComboboxBaseProps {
|
|
|
107
115
|
selectHeadings?: never;
|
|
108
116
|
selectAll?: never;
|
|
109
117
|
removeSelectedItems?: never;
|
|
118
|
+
creatable?: never;
|
|
119
|
+
onCreate?: never;
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
export type MultiComboboxProps<T extends DataWithId> =
|
|
@@ -132,6 +142,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
132
142
|
inputValue,
|
|
133
143
|
onInputValueChange,
|
|
134
144
|
} = props;
|
|
145
|
+
const ariaDescribedBy = props["aria-describedby"];
|
|
135
146
|
|
|
136
147
|
const isChildrenMode = "children" in props && props.children != null;
|
|
137
148
|
|
|
@@ -179,6 +190,12 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
179
190
|
const isVirtualized = isChildrenMode
|
|
180
191
|
? false
|
|
181
192
|
: (props as MultiComboboxDataProps<T>).isVirtualized ?? false;
|
|
193
|
+
const creatableProp = isChildrenMode
|
|
194
|
+
? false
|
|
195
|
+
: (props as MultiComboboxDataProps<T>).creatable ?? false;
|
|
196
|
+
const onCreateProp = isChildrenMode
|
|
197
|
+
? undefined
|
|
198
|
+
: (props as MultiComboboxDataProps<T>).onCreate;
|
|
182
199
|
const children = isChildrenMode
|
|
183
200
|
? (props as MultiComboboxChildrenProps).children
|
|
184
201
|
: undefined;
|
|
@@ -189,6 +206,8 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
189
206
|
|
|
190
207
|
const { containerRef, portalContainer } = useSeedsPortalContainer();
|
|
191
208
|
const [open, setOpen] = React.useState(false);
|
|
209
|
+
const [internalInputValue, setInternalInputValue] = React.useState("");
|
|
210
|
+
const skipNextInputChangeRef = React.useRef(false);
|
|
192
211
|
const virtualizerRef = React.useRef<ReturnType<
|
|
193
212
|
typeof useVirtualizer<HTMLDivElement, Element>
|
|
194
213
|
> | null>(null);
|
|
@@ -226,11 +245,29 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
226
245
|
[visibleData, groupBy]
|
|
227
246
|
);
|
|
228
247
|
|
|
248
|
+
const currentInputValue = inputValue ?? internalInputValue;
|
|
249
|
+
|
|
250
|
+
const creatableSentinel = React.useMemo<CreatableSentinel | null>(() => {
|
|
251
|
+
if (!creatableProp) return null;
|
|
252
|
+
const trimmed = currentInputValue.trim();
|
|
253
|
+
if (!trimmed) return null;
|
|
254
|
+
const lowered = trimmed.toLocaleLowerCase();
|
|
255
|
+
const exactExists = data.some(
|
|
256
|
+
(d) => itemToString(d).trim().toLocaleLowerCase() === lowered
|
|
257
|
+
);
|
|
258
|
+
if (exactExists) return null;
|
|
259
|
+
return makeCreatableSentinel(trimmed);
|
|
260
|
+
}, [creatableProp, currentInputValue, data, itemToString]);
|
|
261
|
+
|
|
229
262
|
// ─── Sentinel flat list ───────────────────────────────────────────────────
|
|
230
263
|
// Used when selectHeadings or selectAll. selectAll implies selectHeadings —
|
|
231
264
|
// group headers are always selectable when the list is flattened.
|
|
232
265
|
|
|
233
|
-
type FlatRow =
|
|
266
|
+
type FlatRow =
|
|
267
|
+
| T
|
|
268
|
+
| GroupHeaderSentinel
|
|
269
|
+
| SelectAllSentinel
|
|
270
|
+
| CreatableSentinel;
|
|
234
271
|
|
|
235
272
|
const flatItems = React.useMemo<FlatRow[] | null>(() => {
|
|
236
273
|
if (!selectHeadings && !selectAll) return null;
|
|
@@ -255,6 +292,22 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
255
292
|
}
|
|
256
293
|
|
|
257
294
|
function handleValueChange(next: FlatRow[]) {
|
|
295
|
+
const creatableClicked = next.find(isCreatableSentinel);
|
|
296
|
+
if (creatableClicked) {
|
|
297
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
298
|
+
const realItems = next.filter(
|
|
299
|
+
(v): v is T =>
|
|
300
|
+
!isGroupHeaderSentinel(v) &&
|
|
301
|
+
!isSelectAllSentinel(v) &&
|
|
302
|
+
!isCreatableSentinel(v)
|
|
303
|
+
);
|
|
304
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
305
|
+
skipNextInputChangeRef.current = true;
|
|
306
|
+
setInternalInputValue("");
|
|
307
|
+
onInputValueChange?.("");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
258
311
|
const selectAllClicked = next.filter(isSelectAllSentinel);
|
|
259
312
|
const groupSentinels = next.filter(isGroupHeaderSentinel);
|
|
260
313
|
const realItems = next.filter(
|
|
@@ -295,8 +348,18 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
295
348
|
setSelectedItems(updated);
|
|
296
349
|
}
|
|
297
350
|
|
|
298
|
-
function handleSimpleValueChange(newItems:
|
|
299
|
-
|
|
351
|
+
function handleSimpleValueChange(newItems: FlatRow[]) {
|
|
352
|
+
const creatableClicked = newItems.find(isCreatableSentinel);
|
|
353
|
+
if (creatableClicked) {
|
|
354
|
+
const newItem = onCreateProp?.(creatableClicked.label);
|
|
355
|
+
const realItems = newItems.filter((v): v is T => !isCreatableSentinel(v));
|
|
356
|
+
setSelectedItems(newItem ? [...realItems, newItem] : realItems);
|
|
357
|
+
skipNextInputChangeRef.current = true;
|
|
358
|
+
setInternalInputValue("");
|
|
359
|
+
onInputValueChange?.("");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
setSelectedItems(newItems as T[]);
|
|
300
363
|
}
|
|
301
364
|
|
|
302
365
|
function removeItem(item: T, e: React.MouseEvent) {
|
|
@@ -350,6 +413,10 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
350
413
|
);
|
|
351
414
|
}
|
|
352
415
|
|
|
416
|
+
if (isCreatableSentinel(row)) {
|
|
417
|
+
return renderCreatableItem(row);
|
|
418
|
+
}
|
|
419
|
+
|
|
353
420
|
const item = row as T;
|
|
354
421
|
return (
|
|
355
422
|
<ComboboxItem
|
|
@@ -366,6 +433,33 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
366
433
|
);
|
|
367
434
|
}
|
|
368
435
|
|
|
436
|
+
function renderCreatableItem(
|
|
437
|
+
sentinel: CreatableSentinel,
|
|
438
|
+
style?: React.CSSProperties,
|
|
439
|
+
index?: number,
|
|
440
|
+
measureRef?: (el: Element | null) => void
|
|
441
|
+
) {
|
|
442
|
+
return (
|
|
443
|
+
<ComboboxItem
|
|
444
|
+
key="__creatable"
|
|
445
|
+
value={sentinel}
|
|
446
|
+
itemId="__creatable"
|
|
447
|
+
label={`Create "${sentinel.label}"`}
|
|
448
|
+
inputType="none"
|
|
449
|
+
renderItem={() => (
|
|
450
|
+
<>
|
|
451
|
+
<Icon name="plus-outline" size="mini" />
|
|
452
|
+
{`Create "${sentinel.label}"`}
|
|
453
|
+
</>
|
|
454
|
+
)}
|
|
455
|
+
style={style}
|
|
456
|
+
index={index}
|
|
457
|
+
data-index={index}
|
|
458
|
+
ref={measureRef}
|
|
459
|
+
/>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
369
463
|
// ─── Items passed to Combobox.Root ────────────────────────────────────────
|
|
370
464
|
// When selectHeadings: flatItems (flat array of group header sentinels + items).
|
|
371
465
|
// When selectAll only: prepend SELECT_ALL_SENTINEL to data so Base UI tracks
|
|
@@ -373,8 +467,12 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
373
467
|
// The sentinel is never in flatItems — it is always rendered manually above
|
|
374
468
|
// Combobox.List, so we must not prepend it when flatItems is used.
|
|
375
469
|
|
|
376
|
-
const
|
|
470
|
+
const baseItems =
|
|
377
471
|
flatItems ?? groups ?? (isChildrenMode ? undefined : visibleData);
|
|
472
|
+
const rootItems =
|
|
473
|
+
creatableSentinel && Array.isArray(baseItems)
|
|
474
|
+
? ([...baseItems, creatableSentinel] as FlatRow[])
|
|
475
|
+
: baseItems;
|
|
378
476
|
|
|
379
477
|
const hasSentinels = selectAll || selectHeadings;
|
|
380
478
|
|
|
@@ -395,9 +493,41 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
395
493
|
if (!nextOpen && eventDetails.reason === "item-press") return;
|
|
396
494
|
setOpen(nextOpen);
|
|
397
495
|
}}
|
|
398
|
-
filter={
|
|
399
|
-
|
|
400
|
-
|
|
496
|
+
filter={
|
|
497
|
+
creatableProp
|
|
498
|
+
? (item: unknown, query: string) => {
|
|
499
|
+
if (isCreatableSentinel(item)) return true;
|
|
500
|
+
if (filter) return filter(item, query);
|
|
501
|
+
// Default Base UI filter: match against itemToStringLabel
|
|
502
|
+
if (isGroupHeaderSentinel(item) || isSelectAllSentinel(item))
|
|
503
|
+
return true;
|
|
504
|
+
const label = itemToString(item as T);
|
|
505
|
+
return label.toLowerCase().includes(query.toLowerCase());
|
|
506
|
+
}
|
|
507
|
+
: filter
|
|
508
|
+
}
|
|
509
|
+
inputValue={
|
|
510
|
+
creatableProp ? currentInputValue : inputValue ?? internalInputValue
|
|
511
|
+
}
|
|
512
|
+
onInputValueChange={(v, eventDetails) => {
|
|
513
|
+
// Preserve search query when an item is selected. Base UI fires
|
|
514
|
+
// 'input-clear' on item-press; ignore it while the popup stays open.
|
|
515
|
+
if (eventDetails.reason === "input-clear" && open) return;
|
|
516
|
+
|
|
517
|
+
if (creatableProp) {
|
|
518
|
+
if (skipNextInputChangeRef.current) {
|
|
519
|
+
skipNextInputChangeRef.current = false;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
setInternalInputValue(v);
|
|
523
|
+
onInputValueChange?.(v);
|
|
524
|
+
} else {
|
|
525
|
+
if (inputValue === undefined) {
|
|
526
|
+
setInternalInputValue(v);
|
|
527
|
+
}
|
|
528
|
+
onInputValueChange?.(v);
|
|
529
|
+
}
|
|
530
|
+
}}
|
|
401
531
|
onItemHighlighted={
|
|
402
532
|
isVirtualized
|
|
403
533
|
? (item, { reason, index }) => {
|
|
@@ -423,20 +553,31 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
423
553
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
554
|
(hasSentinels ? handleValueChange : handleSimpleValueChange) as any
|
|
425
555
|
}
|
|
426
|
-
|
|
427
|
-
|
|
556
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
557
|
+
itemToStringLabel={(item: any) => {
|
|
558
|
+
if (
|
|
559
|
+
!item ||
|
|
560
|
+
isGroupHeaderSentinel(item) ||
|
|
561
|
+
isSelectAllSentinel(item) ||
|
|
562
|
+
isCreatableSentinel(item)
|
|
563
|
+
)
|
|
428
564
|
return "";
|
|
429
565
|
return itemToString(item as T);
|
|
430
566
|
}}
|
|
431
|
-
|
|
567
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
568
|
+
isItemEqualToValue={(a: any, b: any) => {
|
|
569
|
+
if (isCreatableSentinel(a) && isCreatableSentinel(b))
|
|
570
|
+
return a.label === b.label;
|
|
432
571
|
if (isGroupHeaderSentinel(a) && isGroupHeaderSentinel(b))
|
|
433
572
|
return a.groupName === b.groupName;
|
|
434
573
|
if (isSelectAllSentinel(a) && isSelectAllSentinel(b)) return true;
|
|
435
574
|
if (
|
|
436
575
|
!isGroupHeaderSentinel(a) &&
|
|
437
576
|
!isSelectAllSentinel(a) &&
|
|
577
|
+
!isCreatableSentinel(a) &&
|
|
438
578
|
!isGroupHeaderSentinel(b) &&
|
|
439
|
-
!isSelectAllSentinel(b)
|
|
579
|
+
!isSelectAllSentinel(b) &&
|
|
580
|
+
!isCreatableSentinel(b)
|
|
440
581
|
)
|
|
441
582
|
return (a as T).id === (b as T).id;
|
|
442
583
|
return false;
|
|
@@ -475,6 +616,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
475
616
|
<ComboboxTokenInput
|
|
476
617
|
id={id}
|
|
477
618
|
placeholder={value.length > 0 ? "" : placeholder}
|
|
619
|
+
aria-describedby={ariaDescribedBy}
|
|
478
620
|
/>
|
|
479
621
|
<ComboboxActionButtons>
|
|
480
622
|
<ComboboxClear aria-label="Clear all">
|
|
@@ -525,57 +667,83 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
525
667
|
inputType={inputType}
|
|
526
668
|
renderItem={renderItem}
|
|
527
669
|
renderGroupHeading={renderGroupHeading}
|
|
670
|
+
renderCreatableItem={
|
|
671
|
+
creatableProp
|
|
672
|
+
? (sentinel, style, index, measureRef) =>
|
|
673
|
+
renderCreatableItem(
|
|
674
|
+
sentinel,
|
|
675
|
+
style,
|
|
676
|
+
index,
|
|
677
|
+
measureRef
|
|
678
|
+
)
|
|
679
|
+
: undefined
|
|
680
|
+
}
|
|
528
681
|
/>
|
|
529
682
|
) : children ? (
|
|
530
683
|
children
|
|
531
684
|
) : flatItems ? (
|
|
532
685
|
(row: FlatRow) => renderSentinelRow(row)
|
|
533
686
|
) : groups ? (
|
|
534
|
-
(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
687
|
+
(
|
|
688
|
+
group: { value: string; items: T[] } | CreatableSentinel,
|
|
689
|
+
index: number
|
|
690
|
+
) => {
|
|
691
|
+
if (isCreatableSentinel(group)) {
|
|
692
|
+
return renderCreatableItem(group);
|
|
693
|
+
}
|
|
694
|
+
const g = group as { value: string; items: T[] };
|
|
695
|
+
return (
|
|
696
|
+
<React.Fragment key={g.value}>
|
|
697
|
+
<ComboboxGroup items={g.items}>
|
|
698
|
+
<ComboboxGroupLabel>
|
|
699
|
+
{renderGroupHeading
|
|
700
|
+
? renderGroupHeading(g.value)
|
|
701
|
+
: g.value}
|
|
702
|
+
</ComboboxGroupLabel>
|
|
703
|
+
<Combobox.Collection>
|
|
704
|
+
{(item: T) => (
|
|
705
|
+
<ComboboxItem
|
|
706
|
+
key={item.id}
|
|
707
|
+
value={item}
|
|
708
|
+
itemId={String(item.id)}
|
|
709
|
+
label={itemToString(item)}
|
|
710
|
+
disabled={item.disabled}
|
|
711
|
+
inputType={inputType}
|
|
712
|
+
renderItem={
|
|
713
|
+
renderItem
|
|
714
|
+
? () => renderItem(item)
|
|
715
|
+
: () => itemToString(item)
|
|
716
|
+
}
|
|
717
|
+
/>
|
|
718
|
+
)}
|
|
719
|
+
</Combobox.Collection>
|
|
720
|
+
</ComboboxGroup>
|
|
721
|
+
{index < groups.length - 1 && <ComboboxSeparator />}
|
|
722
|
+
</React.Fragment>
|
|
723
|
+
);
|
|
724
|
+
}
|
|
563
725
|
) : (
|
|
564
|
-
(item: T) =>
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
726
|
+
(item: T | CreatableSentinel) => {
|
|
727
|
+
if (isCreatableSentinel(item)) {
|
|
728
|
+
return renderCreatableItem(item);
|
|
729
|
+
}
|
|
730
|
+
const dataItem = item as T;
|
|
731
|
+
return (
|
|
732
|
+
<ComboboxItem
|
|
733
|
+
key={dataItem.id}
|
|
734
|
+
value={dataItem}
|
|
735
|
+
itemId={String(dataItem.id)}
|
|
736
|
+
label={itemToString(dataItem)}
|
|
737
|
+
disabled={dataItem.disabled}
|
|
738
|
+
inputType={inputType}
|
|
739
|
+
renderItem={
|
|
740
|
+
renderItem
|
|
741
|
+
? () => renderItem(dataItem)
|
|
742
|
+
: () => itemToString(dataItem)
|
|
743
|
+
}
|
|
744
|
+
/>
|
|
745
|
+
);
|
|
746
|
+
}
|
|
579
747
|
)}
|
|
580
748
|
</ComboboxList>
|
|
581
749
|
</ComboboxPopup>
|