@sproutsocial/seeds-react-menu 1.17.0 → 1.18.1

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 +37 -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,238 +0,0 @@
1
- import React from "react";
2
- import { MOTION_DURATION_FAST } from "@sproutsocial/seeds-motion/unitless";
3
- import { type EnumPlacements } from "@sproutsocial/seeds-react-popout";
4
- import type {
5
- DataWithId,
6
- GroupedItem,
7
- GroupHeading,
8
- ToggleItem,
9
- } from "./types";
10
-
11
- export function isGroupHeading<T>(item: GroupedItem<T>): item is GroupHeading {
12
- return (
13
- typeof item === "object" &&
14
- item !== null &&
15
- "type" in item &&
16
- item.type === "heading"
17
- );
18
- }
19
-
20
- export function isToggleItem<T>(
21
- item: GroupedItem<T> | T | ToggleItem | null
22
- ): item is ToggleItem {
23
- return (
24
- typeof item === "object" &&
25
- item !== null &&
26
- "label" in item &&
27
- !("type" in item) &&
28
- Object.keys(item).length === 2 && // ToggleItem only has id and label
29
- "id" in item
30
- );
31
- }
32
-
33
- export const menuPlacementOrigin: {
34
- [key in EnumPlacements]?: { originX: number; originY: number };
35
- } = {
36
- top: { originX: 0.5, originY: 1 },
37
- ["top-start"]: { originX: 0, originY: 1 },
38
- ["top-end"]: { originX: 1, originY: 1 },
39
- bottom: { originX: 0.5, originY: 0 },
40
- ["bottom-start"]: { originX: 0, originY: 0 },
41
- ["bottom-end"]: { originX: 1, originY: 0 },
42
- left: { originX: 1, originY: 0.5 },
43
- ["left-start"]: { originX: 1, originY: 0 },
44
- ["left-end"]: { originX: 1, originY: 1 },
45
- right: { originX: 0, originY: 0.5 },
46
- ["right-start"]: { originX: 0, originY: 0 },
47
- ["right-end"]: { originX: 0, originY: 1 },
48
- };
49
-
50
- export const menuAnimationConfig = {
51
- // Turning off layout true. This seems to be what causes the crazy animations when searching.
52
- // layout: true,
53
- variants: {
54
- initial: (
55
- custom: { placement: EnumPlacements } = { placement: "bottom" }
56
- ) => ({
57
- ...menuPlacementOrigin[custom.placement],
58
- scale: 0,
59
- opacity: 0,
60
- }),
61
- animate: (
62
- custom: { placement: EnumPlacements } = { placement: "bottom" }
63
- ) => ({
64
- ...menuPlacementOrigin[custom.placement],
65
- opacity: 1,
66
- scale: 1,
67
-
68
- transition: {
69
- type: "tween",
70
- duration: MOTION_DURATION_FAST,
71
- ease: "circOut",
72
- },
73
- }),
74
- exit: {
75
- opacity: 0,
76
- scale: 1,
77
- transition: {
78
- type: "tween",
79
- duration: MOTION_DURATION_FAST,
80
- ease: "circIn",
81
- },
82
- },
83
- },
84
- initial: "initial",
85
- whileInView: "animate",
86
- viewport: { once: true },
87
- exit: "exit",
88
- };
89
-
90
- export function getFilteredItems<T extends DataWithId>(
91
- data: T[],
92
- inputValue: string,
93
- itemToSearchString: (item: T) => string,
94
- itemToString: (item: T) => string,
95
- options?: {
96
- selectedItems?: T[];
97
- selectedItem?: T | null;
98
- showSelectedItems?: boolean;
99
- }
100
- ) {
101
- const lowerCasedInputValue = inputValue.toLowerCase();
102
-
103
- // If the item string exactly matches the input value, return for all values
104
- // This means an item was selected, and we should allow users to change easily
105
- if (
106
- options?.selectedItem &&
107
- itemToString(options?.selectedItem).toLowerCase() === lowerCasedInputValue
108
- ) {
109
- return data;
110
- }
111
-
112
- return data.filter(function filterItem(item) {
113
- // For multi-select: filter out selected items if showSelectedItems is false
114
- if (
115
- options?.selectedItems &&
116
- !options.showSelectedItems &&
117
- options.selectedItems.includes(item)
118
- ) {
119
- return false;
120
- }
121
-
122
- // Get the pipe-delimited search string for this item
123
- const searchString = itemToSearchString(item);
124
- // Split on '|' and check if any part matches the input value
125
- const searchableParts = searchString
126
- .split("|")
127
- .map((part) => part.toLowerCase().trim());
128
-
129
- return searchableParts.some((part) => part.includes(lowerCasedInputValue));
130
- });
131
- }
132
-
133
- export function groupItems<T extends DataWithId>(
134
- items: T[],
135
- groupBy: (item: T) => string,
136
- allData?: T[]
137
- ): Map<string, T[]> {
138
- if (items.length === 0) {
139
- return new Map<string, T[]>();
140
- }
141
-
142
- // Determine group order from allData if provided, otherwise use items
143
- const dataForOrder = allData || items;
144
- const groupOrder: string[] = [];
145
- const seenGroups = new Set<string>();
146
-
147
- // First pass: determine group order from allData
148
- for (const item of dataForOrder) {
149
- let groupKey = groupBy(item);
150
- if (item.id === "clear-selection") {
151
- continue; // Skip placeholder item for group order
152
- }
153
-
154
- if (!seenGroups.has(groupKey)) {
155
- seenGroups.add(groupKey);
156
- groupOrder.push(groupKey);
157
- }
158
- }
159
-
160
- // Group filtered items by the groupBy function
161
- const grouped = new Map<string, T[]>();
162
- for (const item of items) {
163
- let groupKey = groupBy(item);
164
- if (item.id === "clear-selection") {
165
- continue; // Skip placeholder item for grouping
166
- }
167
-
168
- if (!grouped.has(groupKey)) {
169
- grouped.set(groupKey, []);
170
- }
171
- grouped.get(groupKey)!.push(item);
172
- }
173
-
174
- // Sort the groups by the group order
175
- const sortedGrouped = new Map<string, T[]>();
176
- for (const groupKey of groupOrder) {
177
- const groupItems = grouped.get(groupKey);
178
- if (groupItems) {
179
- sortedGrouped.set(groupKey, groupItems);
180
- }
181
- }
182
-
183
- return sortedGrouped;
184
- }
185
-
186
- export function groupItemsWithHeadings<T extends DataWithId>(
187
- items: T[],
188
- groupBy: (item: T) => string,
189
- allData?: T[],
190
- selectHeadings = false
191
- ): GroupedItem<T>[] {
192
- if (items.length === 0) {
193
- return [];
194
- }
195
-
196
- const grouped = groupItems(items, groupBy, allData);
197
-
198
- // Create the result array with headings and items in the determined order
199
- // Only include groups that have items in the filtered items
200
- const result: GroupedItem<T>[] = [];
201
- for (const groupKey of grouped.keys()) {
202
- const groupItems = grouped.get(groupKey);
203
- // Only include groups that have items in the filtered data
204
- if (groupItems && groupItems.length > 0) {
205
- // Add heading
206
- if (selectHeadings) {
207
- result.push({
208
- id: `heading-${groupKey}`,
209
- type: "heading",
210
- label: groupKey,
211
- });
212
- }
213
- // Add items in this group
214
- result.push(...groupItems);
215
- }
216
- }
217
-
218
- return result;
219
- }
220
-
221
- /**
222
- * Creates a wrapper function for itemToString that handles ToggleItem and GroupHeading separately.
223
- * These items are not selectable (only type T items can be selected), so they use their label property directly.
224
- */
225
- export function createItemToStringForDownshift<T extends DataWithId>(
226
- itemToString: (item: T | null) => string
227
- ) {
228
- return (item: T | ToggleItem | GroupHeading | null): string => {
229
- if (!item) return "";
230
- if (isToggleItem(item)) {
231
- return item.label;
232
- }
233
- if (isGroupHeading(item)) {
234
- return item.label;
235
- }
236
- return itemToString(item);
237
- };
238
- }
@@ -1,325 +0,0 @@
1
- import { useRef, useEffect } from "react";
2
- import { useVirtualizer } from "@tanstack/react-virtual";
3
- import type { ReactNode, MouseEvent } from "react";
4
- import { StyledMenuContent } from "../../MenuContent/styles";
5
- import {
6
- MenuItem,
7
- MenuHeading,
8
- isGroupHeading,
9
- isToggleItem,
10
- type DataWithId,
11
- type GroupedItem,
12
- type ToggleItem,
13
- } from "../Common-V2";
14
- import type { InputType } from "../../MenuItem/MenuItemTypes";
15
-
16
- interface AutocompleteContentProps<T extends DataWithId> {
17
- items: GroupedItem<T>[];
18
- getMenuProps: any;
19
- getItemProps: any;
20
- selectedItems: T[];
21
- highlightedIndex: number;
22
- renderItem: (item: T) => ReactNode;
23
- renderGroupHeading?: (label: string) => string;
24
- EmptyState?: ReactNode;
25
- itemToKey: (item: T) => string | number;
26
- itemToString: (item: T | null) => string;
27
- inputType: InputType;
28
- selectHeadings?: boolean;
29
- onHeadingClick?: (groupLabel: string, allSelected: boolean) => void;
30
- groupBy?: (item: T) => string;
31
- allData?: T[];
32
- selectAllItem?: ToggleItem;
33
- }
34
-
35
- export const VirtualizedAutocompleteContent = <T extends DataWithId>({
36
- items,
37
- getMenuProps,
38
- getItemProps,
39
- selectedItems,
40
- highlightedIndex,
41
- renderItem,
42
- renderGroupHeading,
43
- EmptyState,
44
- itemToKey,
45
- itemToString,
46
- inputType,
47
- selectHeadings = false,
48
- onHeadingClick,
49
- groupBy,
50
- allData,
51
- selectAllItem,
52
- }: AutocompleteContentProps<T>) => {
53
- const containerRef = useRef(null);
54
-
55
- // Check if there are any actual items (not just headings)
56
- const hasItems = items.some((item) => !isGroupHeading(item));
57
-
58
- const rowVirtualizer = useVirtualizer({
59
- count: items.length,
60
- getScrollElement: () => containerRef.current,
61
- estimateSize: (index) => {
62
- const item = items[index];
63
- return isGroupHeading(item) ? 30 : 35;
64
- },
65
- });
66
-
67
- useEffect(() => {
68
- if (
69
- highlightedIndex !== undefined &&
70
- highlightedIndex !== null &&
71
- highlightedIndex >= 0
72
- ) {
73
- rowVirtualizer.scrollToIndex(highlightedIndex);
74
- }
75
- }, [highlightedIndex, rowVirtualizer]);
76
-
77
- const virtualItems = rowVirtualizer.getVirtualItems();
78
-
79
- const menuProps = getMenuProps({ ref: containerRef });
80
- return (
81
- <div {...menuProps} style={{ maxHeight: "400px", overflow: "auto" }}>
82
- {!hasItems && EmptyState ? (
83
- <div style={{ padding: "16px" }}>{EmptyState}</div>
84
- ) : (
85
- <StyledMenuContent
86
- style={{
87
- height: `${rowVirtualizer.getTotalSize()}px`,
88
- position: "relative",
89
- }}
90
- >
91
- <div
92
- style={{
93
- position: "absolute",
94
- top: 0,
95
- left: 0,
96
- width: "100%",
97
- transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,
98
- }}
99
- >
100
- {virtualItems.map((virtualItem) => {
101
- const item = items[virtualItem.index];
102
-
103
- if (isGroupHeading(item)) {
104
- // Find all items in this group from the full data array (if groupBy is provided)
105
- const groupItems =
106
- groupBy && allData
107
- ? allData.filter(
108
- (dataItem) => groupBy(dataItem) === item.label
109
- )
110
- : [];
111
-
112
- // Check if all items in the group are selected
113
- const allSelected =
114
- groupItems.length > 0 &&
115
- groupItems.every((groupItem) =>
116
- selectedItems.includes(groupItem)
117
- );
118
-
119
- const headingItemProps = selectHeadings
120
- ? getItemProps({ index: virtualItem.index })
121
- : {};
122
- const handleHeadingClick = (e: MouseEvent) => {
123
- // Call Downshift's onClick if it exists
124
- headingItemProps.onClick?.(e);
125
- // Then call our custom handler
126
- if (onHeadingClick) {
127
- onHeadingClick(item.label, allSelected);
128
- }
129
- };
130
-
131
- return (
132
- <MenuHeading
133
- key={virtualItem.key}
134
- label={item.label}
135
- {...headingItemProps}
136
- $highlighted={highlightedIndex === virtualItem.index}
137
- renderGroupHeading={renderGroupHeading}
138
- selectable={selectHeadings}
139
- selected={allSelected}
140
- id={item.id}
141
- onClick={selectHeadings ? handleHeadingClick : undefined}
142
- style={{
143
- width: "100%",
144
- }}
145
- />
146
- );
147
- }
148
-
149
- // Handle ToggleItem separately
150
- if (isToggleItem(item)) {
151
- const toggleItem = item as ToggleItem;
152
- const isItemSelected =
153
- selectAllItem && toggleItem.id === selectAllItem.id
154
- ? allData &&
155
- allData.length > 0 &&
156
- allData.every((item) => selectedItems.includes(item))
157
- : false;
158
-
159
- return (
160
- <MenuItem
161
- key={virtualItem.key}
162
- inputType={inputType}
163
- selected={isItemSelected}
164
- id={toggleItem.id}
165
- {...getItemProps({ index: virtualItem.index })}
166
- style={{
167
- width: "100%",
168
- /* height: `${virtualItem.size}px`,
169
- transform: `translateY(${virtualItem.start}px)`, */
170
- }}
171
- $highlighted={highlightedIndex === virtualItem.index}
172
- $active={false}
173
- >
174
- {toggleItem.label}
175
- </MenuItem>
176
- );
177
- }
178
-
179
- const dataItem = item as T;
180
- const isItemSelected = selectedItems.includes(dataItem);
181
- return (
182
- <MenuItem
183
- key={virtualItem.key}
184
- inputType={inputType}
185
- selected={isItemSelected}
186
- id={itemToKey(dataItem)}
187
- {...getItemProps({ index: virtualItem.index })}
188
- style={{
189
- width: "100%",
190
- }}
191
- $highlighted={highlightedIndex === virtualItem.index}
192
- $active={false}
193
- >
194
- {renderItem(dataItem)}
195
- </MenuItem>
196
- );
197
- })}
198
- </div>
199
- </StyledMenuContent>
200
- )}
201
- </div>
202
- );
203
- };
204
-
205
- export const NonVirtualizedAutocompleteContent = <T extends DataWithId>({
206
- items,
207
- getMenuProps,
208
- getItemProps,
209
- selectedItems,
210
- highlightedIndex,
211
- renderItem,
212
- renderGroupHeading,
213
- EmptyState,
214
- itemToKey,
215
- itemToString,
216
- inputType,
217
- selectHeadings = false,
218
- onHeadingClick,
219
- groupBy,
220
- allData,
221
- selectAllItem,
222
- }: AutocompleteContentProps<T>) => {
223
- // Check if there are any actual items (not just headings)
224
- const hasItems = items.some((item) => !isGroupHeading(item));
225
-
226
- const menuProps = getMenuProps();
227
- return (
228
- <div {...menuProps} style={{ maxHeight: "400px", overflow: "auto" }}>
229
- {!hasItems && EmptyState ? (
230
- <div style={{ padding: "16px" }}>{EmptyState}</div>
231
- ) : (
232
- <StyledMenuContent
233
- style={{
234
- position: "relative",
235
- }}
236
- >
237
- {items.map((item, index) => {
238
- if (isGroupHeading(item)) {
239
- // Find all items in this group from the full data array (if groupBy is provided)
240
- const groupItems =
241
- groupBy && allData
242
- ? allData.filter(
243
- (dataItem) => groupBy(dataItem) === item.label
244
- )
245
- : [];
246
-
247
- // Check if all items in the group are selected
248
- const allSelected =
249
- groupItems.length > 0 &&
250
- groupItems.every((groupItem) =>
251
- selectedItems.includes(groupItem)
252
- );
253
-
254
- const headingItemProps = selectHeadings
255
- ? getItemProps({ index })
256
- : {};
257
- const handleHeadingClick = (e: MouseEvent) => {
258
- // Call Downshift's onClick if it exists
259
- headingItemProps.onClick?.(e);
260
- // Then call our custom handler
261
- if (onHeadingClick) {
262
- onHeadingClick(item.label, allSelected);
263
- }
264
- };
265
-
266
- return (
267
- <MenuHeading
268
- key={index}
269
- {...headingItemProps}
270
- $highlighted={highlightedIndex === index}
271
- label={item.label}
272
- renderGroupHeading={renderGroupHeading}
273
- selectable={selectHeadings}
274
- selected={allSelected}
275
- id={item.id}
276
- onClick={selectHeadings ? handleHeadingClick : undefined}
277
- />
278
- );
279
- }
280
-
281
- // Handle ToggleItem separately
282
- if (isToggleItem(item)) {
283
- const toggleItem = item as ToggleItem;
284
- const isItemSelected =
285
- selectAllItem && toggleItem.id === selectAllItem.id
286
- ? allData &&
287
- allData.length > 0 &&
288
- allData.every((item) => selectedItems.includes(item))
289
- : false;
290
- return (
291
- <MenuItem
292
- key={index}
293
- {...getItemProps({ index })}
294
- $highlighted={highlightedIndex === index}
295
- $active={false}
296
- inputType={inputType}
297
- selected={isItemSelected}
298
- id={toggleItem.id}
299
- >
300
- {toggleItem.label}
301
- </MenuItem>
302
- );
303
- }
304
-
305
- const dataItem = item as T;
306
- const isItemSelected = selectedItems.includes(dataItem);
307
- return (
308
- <MenuItem
309
- key={index}
310
- {...getItemProps({ index })}
311
- $highlighted={highlightedIndex === index}
312
- $active={false}
313
- inputType={inputType}
314
- selected={isItemSelected}
315
- id={itemToKey(dataItem)}
316
- >
317
- {renderItem(dataItem)}
318
- </MenuItem>
319
- );
320
- })}
321
- </StyledMenuContent>
322
- )}
323
- </div>
324
- );
325
- };