@sproutsocial/seeds-react-menu 1.16.16 → 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 (84) hide show
  1. package/.turbo/turbo-build.log +14 -22
  2. package/CHANGELOG.md +51 -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 +870 -1081
  6. package/dist/esm/v3/index.js.map +1 -1
  7. package/dist/v3/index.d.mts +82 -38
  8. package/dist/v3/index.d.ts +82 -38
  9. package/dist/v3/index.js +887 -1098
  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/ActionMenu/ActionMenu.stories.tsx +1 -1
  14. package/src/Autocomplete/Autocomplete.stories.tsx +1 -1
  15. package/src/MenuContent/MenuContent.stories.tsx +1 -1
  16. package/src/MenuFooter/MenuFooter.stories.tsx +1 -1
  17. package/src/MenuGroup/MenuGroup.stories.tsx +1 -1
  18. package/src/MenuHeader/MenuHeader.stories.tsx +1 -1
  19. package/src/MenuItem/MenuItem.stories.tsx +1 -1
  20. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +1 -1
  21. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +1 -1
  22. package/src/NestedMenu/NestedMenu.stories.tsx +1 -1
  23. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +1 -1
  24. package/src/SubmenuItem/SubmenuItem.stories.tsx +1 -1
  25. package/src/v3/ActionMenu.stories.tsx +1 -1
  26. package/src/v3/ActionMenuStyles.tsx +169 -149
  27. package/src/v3/Common/ComboboxStyles.tsx +396 -511
  28. package/src/v3/Common/SelectIcons.tsx +17 -8
  29. package/src/v3/Common/SelectItem.tsx +25 -38
  30. package/src/v3/Common/SelectStyles.tsx +146 -144
  31. package/src/v3/Common/cn.ts +38 -0
  32. package/src/v3/MenuButton.stories.tsx +1 -1
  33. package/src/v3/MenuButtonStyles.tsx +55 -34
  34. package/src/v3/ModalStories.stories.tsx +1 -1
  35. package/src/v3/MultiCombobox.stories.tsx +1 -1
  36. package/src/v3/MultiCombobox.tsx +7 -0
  37. package/src/v3/MultiSearchableSelect.stories.tsx +1 -1
  38. package/src/v3/MultiSearchableSelect.tsx +15 -8
  39. package/src/v3/MultiSelect.stories.tsx +1 -1
  40. package/src/v3/MultiSelect.tsx +20 -17
  41. package/src/v3/SingleCombobox.stories.tsx +1 -1
  42. package/src/v3/SingleCombobox.tsx +7 -0
  43. package/src/v3/SingleSearchableSelect.stories.tsx +1 -1
  44. package/src/v3/SingleSearchableSelect.tsx +7 -0
  45. package/src/v3/SingleSelect.stories.tsx +1 -1
  46. package/src/v3/SingleSelect.tsx +22 -14
  47. package/src/v3/__tests__/ariaLabels.test.tsx +133 -0
  48. package/src/v3/menu.css +947 -0
  49. package/tsup.config.ts +0 -1
  50. package/dist/esm/chunk-ROQATIB7.js +0 -357
  51. package/dist/esm/chunk-ROQATIB7.js.map +0 -1
  52. package/dist/esm/v2/index.js +0 -2089
  53. package/dist/esm/v2/index.js.map +0 -1
  54. package/dist/v2/index.d.mts +0 -108
  55. package/dist/v2/index.d.ts +0 -108
  56. package/dist/v2/index.js +0 -2280
  57. package/dist/v2/index.js.map +0 -1
  58. package/src/v2/Autocomplete/Autocomplete.stories.tsx +0 -645
  59. package/src/v2/Autocomplete/MultiAutocomplete.tsx +0 -487
  60. package/src/v2/Autocomplete/SingleAutocomplete.tsx +0 -366
  61. package/src/v2/Autocomplete/index.ts +0 -2
  62. package/src/v2/Common-V2/ComboboxContent.tsx +0 -457
  63. package/src/v2/Common-V2/MenuGroup.tsx +0 -60
  64. package/src/v2/Common-V2/MenuGroupTypes.ts +0 -30
  65. package/src/v2/Common-V2/MenuHeading.tsx +0 -83
  66. package/src/v2/Common-V2/MenuItem.tsx +0 -138
  67. package/src/v2/Common-V2/Popout.tsx +0 -124
  68. package/src/v2/Common-V2/PopoutTypes.tsx +0 -109
  69. package/src/v2/Common-V2/SelectToggleButton.tsx +0 -99
  70. package/src/v2/Common-V2/index.ts +0 -11
  71. package/src/v2/Common-V2/props.ts +0 -84
  72. package/src/v2/Common-V2/styles.tsx +0 -41
  73. package/src/v2/Common-V2/types.ts +0 -16
  74. package/src/v2/Common-V2/useHighlightedIndex.ts +0 -62
  75. package/src/v2/Common-V2/utils.ts +0 -238
  76. package/src/v2/SearchableSelect/AutocompleteContent.tsx +0 -325
  77. package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +0 -527
  78. package/src/v2/SearchableSelect/SearchableSelect.tsx +0 -395
  79. package/src/v2/SearchableSelect/index.ts +0 -12
  80. package/src/v2/Select/MultiSelect.tsx +0 -417
  81. package/src/v2/Select/Select.stories.tsx +0 -593
  82. package/src/v2/Select/SingleSelect.tsx +0 -285
  83. package/src/v2/Select/index.ts +0 -2
  84. package/src/v2/index.ts +0 -2
