@sproutsocial/seeds-react-menu 1.17.0 → 1.18.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.
Files changed (59) hide show
  1. package/.turbo/turbo-build.log +14 -22
  2. package/CHANGELOG.md +30 -0
  3. package/dist/esm/index.js +542 -229
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/v3/index.js +793 -1038
  6. package/dist/esm/v3/index.js.map +1 -1
  7. package/dist/v3/index.d.mts +64 -38
  8. package/dist/v3/index.d.ts +64 -38
  9. package/dist/v3/index.js +813 -1058
  10. package/dist/v3/index.js.map +1 -1
  11. package/dist/v3/menu.css +947 -0
  12. package/package.json +20 -21
  13. package/src/v3/ActionMenuStyles.tsx +169 -149
  14. package/src/v3/Common/ComboboxStyles.tsx +396 -511
  15. package/src/v3/Common/SelectIcons.tsx +17 -8
  16. package/src/v3/Common/SelectItem.tsx +25 -38
  17. package/src/v3/Common/SelectStyles.tsx +146 -144
  18. package/src/v3/Common/cn.ts +38 -0
  19. package/src/v3/MenuButtonStyles.tsx +55 -34
  20. package/src/v3/MultiSearchableSelect.tsx +8 -8
  21. package/src/v3/MultiSelect.tsx +11 -16
  22. package/src/v3/SingleSelect.tsx +13 -13
  23. package/src/v3/menu.css +947 -0
  24. package/tsup.config.ts +0 -1
  25. package/dist/esm/chunk-ROQATIB7.js +0 -357
  26. package/dist/esm/chunk-ROQATIB7.js.map +0 -1
  27. package/dist/esm/v2/index.js +0 -2089
  28. package/dist/esm/v2/index.js.map +0 -1
  29. package/dist/v2/index.d.mts +0 -108
  30. package/dist/v2/index.d.ts +0 -108
  31. package/dist/v2/index.js +0 -2280
  32. package/dist/v2/index.js.map +0 -1
  33. package/src/v2/Autocomplete/Autocomplete.stories.tsx +0 -645
  34. package/src/v2/Autocomplete/MultiAutocomplete.tsx +0 -487
  35. package/src/v2/Autocomplete/SingleAutocomplete.tsx +0 -366
  36. package/src/v2/Autocomplete/index.ts +0 -2
  37. package/src/v2/Common-V2/ComboboxContent.tsx +0 -457
  38. package/src/v2/Common-V2/MenuGroup.tsx +0 -60
  39. package/src/v2/Common-V2/MenuGroupTypes.ts +0 -30
  40. package/src/v2/Common-V2/MenuHeading.tsx +0 -83
  41. package/src/v2/Common-V2/MenuItem.tsx +0 -138
  42. package/src/v2/Common-V2/Popout.tsx +0 -124
  43. package/src/v2/Common-V2/PopoutTypes.tsx +0 -109
  44. package/src/v2/Common-V2/SelectToggleButton.tsx +0 -99
  45. package/src/v2/Common-V2/index.ts +0 -11
  46. package/src/v2/Common-V2/props.ts +0 -84
  47. package/src/v2/Common-V2/styles.tsx +0 -41
  48. package/src/v2/Common-V2/types.ts +0 -16
  49. package/src/v2/Common-V2/useHighlightedIndex.ts +0 -62
  50. package/src/v2/Common-V2/utils.ts +0 -238
  51. package/src/v2/SearchableSelect/AutocompleteContent.tsx +0 -325
  52. package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +0 -527
  53. package/src/v2/SearchableSelect/SearchableSelect.tsx +0 -395
  54. package/src/v2/SearchableSelect/index.ts +0 -12
  55. package/src/v2/Select/MultiSelect.tsx +0 -417
  56. package/src/v2/Select/Select.stories.tsx +0 -593
  57. package/src/v2/Select/SingleSelect.tsx +0 -285
  58. package/src/v2/Select/index.ts +0 -2
  59. package/src/v2/index.ts +0 -2
