@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,417 +0,0 @@
1
- import { useState, useMemo } from "react";
2
- import { useSelect, useMultipleSelection } from "downshift";
3
- import type { ReactNode } from "react";
4
- import { Label } from "@sproutsocial/seeds-react-label";
5
- // This will eventually be imported from seeds-react-popout
6
- import { Popout } from "../Common-V2";
7
- import {
8
- isGroupHeading,
9
- isToggleItem,
10
- menuAnimationConfig,
11
- groupItemsWithHeadings,
12
- useHighlightedIndex,
13
- createItemToStringForDownshift,
14
- ComboboxContent,
15
- SelectToggleButton,
16
- } from "../Common-V2";
17
- import {
18
- type DataWithId,
19
- type GroupedItem,
20
- type ToggleItem,
21
- type GroupHeading,
22
- type BaseMultiSelectProps,
23
- } from "../Common-V2";
24
-
25
- function defaultReducer(state, actionAndChanges) {
26
- const { changes, type } = actionAndChanges;
27
-
28
- switch (type) {
29
- case useSelect.stateChangeTypes.ItemClick:
30
- // If the item is already selected, don't set highlighted index to 0
31
- // We don't have access to selectedItems in the reducer
32
- return {
33
- ...changes,
34
- isOpen: true, // keep the menu open after selection.
35
- highlightedIndex: 0, // with the first option highlighted.
36
- };
37
- default:
38
- // For keyboard Enter/Space on menu items, keep menu open if an item is being selected
39
- if (changes.selectedItem && changes.isOpen === false) {
40
- return {
41
- ...changes,
42
- isOpen: true, // keep the menu open after keyboard selection.
43
- highlightedIndex: 0, // with the first option highlighted.
44
- };
45
- }
46
- return changes;
47
- }
48
- }
49
-
50
- export interface MultiSelectProps<T extends DataWithId>
51
- extends BaseMultiSelectProps<T> {
52
- renderSelections?: (items: T[]) => ReactNode;
53
- triggerButtonProps?: any;
54
- shouldOverflow?: boolean;
55
- }
56
-
57
- export function MultiSelect<T extends DataWithId>({
58
- id,
59
- data,
60
- selectedItemIds: controlledSelectedItemIds,
61
- isOpen: controlledIsOpen,
62
- itemToKey = (item: T) => item?.id,
63
- itemToString,
64
- renderSelections,
65
- renderItem,
66
- groupBy,
67
- renderGroupHeading,
68
- isVirtualized = false,
69
- estimatedItemHeight,
70
- showSelectedItems = false,
71
- selectHeadings = false,
72
- selectAllItem,
73
- placeholder,
74
- onSelectedItemIdsChange,
75
- onIsOpenChange,
76
- stateReducer = defaultReducer,
77
- triggerButtonProps,
78
- menuProps,
79
- inputType = "checkbox",
80
- shouldOverflow,
81
- onPointerDownOutside,
82
- onInteractOutside,
83
- popoutPlacement,
84
- }: MultiSelectProps<T>) {
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 isOpen if provided, otherwise use internal state
101
- const isIsOpenControlled = controlledIsOpen !== undefined;
102
- const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
103
-
104
- const updateSelectedItems = (newSelectedItems: T[]) => {
105
- const newSelectedItemIds = newSelectedItems.map((item) => itemToKey(item));
106
- if (!isSelectedItemIdsControlled) {
107
- setUncontrolledSelectedItemIds(newSelectedItemIds);
108
- }
109
- onSelectedItemIdsChange?.(newSelectedItemIds);
110
- };
111
-
112
- const updateIsOpen = (newIsOpen: boolean) => {
113
- if (!isIsOpenControlled) {
114
- setUncontrolledIsOpen(newIsOpen);
115
- }
116
- onIsOpenChange?.(newIsOpen);
117
- };
118
-
119
- // For Select, we show all items (no filtering)
120
- // Filter out selected items if showSelectedItems is false
121
- const filteredData = useMemo<T[]>(() => {
122
- if (showSelectedItems) {
123
- return data;
124
- }
125
- return data.filter((item) => !selectedItems.includes(item));
126
- }, [data, selectedItems, showSelectedItems]);
127
-
128
- const items = useMemo<GroupedItem<T>[]>(() => {
129
- let groupedItems: GroupedItem<T>[];
130
-
131
- if (groupBy) {
132
- // Group first, then add selectAllItem at the beginning
133
- groupedItems = groupItemsWithHeadings(filteredData, groupBy, data, true);
134
- } else {
135
- groupedItems = filteredData;
136
- }
137
-
138
- // Add selectAllItem at the beginning if provided
139
- if (selectAllItem) {
140
- return [selectAllItem, ...groupedItems];
141
- }
142
-
143
- return groupedItems;
144
- }, [filteredData, groupBy, data, selectAllItem, selectHeadings]);
145
-
146
- // Handler for heading clicks - selects/deselects all items in a group
147
- const handleHeadingClick = useMemo(() => {
148
- if (!selectHeadings || !groupBy) {
149
- return undefined;
150
- }
151
-
152
- return (groupLabel: string, allSelected: boolean) => {
153
- // Find all items in this group from the full data array
154
- const groupItems = data.filter((item) => groupBy(item) === groupLabel);
155
-
156
- if (allSelected) {
157
- // Deselect all items in the group
158
- const newSelectedItems = selectedItems.filter(
159
- (item) => !groupItems.includes(item)
160
- );
161
- updateSelectedItems(newSelectedItems);
162
- } else {
163
- // Select all items in the group (add them if not already selected)
164
- const newSelectedItems = [...selectedItems];
165
- for (const groupItem of groupItems) {
166
- if (!newSelectedItems.includes(groupItem)) {
167
- newSelectedItems.push(groupItem);
168
- }
169
- }
170
- updateSelectedItems(newSelectedItems);
171
- }
172
- };
173
- }, [selectHeadings, groupBy, data, selectedItems, updateSelectedItems]);
174
-
175
- const { getSelectedItemProps, getDropdownProps, removeSelectedItem } =
176
- useMultipleSelection<T>({
177
- selectedItems,
178
- onStateChange({ selectedItems: newSelectedItems, type }) {
179
- switch (type) {
180
- case useMultipleSelection.stateChangeTypes
181
- .SelectedItemKeyDownBackspace:
182
- case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
183
- case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
184
- case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
185
- if (newSelectedItems !== undefined) {
186
- updateSelectedItems(newSelectedItems);
187
- }
188
- break;
189
- default:
190
- break;
191
- }
192
- },
193
- });
194
-
195
- // Filter out headings for downshift unless selectHeadings is true
196
- const itemsForSelect = useMemo<(T | ToggleItem | GroupHeading)[]>(() => {
197
- if (selectHeadings) {
198
- // Include all items (headings, toggle items, and data items)
199
- return items;
200
- }
201
- return items.filter(
202
- (item): item is T | ToggleItem => !isGroupHeading(item)
203
- );
204
- }, [items, selectHeadings]);
205
-
206
- // Wrapper function to handle ToggleItem and GroupHeading separately
207
- const itemToStringForDownshift = useMemo(
208
- () => createItemToStringForDownshift(itemToString),
209
- [itemToString]
210
- );
211
-
212
- const selectProps = useSelect<T | ToggleItem | GroupHeading>({
213
- toggleButtonId: id,
214
- items: itemsForSelect,
215
- itemToString: itemToStringForDownshift,
216
- // defaultHighlightedIndex: 0, // after selection, highlight the first item.
217
- selectedItem: null,
218
- isOpen,
219
- stateReducer,
220
- // Could maybe let devs override this too?
221
- onStateChange({ type, selectedItem: newSelectedItem, isOpen: newIsOpen }) {
222
- // Handle isOpen changes for any state change type
223
- if (newIsOpen !== undefined) {
224
- updateIsOpen(newIsOpen);
225
- }
226
-
227
- // Handle item selection for both click and keyboard (Enter/Space) interactions
228
- // When Enter/Space is pressed on a highlighted item, useSelect sets selectedItem
229
- // We detect selection when selectedItem is set and menu is/was open
230
- if (newSelectedItem && newSelectedItem !== null) {
231
- // Check if this is a selection event (click or keyboard) and menu is open
232
- const isSelectionEvent =
233
- type === useSelect.stateChangeTypes.ItemClick ||
234
- type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
235
-
236
- if (isSelectionEvent) {
237
- // Handle selectAllItem selection
238
- if (
239
- selectAllItem &&
240
- isToggleItem(newSelectedItem) &&
241
- newSelectedItem.id === selectAllItem.id
242
- ) {
243
- // Check if all items are selected (excluding selectAllItem itself)
244
- const allSelected =
245
- data.length > 0 &&
246
- data.every((item) => selectedItems.includes(item));
247
- if (allSelected) {
248
- // Deselect all items
249
- updateSelectedItems([]);
250
- } else {
251
- // Select all items
252
- updateSelectedItems([...data]);
253
- }
254
- } else if (
255
- selectHeadings &&
256
- groupBy &&
257
- isGroupHeading(newSelectedItem)
258
- ) {
259
- // Handle heading selection - select/deselect all items in the group
260
- const groupLabel = newSelectedItem.label;
261
- const groupItems = data.filter(
262
- (item) => groupBy(item) === groupLabel
263
- );
264
- const allSelected =
265
- groupItems.length > 0 &&
266
- groupItems.every((item) => selectedItems.includes(item));
267
-
268
- if (allSelected) {
269
- // Deselect all items in the group
270
- updateSelectedItems(
271
- selectedItems.filter((item) => !groupItems.includes(item))
272
- );
273
- } else {
274
- // Select all items in the group
275
- const newItems = [
276
- ...selectedItems.filter((item) => !groupItems.includes(item)),
277
- ...groupItems,
278
- ];
279
- updateSelectedItems(newItems);
280
- }
281
- } else if (
282
- !isGroupHeading(newSelectedItem) &&
283
- !isToggleItem(newSelectedItem)
284
- ) {
285
- // Only handle regular data items
286
- const itemIndex = selectedItems.findIndex(
287
- (item) => itemToKey(item) === itemToKey(newSelectedItem as T)
288
- );
289
-
290
- if (itemIndex >= 0) {
291
- updateSelectedItems(
292
- selectedItems.filter((_, index) => index !== itemIndex)
293
- );
294
- } else {
295
- updateSelectedItems([...selectedItems, newSelectedItem as T]);
296
- }
297
- }
298
- }
299
- }
300
- },
301
- });
302
-
303
- const {
304
- isOpen: selectIsOpen,
305
- getToggleButtonProps,
306
- getLabelProps,
307
- getMenuProps,
308
- highlightedIndex: selectHighlightedIndex,
309
- getItemProps,
310
- } = selectProps;
311
-
312
- // Map the highlighted index from select (which only knows about items) to the full items array (which includes headings)
313
- const highlightedIndex = useHighlightedIndex(
314
- items,
315
- itemsForSelect,
316
- selectHighlightedIndex,
317
- !!groupBy,
318
- selectHeadings ?? false
319
- );
320
-
321
- const buttonText = renderSelections
322
- ? renderSelections(selectedItems)
323
- : selectedItems.length > 0
324
- ? selectedItems.map((item) => itemToString(item)).join(", ")
325
- : placeholder;
326
-
327
- const toggleButtonIdProps = getToggleButtonProps();
328
- // Downshift adds aria-labelledby by default, which can conflict with our Label usage
329
- toggleButtonIdProps["aria-labelledby"] = undefined;
330
- if (!selectIsOpen) {
331
- // Remove aria-controls when the menu is closed to avoid referencing a non-existent element
332
- // @ts-expect-error downshift types require aria-controls even though it is not required
333
- toggleButtonIdProps["aria-controls"] = undefined;
334
- }
335
-
336
- return (
337
- <Popout
338
- open={selectIsOpen}
339
- onOpenChange={updateIsOpen}
340
- side={popoutPlacement}
341
- onOpenAutoFocus={(e) => {
342
- // Prevent default auto-focus behavior
343
- e.preventDefault();
344
- }}
345
- onCloseAutoFocus={(e) => {
346
- // Prevent default auto-focus behavior
347
- e.preventDefault();
348
- }}
349
- onPointerDownOutside={onPointerDownOutside}
350
- onInteractOutside={onInteractOutside}
351
- animationConfig={menuAnimationConfig}
352
- content={
353
- <ComboboxContent
354
- items={items}
355
- getMenuProps={getMenuProps}
356
- getItemProps={(props: { index: number }) => {
357
- // Only apply getItemProps to actual items, not headings or ToggleItems
358
- const item = items[props.index];
359
- if (isToggleItem(item)) {
360
- // Handle ToggleItem clicks manually
361
- return {
362
- onClick: () => {
363
- // This will be handled by the selection logic in onStateChange
364
- // We need to trigger the selection manually
365
- const allSelected =
366
- data.length > 0 &&
367
- data.every((item) => selectedItems.includes(item));
368
- if (allSelected) {
369
- updateSelectedItems([]);
370
- } else {
371
- updateSelectedItems([...data]);
372
- }
373
- },
374
- };
375
- }
376
- const dataItem = item as T;
377
- // Find the index in itemsForSelect using itemToKey for safer comparison
378
- const itemIndex = itemsForSelect.findIndex((selectItem) => {
379
- if (isGroupHeading(selectItem) || isToggleItem(selectItem)) {
380
- return false;
381
- }
382
- return itemToKey(selectItem as T) === itemToKey(dataItem);
383
- });
384
- if (itemIndex === -1) {
385
- return {};
386
- }
387
- return getItemProps({ item: dataItem, index: itemIndex });
388
- }}
389
- selectedItems={selectedItems}
390
- highlightedIndex={highlightedIndex ?? -1}
391
- renderItem={renderItem}
392
- renderGroupHeading={renderGroupHeading}
393
- itemToKey={itemToKey}
394
- itemToString={itemToString}
395
- inputType={inputType}
396
- selectHeadings={selectHeadings}
397
- onHeadingClick={handleHeadingClick}
398
- groupBy={groupBy}
399
- allData={data}
400
- selectAllItem={selectAllItem}
401
- isVirtualized={isVirtualized}
402
- estimatedItemHeight={estimatedItemHeight}
403
- {...menuProps}
404
- />
405
- }
406
- >
407
- <SelectToggleButton
408
- {...toggleButtonIdProps}
409
- {...triggerButtonProps}
410
- isOpen={selectIsOpen}
411
- shouldOverflow={shouldOverflow}
412
- >
413
- {buttonText}
414
- </SelectToggleButton>
415
- </Popout>
416
- );
417
- }