@@ -1,527 +0,0 @@
1
- import { useState, useMemo } from "react";
2
- import { useCombobox, useMultipleSelection } from "downshift";
3
- import type { ReactNode } from "react";
4
- import { Label } from "@sproutsocial/seeds-react-label";
5
- import { Input } from "@sproutsocial/seeds-react-input";
6
- // This will eventually be imported from seeds-react-popout
7
- import { Popout, SelectToggleButton } from "../Common-V2";
8
- import {
9
- VirtualizedAutocompleteContent,
10
- NonVirtualizedAutocompleteContent,
11
- } from "./AutocompleteContent";
12
- import {
13
- isGroupHeading,
14
- isToggleItem,
15
- menuAnimationConfig,
16
- getFilteredItems,
17
- groupItemsWithHeadings,
18
- useHighlightedIndex,
19
- createItemToStringForDownshift,
20
- } from "../Common-V2";
21
- import {
22
- type DataWithId,
23
- type GroupedItem,
24
- type ToggleItem,
25
- type GroupHeading,
26
- type BaseMultiAutocompleteProps,
27
- } from "../Common-V2";
28
-
29
- function defaultReducer(state, actionAndChanges) {
30
- const { changes, type } = actionAndChanges;
31
-
32
- switch (type) {
33
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
34
- case useCombobox.stateChangeTypes.ItemClick:
35
- // If the item is already selected, don't set highlighted index to 0
36
- // We don't have access to selectedItems in the reducer
37
- return {
38
- ...changes,
39
- isOpen: true, // keep the menu open after selection.
40
- highlightedIndex: 0, // with the first option highlighted.
41
- };
42
- default:
43
- return changes;
44
- }
45
- }
46
-
47
- export interface SearchableMultiSelectProps<T extends DataWithId>
48
- extends BaseMultiAutocompleteProps<T> {
49
- renderSelections?: (items: T[]) => ReactNode;
50
- name: string;
51
- triggerButtonProps?: any;
52
- }
53
-
54
- export function SearchableMultiSelect<T extends DataWithId>({
55
- id,
56
- data,
57
- selectedItemIds: controlledSelectedItemIds,
58
- inputValue: controlledInputValue,
59
- isOpen: controlledIsOpen,
60
- itemToKey = (item: T) => item?.id,
61
- itemToString,
62
- itemToSearchString,
63
- renderSelections,
64
- renderItem,
65
- groupBy,
66
- renderGroupHeading,
67
- isVirtualized = false,
68
- showSelectedItems = false,
69
- selectHeadings = false,
70
- selectAllItem,
71
- EmptyState,
72
- name,
73
- placeholder,
74
- onSelectedItemIdsChange,
75
- onInputValueChange,
76
- onIsOpenChange,
77
- stateReducer = defaultReducer,
78
- triggerButtonProps,
79
- onPointerDownOutside,
80
- onInteractOutside,
81
- popoutPlacement,
82
- }: SearchableMultiSelectProps<T>) {
83
- const [uncontrolledInputValue, setUncontrolledInputValue] =
84
- useState<string>("");
85
- const [uncontrolledSelectedItemIds, setUncontrolledSelectedItemIds] =
86
- useState<T["id"][]>([]);
87
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
88
-
89
- // Use controlled selectedItemIds if provided, otherwise use internal state
90
- const isSelectedItemIdsControlled = controlledSelectedItemIds !== undefined;
91
- const selectedItemIds = isSelectedItemIdsControlled
92
- ? controlledSelectedItemIds
93
- : uncontrolledSelectedItemIds;
94
-
95
- // Convert selectedItemIds to selectedItems for internal use
96
- const selectedItems = useMemo<T[]>(() => {
97
- return data.filter((item) => selectedItemIds.includes(itemToKey(item)));
98
- }, [selectedItemIds, data, itemToKey]);
99
-
100
- // Use controlled inputValue if provided, otherwise use internal state
101
- const isInputValueControlled = controlledInputValue !== undefined;
102
- const inputValue = isInputValueControlled
103
- ? controlledInputValue
104
- : uncontrolledInputValue;
105
-
106
- // Use controlled isOpen if provided, otherwise use internal state
107
- const isIsOpenControlled = controlledIsOpen !== undefined;
108
- const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
109
-
110
- const updateSelectedItems = (newSelectedItems: T[]) => {
111
- const newSelectedItemIds = newSelectedItems.map((item) => itemToKey(item));
112
- if (!isSelectedItemIdsControlled) {
113
- setUncontrolledSelectedItemIds(newSelectedItemIds);
114
- }
115
- onSelectedItemIdsChange?.(newSelectedItemIds);
116
- };
117
-
118
- const updateInputValue = (newInputValue: string) => {
119
- if (!isInputValueControlled) {
120
- setUncontrolledInputValue(newInputValue);
121
- }
122
- onInputValueChange?.(newInputValue);
123
- };
124
-
125
- const updateIsOpen = (newIsOpen: boolean) => {
126
- if (!isIsOpenControlled) {
127
- setUncontrolledIsOpen(newIsOpen);
128
- }
129
- onIsOpenChange?.(newIsOpen);
130
- };
131
-
132
- // Use itemToSearchString if provided, otherwise fallback to itemToString
133
- const searchStringFn =
134
- itemToSearchString ?? ((item: T) => itemToString(item));
135
-
136
- const filteredItems = useMemo<T[]>(
137
- () =>
138
- getFilteredItems(data, inputValue, searchStringFn, itemToString, {
139
- selectedItems,
140
- showSelectedItems,
141
- }),
142
- [
143
- data,
144
- selectedItems,
145
- inputValue,
146
- searchStringFn,
147
- showSelectedItems,
148
- itemToString,
149
- ]
150
- );
151
-
152
- const items = useMemo<GroupedItem<T>[]>(() => {
153
- let groupedItems: GroupedItem<T>[];
154
-
155
- if (groupBy) {
156
- // Group first, then add selectAllItem at the beginning
157
- groupedItems = groupItemsWithHeadings(filteredItems, groupBy, data);
158
- } else {
159
- groupedItems = filteredItems;
160
- }
161
-
162
- // Add selectAllItem at the beginning if provided
163
- if (selectAllItem) {
164
- return [selectAllItem, ...groupedItems];
165
- }
166
-
167
- return groupedItems;
168
- }, [filteredItems, groupBy, selectAllItem, data]);
169
-
170
- // Handler for heading clicks - selects/deselects all items in a group
171
- const handleHeadingClick = useMemo(() => {
172
- if (!selectHeadings || !groupBy) {
173
- return undefined;
174
- }
175
-
176
- return (groupLabel: string, allSelected: boolean) => {
177
- // Find all items in this group from the full data array
178
- const groupItems = data.filter((item) => groupBy(item) === groupLabel);
179
-
180
- if (allSelected) {
181
- // Deselect all items in the group
182
- const newSelectedItems = selectedItems.filter(
183
- (item) => !groupItems.includes(item)
184
- );
185
- updateSelectedItems(newSelectedItems);
186
- } else {
187
- // Select all items in the group (add them if not already selected)
188
- const newSelectedItems = [...selectedItems];
189
- for (const groupItem of groupItems) {
190
- if (!newSelectedItems.includes(groupItem)) {
191
- newSelectedItems.push(groupItem);
192
- }
193
- }
194
- updateSelectedItems(newSelectedItems);
195
- }
196
- };
197
- }, [selectHeadings, groupBy, data, selectedItems, updateSelectedItems]);
198
-
199
- const { getSelectedItemProps, getDropdownProps, removeSelectedItem } =
200
- useMultipleSelection<T>({
201
- selectedItems,
202
- onStateChange({ selectedItems: newSelectedItems, type }) {
203
- switch (type) {
204
- case useMultipleSelection.stateChangeTypes
205
- .SelectedItemKeyDownBackspace:
206
- case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
207
- case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
208
- case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
209
- if (newSelectedItems !== undefined) {
210
- updateSelectedItems(newSelectedItems);
211
- }
212
- break;
213
- default:
214
- break;
215
- }
216
- },
217
- });
218
-
219
- // Filter out headings for downshift unless selectHeadings is true
220
- const itemsForCombobox = useMemo<(T | ToggleItem | GroupHeading)[]>(() => {
221
- if (selectHeadings) {
222
- // Include all items (headings, toggle items, and data items)
223
- return items;
224
- }
225
- return items.filter(
226
- (item): item is T | ToggleItem => !isGroupHeading(item)
227
- );
228
- }, [items, selectHeadings]);
229
-
230
- // Wrapper function to handle ToggleItem and GroupHeading separately
231
- const itemToStringForDownshift = useMemo(
232
- () => createItemToStringForDownshift(itemToString),
233
- [itemToString]
234
- );
235
-
236
- const comboboxProps = useCombobox<T | ToggleItem | GroupHeading>({
237
- inputId: id,
238
- items: itemsForCombobox,
239
- itemToString: itemToStringForDownshift,
240
- defaultHighlightedIndex: 0,
241
- selectedItem: null,
242
- inputValue,
243
- isOpen,
244
- stateReducer,
245
- onStateChange({
246
- inputValue: newInputValue,
247
- type,
248
- selectedItem: newSelectedItem,
249
- isOpen: newIsOpen,
250
- }) {
251
- // Handle isOpen changes for any state change type
252
- if (
253
- newIsOpen !== undefined &&
254
- type !== useCombobox.stateChangeTypes.InputBlur
255
- ) {
256
- updateIsOpen(newIsOpen);
257
- }
258
-
259
- switch (type) {
260
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
261
- case useCombobox.stateChangeTypes.ItemClick:
262
- if (newSelectedItem) {
263
- // Handle selectAllItem selection
264
- if (
265
- selectAllItem &&
266
- isToggleItem(newSelectedItem) &&
267
- newSelectedItem.id === selectAllItem.id
268
- ) {
269
- // Check if all items are selected (excluding selectAllItem itself)
270
- const allSelected =
271
- data.length > 0 &&
272
- data.every((item) => selectedItems.includes(item));
273
- if (allSelected) {
274
- // Deselect all items
275
- updateSelectedItems([]);
276
- } else {
277
- // Select all items
278
- updateSelectedItems([...data]);
279
- }
280
- updateInputValue("");
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
- updateInputValue("");
309
- } else if (
310
- !isGroupHeading(newSelectedItem) &&
311
- !isToggleItem(newSelectedItem)
312
- ) {
313
- // Only handle regular data items
314
- const itemIndex = selectedItems.findIndex(
315
- (item) => itemToKey(item) === itemToKey(newSelectedItem as T)
316
- );
317
- if (itemIndex >= 0) {
318
- updateSelectedItems(
319
- selectedItems.filter((_, index) => index !== itemIndex)
320
- );
321
- } else {
322
- updateSelectedItems([...selectedItems, newSelectedItem as T]);
323
- }
324
- updateInputValue("");
325
- }
326
- }
327
- break;
328
-
329
- case useCombobox.stateChangeTypes.InputChange:
330
- updateInputValue(newInputValue ?? "");
331
- break;
332
-
333
- default:
334
- break;
335
- }
336
- },
337
- });
338
- const {
339
- isOpen: comboboxIsOpen,
340
- getLabelProps,
341
- getMenuProps,
342
- getInputProps,
343
- highlightedIndex: comboboxHighlightedIndex,
344
- getItemProps,
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 buttonContent = renderSelections
357
- ? renderSelections(selectedItems)
358
- : selectedItems.length > 0
359
- ? selectedItems.map((item) => itemToString(item)).join(", ")
360
- : placeholder;
361
-
362
- // Handle button click to toggle popout
363
- const handleButtonClick = () => {
364
- updateIsOpen(!comboboxIsOpen);
365
- };
366
-
367
- return (
368
- <Popout
369
- open={comboboxIsOpen}
370
- onOpenChange={updateIsOpen}
371
- side={popoutPlacement}
372
- animationConfig={menuAnimationConfig}
373
- onOpenAutoFocus={(e) => {
374
- // Prevent default auto-focus behavior - we'll focus the input manually
375
- e.preventDefault();
376
- // Focus the input when popout opens
377
- const inputElement = document.querySelector(
378
- `input[name="${name}"]`
379
- ) as HTMLInputElement;
380
- if (inputElement) {
381
- setTimeout(() => {
382
- inputElement.focus();
383
- }, 0);
384
- }
385
- }}
386
- onCloseAutoFocus={(e) => {
387
- // Prevent default auto-focus behavior
388
- e.preventDefault();
389
- }}
390
- onPointerDownOutside={onPointerDownOutside}
391
- onInteractOutside={onInteractOutside}
392
- content={
393
- <div>
394
- {/* Input inside the Popout */}
395
- <div style={{ marginBottom: "8px" }}>
396
- {(() => {
397
- const inputProps = getInputProps(
398
- getDropdownProps({ preventKeyAction: isOpen })
399
- );
400
- const { ref, id, ...restProps } = inputProps as any;
401
- return (
402
- <Input
403
- id={id}
404
- name={name}
405
- inputProps={{
406
- ...restProps,
407
- autoComplete: "off", // Override with "off" to prevent browser autocomplete
408
- }}
409
- innerRef={ref}
410
- placeholder={placeholder}
411
- />
412
- );
413
- })()}
414
- </div>
415
- {/* Menu inside the Popout */}
416
- {isVirtualized ? (
417
- <VirtualizedAutocompleteContent
418
- items={items}
419
- getMenuProps={getMenuProps}
420
- getItemProps={(props: { index: number }) => {
421
- // Only apply getItemProps to actual items, not headings or ToggleItems
422
- const item = items[props.index];
423
- if (isGroupHeading(item) && !selectHeadings) {
424
- return {};
425
- }
426
- if (isToggleItem(item)) {
427
- // Handle ToggleItem clicks manually
428
- return {
429
- onClick: () => {
430
- const allSelected =
431
- data.length > 0 &&
432
- data.every((item) => selectedItems.includes(item));
433
- if (allSelected) {
434
- updateSelectedItems([]);
435
- } else {
436
- updateSelectedItems([...data]);
437
- }
438
- },
439
- };
440
- }
441
- const dataItem = item as T;
442
- // Find the index in itemsForCombobox
443
- const itemIndex = itemsForCombobox.indexOf(dataItem);
444
- if (itemIndex === -1) {
445
- return {};
446
- }
447
- return getItemProps({ item: dataItem, index: itemIndex });
448
- }}
449
- selectedItems={selectedItems}
450
- highlightedIndex={highlightedIndex ?? -1}
451
- renderItem={renderItem}
452
- renderGroupHeading={renderGroupHeading}
453
- EmptyState={EmptyState}
454
- itemToKey={itemToKey}
455
- itemToString={itemToString}
456
- inputType="checkbox"
457
- selectHeadings={selectHeadings}
458
- onHeadingClick={handleHeadingClick}
459
- groupBy={groupBy}
460
- allData={data}
461
- selectAllItem={selectAllItem}
462
- />
463
- ) : (
464
- <NonVirtualizedAutocompleteContent
465
- items={items}
466
- getMenuProps={getMenuProps}
467
- getItemProps={(props: { index: number }) => {
468
- // Only apply getItemProps to actual items, not headings or ToggleItems
469
- const item = items[props.index];
470
- if (isGroupHeading(item) && !selectHeadings) {
471
- return {};
472
- }
473
- if (isToggleItem(item)) {
474
- // Handle ToggleItem clicks manually
475
- return {
476
- onClick: () => {
477
- const allSelected =
478
- data.length > 0 &&
479
- data.every((item) => selectedItems.includes(item));
480
- if (allSelected) {
481
- updateSelectedItems([]);
482
- } else {
483
- updateSelectedItems([...data]);
484
- }
485
- },
486
- };
487
- }
488
- const dataItem = item as T;
489
- // Find the index in itemsForCombobox
490
- const itemIndex = itemsForCombobox.indexOf(dataItem);
491
- if (itemIndex === -1) {
492
- return {};
493
- }
494
- return getItemProps({ item: dataItem, index: itemIndex });
495
- }}
496
- selectedItems={selectedItems}
497
- highlightedIndex={highlightedIndex ?? -1}
498
- renderItem={renderItem}
499
- renderGroupHeading={renderGroupHeading}
500
- EmptyState={EmptyState}
501
- itemToKey={itemToKey}
502
- itemToString={itemToString}
503
- inputType="checkbox"
504
- selectHeadings={selectHeadings}
505
- onHeadingClick={handleHeadingClick}
506
- groupBy={groupBy}
507
- allData={data}
508
- selectAllItem={selectAllItem}
509
- />
510
- )}
511
- </div>
512
- }
513
- >
514
- <div>
515
- <SelectToggleButton
516
- onClick={handleButtonClick}
517
- {...triggerButtonProps}
518
- name={name}
519
- isOpen={comboboxIsOpen}
520
- type="button"
521
- >
522
- {buttonContent}
523
- </SelectToggleButton>
524
- </div>
525
- </Popout>
526
- );
527
- }