@@ -1,487 +0,0 @@
1
- import { useState, useMemo, useRef } from "react";
2
- import { useCombobox, useMultipleSelection } from "downshift";
3
- import { TokenInput } from "@sproutsocial/seeds-react-token-input";
4
- // This will eventually be imported from seeds-react-popout
5
- import { Popout } from "../Common-V2";
6
- import {
7
- isGroupHeading,
8
- isToggleItem,
9
- menuAnimationConfig,
10
- getFilteredItems,
11
- groupItemsWithHeadings,
12
- useHighlightedIndex,
13
- createItemToStringForDownshift,
14
- ComboboxContent,
15
- } from "../Common-V2";
16
- import {
17
- type DataWithId,
18
- type GroupedItem,
19
- type ToggleItem,
20
- type GroupHeading,
21
- type BaseMultiAutocompleteProps,
22
- } from "../Common-V2";
23
-
24
- function defaultReducer(state, actionAndChanges) {
25
- const { changes, type } = actionAndChanges;
26
-
27
- switch (type) {
28
- case useCombobox.stateChangeTypes.InputBlur:
29
- // Do not close the menu on blur from this event
30
- // There is a very brief blur when clicking an item, even though the input seemingly does not lose focus
31
- // Closing the menu is handled in the onBlur handler instead, which does not have this issue
32
- return {
33
- ...changes,
34
- isOpen: true,
35
- };
36
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
37
- case useCombobox.stateChangeTypes.ItemClick:
38
- return {
39
- ...changes,
40
- isOpen: true, // keep the menu open after selection.
41
- highlightedIndex: state.highlightedIndex, // preserve the current position so the list doesn't jump.
42
- };
43
- default:
44
- return changes;
45
- }
46
- }
47
-
48
- export interface MultiAutocompleteProps<T extends DataWithId>
49
- extends BaseMultiAutocompleteProps<T> {}
50
-
51
- export function MultiAutocomplete<T extends DataWithId>({
52
- id,
53
- data,
54
- selectedItemIds: controlledSelectedItemIds,
55
- inputValue: controlledInputValue,
56
- isOpen: controlledIsOpen,
57
- itemToKey = (item: T) => item?.id,
58
- itemToString,
59
- itemToSearchString,
60
- renderItem,
61
- groupBy,
62
- renderGroupHeading,
63
- isVirtualized = false,
64
- estimatedItemHeight,
65
- showSelectedItems = false,
66
- selectHeadings = false,
67
- selectAllItem,
68
- EmptyState,
69
- placeholder,
70
- onSelectedItemIdsChange,
71
- onInputValueChange,
72
- onIsOpenChange,
73
- stateReducer = defaultReducer,
74
- menuProps,
75
- inputType = "checkbox",
76
- onPointerDownOutside,
77
- onInteractOutside,
78
- popoutPlacement,
79
- }: MultiAutocompleteProps<T>) {
80
- const mousedownInPopoverRef = useRef(false);
81
-
82
- const [uncontrolledInputValue, setUncontrolledInputValue] =
83
- useState<string>("");
84
- const [uncontrolledSelectedItemIds, setUncontrolledSelectedItemIds] =
85
- useState<T["id"][]>([]);
86
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
87
-
88
- // Use controlled selectedItemIds if provided, otherwise use internal state
89
- const isSelectedItemIdsControlled = controlledSelectedItemIds !== undefined;
90
- const selectedItemIds = isSelectedItemIdsControlled
91
- ? controlledSelectedItemIds
92
- : uncontrolledSelectedItemIds;
93
-
94
- // Convert selectedItemIds to selectedItems for internal use
95
- const selectedItems = useMemo<T[]>(() => {
96
- return data.filter((item) => selectedItemIds.includes(itemToKey(item)));
97
- }, [selectedItemIds, data, itemToKey]);
98
-
99
- // Use controlled inputValue if provided, otherwise use internal state
100
- const isInputValueControlled = controlledInputValue !== undefined;
101
- const inputValue = isInputValueControlled
102
- ? controlledInputValue
103
- : uncontrolledInputValue;
104
-
105
- // Use controlled isOpen if provided, otherwise use internal state
106
- const isIsOpenControlled = controlledIsOpen !== undefined;
107
- const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
108
-
109
- const updateSelectedItems = (newSelectedItems: T[]) => {
110
- const newSelectedItemIds = newSelectedItems.map((item) => itemToKey(item));
111
- if (!isSelectedItemIdsControlled) {
112
- setUncontrolledSelectedItemIds(newSelectedItemIds);
113
- }
114
- onSelectedItemIdsChange?.(newSelectedItemIds);
115
- };
116
-
117
- const updateInputValue = (newInputValue: string) => {
118
- if (!isInputValueControlled) {
119
- setUncontrolledInputValue(newInputValue);
120
- }
121
- onInputValueChange?.(newInputValue);
122
- };
123
-
124
- const updateIsOpen = (newIsOpen: boolean) => {
125
- if (!newIsOpen) {
126
- updateInputValue("");
127
- }
128
- if (!isIsOpenControlled) {
129
- setUncontrolledIsOpen(newIsOpen);
130
- }
131
- onIsOpenChange?.(newIsOpen);
132
- };
133
-
134
- // Use itemToSearchString if provided, otherwise fallback to itemToString
135
- const searchStringFn =
136
- itemToSearchString ?? ((item: T) => itemToString(item));
137
-
138
- const filteredItems = useMemo<T[]>(
139
- () =>
140
- getFilteredItems(data, inputValue, searchStringFn, itemToString, {
141
- selectedItems,
142
- showSelectedItems,
143
- }),
144
- [data, selectedItems, inputValue, searchStringFn, showSelectedItems]
145
- );
146
-
147
- const items = useMemo<GroupedItem<T>[]>(() => {
148
- let listItems: GroupedItem<T>[];
149
-
150
- // If groupBy is provided and selectHeadings is true we need to add group heading items to the list
151
- if (groupBy) {
152
- // Group first, then add selectAllItem at the beginning
153
- // If headings are not selectable we will just reorder the items
154
- // If headings are selectable we will add them to the list
155
- listItems = groupItemsWithHeadings(
156
- filteredItems,
157
- groupBy,
158
- data,
159
- selectHeadings
160
- );
161
- } else {
162
- listItems = filteredItems;
163
- }
164
-
165
- // Add selectAllItem at the beginning if provided
166
- if (selectAllItem) {
167
- return [selectAllItem, ...listItems];
168
- }
169
-
170
- return listItems;
171
- }, [filteredItems, groupBy, selectAllItem, selectHeadings, data]);
172
-
173
- // Handler for heading clicks - selects/deselects all items in a group
174
- const handleHeadingClick = useMemo(() => {
175
- if (!selectHeadings || !groupBy) {
176
- return undefined;
177
- }
178
-
179
- return (groupLabel: string, allSelected: boolean) => {
180
- // Find all items in this group from the full data array
181
- const groupItems = data.filter((item) => groupBy(item) === groupLabel);
182
-
183
- if (allSelected) {
184
- // Deselect all items in the group
185
- const newSelectedItems = selectedItems.filter(
186
- (item) => !groupItems.includes(item)
187
- );
188
- updateSelectedItems(newSelectedItems);
189
- } else {
190
- // Select all items in the group (add them if not already selected)
191
- const newSelectedItems = [...selectedItems];
192
- for (const groupItem of groupItems) {
193
- if (!newSelectedItems.includes(groupItem)) {
194
- newSelectedItems.push(groupItem);
195
- }
196
- }
197
- updateSelectedItems(newSelectedItems);
198
- }
199
- };
200
- }, [selectHeadings, groupBy, data, selectedItems, updateSelectedItems]);
201
-
202
- const { getSelectedItemProps, getDropdownProps, removeSelectedItem } =
203
- useMultipleSelection<T>({
204
- selectedItems,
205
- onStateChange({ selectedItems: newSelectedItems, type }) {
206
- switch (type) {
207
- case useMultipleSelection.stateChangeTypes
208
- .SelectedItemKeyDownBackspace:
209
- case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
210
- case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
211
- case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
212
- if (newSelectedItems !== undefined) {
213
- updateSelectedItems(newSelectedItems);
214
- }
215
- break;
216
- default:
217
- break;
218
- }
219
- },
220
- });
221
-
222
- // Filter out headings for downshift unless selectHeadings is true
223
- const itemsForCombobox = useMemo<(T | ToggleItem | GroupHeading)[]>(() => {
224
- if (selectHeadings) {
225
- // Include all items (headings, toggle items, and data items)
226
- return items;
227
- }
228
- return items.filter(
229
- (item): item is T | ToggleItem => !isGroupHeading(item)
230
- );
231
- }, [items, selectHeadings]);
232
-
233
- // Wrapper function to handle ToggleItem and GroupHeading separately
234
- const itemToStringForDownshift = useMemo(
235
- () => createItemToStringForDownshift(itemToString),
236
- [itemToString]
237
- );
238
-
239
- const comboboxProps = useCombobox<T | ToggleItem | GroupHeading>({
240
- inputId: id,
241
- items: itemsForCombobox,
242
- itemToString: itemToStringForDownshift,
243
- defaultHighlightedIndex: -1,
244
- selectedItem: null,
245
- inputValue,
246
- isOpen,
247
- stateReducer,
248
- // Could maybe let devs override this too?
249
- onStateChange({
250
- inputValue: newInputValue,
251
- type,
252
- selectedItem: newSelectedItem,
253
- isOpen: newIsOpen,
254
- }) {
255
- // Handle isOpen changes for any state change type
256
- if (newIsOpen !== undefined) {
257
- updateIsOpen(newIsOpen);
258
- }
259
-
260
- switch (type) {
261
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
262
- case useCombobox.stateChangeTypes.ItemClick:
263
- if (newSelectedItem) {
264
- // Handle selectAllItem selection
265
- if (
266
- selectAllItem &&
267
- isToggleItem(newSelectedItem) &&
268
- newSelectedItem.id === selectAllItem.id
269
- ) {
270
- // Check if all items are selected (excluding selectAllItem itself)
271
- const allSelected =
272
- data.length > 0 &&
273
- data.every((item) => selectedItems.includes(item));
274
- if (allSelected) {
275
- // Deselect all items
276
- updateSelectedItems([]);
277
- } else {
278
- // Select all items
279
- updateSelectedItems([...data]);
280
- }
281
- } else if (
282
- selectHeadings &&
283
- groupBy &&
284
- isGroupHeading(newSelectedItem)
285
- ) {
286
- // Handle heading selection - select/deselect all items in the group
287
- const groupLabel = newSelectedItem.label;
288
- const groupItems = data.filter(
289
- (item) => groupBy(item) === groupLabel
290
- );
291
- const allSelected =
292
- groupItems.length > 0 &&
293
- groupItems.every((item) => selectedItems.includes(item));
294
-
295
- if (allSelected) {
296
- // Deselect all items in the group
297
- updateSelectedItems(
298
- selectedItems.filter((item) => !groupItems.includes(item))
299
- );
300
- } else {
301
- // Select all items in the group
302
- const newItems = [
303
- ...selectedItems.filter((item) => !groupItems.includes(item)),
304
- ...groupItems,
305
- ];
306
- updateSelectedItems(newItems);
307
- }
308
- } else if (
309
- !isGroupHeading(newSelectedItem) &&
310
- !isToggleItem(newSelectedItem)
311
- ) {
312
- // Only handle regular data items
313
- const itemIndex = selectedItems.findIndex(
314
- (item) => itemToKey(item) === itemToKey(newSelectedItem as T)
315
- );
316
- if (itemIndex >= 0) {
317
- updateSelectedItems(
318
- selectedItems.filter((_, index) => index !== itemIndex)
319
- );
320
- } else {
321
- updateSelectedItems([...selectedItems, newSelectedItem as T]);
322
- }
323
- }
324
- }
325
- break;
326
-
327
- case useCombobox.stateChangeTypes.InputChange:
328
- updateInputValue(newInputValue ?? "");
329
- break;
330
-
331
- default:
332
- break;
333
- }
334
- },
335
- });
336
- const {
337
- isOpen: comboboxIsOpen,
338
- getToggleButtonProps,
339
- getLabelProps,
340
- getMenuProps,
341
- getInputProps,
342
- highlightedIndex: comboboxHighlightedIndex,
343
- getItemProps,
344
- selectedItem,
345
- } = comboboxProps;
346
-
347
- // Map the highlighted index from combobox (which only knows about items) to the full items array (which includes headings)
348
- const highlightedIndex = useHighlightedIndex(
349
- items,
350
- itemsForCombobox,
351
- comboboxHighlightedIndex,
352
- !!groupBy,
353
- selectHeadings ?? false
354
- );
355
-
356
- const inputProps = getInputProps({ preventKeyAction: isOpen });
357
- // Downshift adds aria-labelledby by default, which can conflict with our Label usage
358
- inputProps["aria-labelledby"] = undefined;
359
- if (!comboboxIsOpen) {
360
- // Remove aria-controls when the menu is closed to avoid referencing a non-existent element
361
- // @ts-expect-error downshift types require aria-controls even though it is not required
362
- inputProps["aria-controls"] = undefined;
363
- }
364
-
365
- const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
366
- // Call any provided onBlur handler
367
- if (inputProps.onBlur) {
368
- inputProps.onBlur(e);
369
- }
370
- // Don't close if the blur was caused by a mousedown inside the popover (e.g. clicking the scrollbar)
371
- // Re-focus the input so future blur events can still close the menu
372
- if (mousedownInPopoverRef.current) {
373
- mousedownInPopoverRef.current = false;
374
- e.target.focus();
375
- return;
376
- }
377
- // Need to figure why onblur is getting called when selecting select all item
378
- updateIsOpen(false);
379
- };
380
-
381
- const inputPropsToSpread = { ...inputProps };
382
- // Remove onBlur from inputProps
383
- // The blur event needs added to the root of token input
384
- delete inputPropsToSpread.onBlur;
385
-
386
- return (
387
- <Popout
388
- open={comboboxIsOpen}
389
- onOpenChange={updateIsOpen}
390
- side={popoutPlacement}
391
- onOpenAutoFocus={(e) => {
392
- // Prevent default auto-focus behavior
393
- e.preventDefault();
394
- }}
395
- onCloseAutoFocus={(e) => {
396
- // Prevent default auto-focus behavior
397
- e.preventDefault();
398
- }}
399
- onPointerDownOutside={onPointerDownOutside}
400
- onInteractOutside={onInteractOutside}
401
- animationConfig={menuAnimationConfig}
402
- onMouseDown={() => {
403
- mousedownInPopoverRef.current = true;
404
- }}
405
- onMouseUp={() => {
406
- mousedownInPopoverRef.current = false;
407
- }}
408
- content={
409
- <ComboboxContent
410
- items={items}
411
- getMenuProps={getMenuProps}
412
- getItemProps={(props: { index: number }) => {
413
- // Only apply getItemProps to actual items, not headings or ToggleItems
414
- const item = items[props.index];
415
- if (isToggleItem(item)) {
416
- // Handle ToggleItem clicks manually
417
- return {
418
- onClick: () => {
419
- const allSelected =
420
- data.length > 0 &&
421
- data.every((item) => selectedItems.includes(item));
422
- if (allSelected) {
423
- updateSelectedItems([]);
424
- } else {
425
- updateSelectedItems([...data]);
426
- }
427
- },
428
- };
429
- }
430
- const dataItem = item as T;
431
- // Find the index in itemsForCombobox by comparing keys
432
- const itemIndex = itemsForCombobox.indexOf(dataItem);
433
- if (itemIndex === -1) {
434
- return {};
435
- }
436
- return getItemProps({ item: dataItem, index: itemIndex });
437
- }}
438
- selectedItems={selectedItems}
439
- highlightedIndex={highlightedIndex ?? -1}
440
- renderItem={renderItem}
441
- renderGroupHeading={renderGroupHeading}
442
- EmptyState={EmptyState}
443
- itemToKey={itemToKey}
444
- itemToString={itemToString}
445
- inputType={inputType}
446
- selectHeadings={selectHeadings}
447
- onHeadingClick={handleHeadingClick}
448
- groupBy={groupBy}
449
- allData={data}
450
- selectAllItem={selectAllItem}
451
- isVirtualized={isVirtualized}
452
- estimatedItemHeight={estimatedItemHeight}
453
- {...menuProps}
454
- />
455
- }
456
- >
457
- <TokenInput
458
- placeholder={placeholder}
459
- inputProps={{
460
- ...inputPropsToSpread,
461
- autoComplete: "off", // Override with "off" to prevent browser autocomplete
462
- }}
463
- tokens={selectedItems.map((item) => ({
464
- id: String(itemToKey(item)),
465
- value: itemToString(item),
466
- }))}
467
- onRemoveToken={(tokenId) => {
468
- const itemToRemove = selectedItems.find(
469
- (item) => String(itemToKey(item)) === tokenId
470
- );
471
- if (itemToRemove) {
472
- removeSelectedItem(itemToRemove);
473
- }
474
- }}
475
- onClickToken={(e) => {
476
- e.stopPropagation();
477
- }}
478
- onBlur={handleBlur}
479
- aria-controls={undefined}
480
- aria-expanded={undefined}
481
- aria-haspopup={undefined}
482
- // @ts-expect-error Popout adds the type=button prop but TokenInput does not expect that
483
- type={undefined}
484
- />
485
- </Popout>
486
- );
487
- }