@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,366 +0,0 @@
1
- import { useState, useMemo, useEffect } from "react";
2
- import { useCombobox } 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 } from "../Common-V2";
8
- import {
9
- isGroupHeading,
10
- isToggleItem,
11
- menuAnimationConfig,
12
- getFilteredItems,
13
- groupItemsWithHeadings,
14
- useHighlightedIndex,
15
- createItemToStringForDownshift,
16
- ComboboxContent,
17
- } from "../Common-V2";
18
- import {
19
- type DataWithId,
20
- type GroupedItem,
21
- type ToggleItem,
22
- type BaseSingleAutocompleteProps,
23
- } from "../Common-V2";
24
-
25
- function defaultReducer(state, actionAndChanges) {
26
- const { changes, type } = actionAndChanges;
27
-
28
- switch (type) {
29
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
30
- case useCombobox.stateChangeTypes.ItemClick:
31
- return {
32
- ...changes,
33
- isOpen: false, // close the menu after selection for single select
34
- highlightedIndex: 0, // reset highlight
35
- };
36
- default:
37
- return changes;
38
- }
39
- }
40
-
41
- export interface SingleAutocompleteProps<T extends DataWithId>
42
- extends BaseSingleAutocompleteProps<T> {
43
- includePlaceholderItem?: boolean;
44
- }
45
-
46
- export function SingleAutocomplete<T extends DataWithId>({
47
- id,
48
- data,
49
- selectedItemId: controlledSelectedItemId,
50
- inputValue: controlledInputValue,
51
- isOpen: controlledIsOpen,
52
- itemToKey = (item: T) => item?.id,
53
- itemToString,
54
- itemToSearchString,
55
- renderItem,
56
- groupBy,
57
- renderGroupHeading,
58
- isVirtualized = false,
59
- estimatedItemHeight,
60
- EmptyState,
61
- placeholder,
62
- onSelectedItemIdChange,
63
- onInputValueChange,
64
- onIsOpenChange,
65
- stateReducer = defaultReducer,
66
- menuProps,
67
- inputType = "icon",
68
- includePlaceholderItem = false,
69
- onPointerDownOutside,
70
- onInteractOutside,
71
- popoutPlacement,
72
- }: SingleAutocompleteProps<T>) {
73
- const [uncontrolledInputValue, setUncontrolledInputValue] =
74
- useState<string>("");
75
- const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
76
- T["id"] | null
77
- >(null);
78
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
79
-
80
- // Use controlled selectedItemId if provided, otherwise use internal state
81
- const isSelectedItemIdControlled = controlledSelectedItemId !== undefined;
82
- const selectedItemId = isSelectedItemIdControlled
83
- ? controlledSelectedItemId
84
- : uncontrolledSelectedItemId;
85
-
86
- // Convert selectedItemId to selectedItem for internal use
87
- const selectedItem = useMemo<T | null>(() => {
88
- if (selectedItemId === null || selectedItemId === undefined) {
89
- return null;
90
- }
91
- return data.find((item) => itemToKey(item) === selectedItemId) ?? null;
92
- }, [selectedItemId, data, itemToKey]);
93
-
94
- // Use controlled inputValue if provided, otherwise use internal state
95
- const isInputValueControlled = controlledInputValue !== undefined;
96
- const inputValue = isInputValueControlled
97
- ? controlledInputValue
98
- : uncontrolledInputValue;
99
-
100
- // Use controlled isOpen if provided, otherwise use internal state
101
- const isIsOpenControlled = controlledIsOpen !== undefined;
102
- const isOpen = isIsOpenControlled ? controlledIsOpen : uncontrolledIsOpen;
103
-
104
- // Sync inputValue with selectedItem when selectedItem changes externally and menu is closed
105
- useEffect(() => {
106
- if (selectedItem && !isOpen) {
107
- // Only update if menu is closed - when open, user might be typing
108
- const selectedItemString = itemToString(selectedItem);
109
- if (!isInputValueControlled) {
110
- // For uncontrolled input, update internal state
111
- if (uncontrolledInputValue !== selectedItemString) {
112
- setUncontrolledInputValue(selectedItemString);
113
- }
114
- } else {
115
- // For controlled input, notify parent if value doesn't match
116
- // Parent is responsible for updating the controlled inputValue
117
- if (inputValue !== selectedItemString) {
118
- onInputValueChange?.(selectedItemString);
119
- }
120
- }
121
- }
122
- }, [
123
- selectedItem,
124
- isOpen,
125
- isInputValueControlled,
126
- itemToString,
127
- uncontrolledInputValue,
128
- inputValue,
129
- onInputValueChange,
130
- ]);
131
-
132
- const updateSelectedItem = (newSelectedItem: T | null) => {
133
- const newSelectedItemId = newSelectedItem
134
- ? itemToKey(newSelectedItem)
135
- : null;
136
- if (!isSelectedItemIdControlled) {
137
- setUncontrolledSelectedItemId(newSelectedItemId);
138
- }
139
- onSelectedItemIdChange?.(newSelectedItemId);
140
- };
141
-
142
- const updateInputValue = (newInputValue: string) => {
143
- if (!isInputValueControlled) {
144
- setUncontrolledInputValue(newInputValue);
145
- }
146
- onInputValueChange?.(newInputValue);
147
- };
148
-
149
- const updateIsOpen = (newIsOpen: boolean) => {
150
- if (!isIsOpenControlled) {
151
- setUncontrolledIsOpen(newIsOpen);
152
- }
153
- onIsOpenChange?.(newIsOpen);
154
- };
155
-
156
- // Use itemToSearchString if provided, otherwise fallback to itemToString
157
- const searchStringFn =
158
- itemToSearchString ?? ((item: T) => itemToString(item));
159
-
160
- const filteredItems = useMemo<T[]>(
161
- () =>
162
- getFilteredItems(data, inputValue, searchStringFn, itemToString, {
163
- selectedItem,
164
- }),
165
- [data, selectedItem, inputValue, searchStringFn, itemToString]
166
- );
167
-
168
- const placeholderItem: ToggleItem = {
169
- id: "clear-selection",
170
- label: placeholder,
171
- };
172
-
173
- const items = useMemo<GroupedItem<T>[]>(() => {
174
- let groupedItems: GroupedItem<T>[];
175
-
176
- if (groupBy) {
177
- groupedItems = groupItemsWithHeadings(
178
- filteredItems,
179
- groupBy,
180
- data,
181
- isVirtualized
182
- );
183
- } else {
184
- groupedItems = filteredItems;
185
- }
186
-
187
- // Add placeholder item at the start if needed
188
- groupedItems = includePlaceholderItem
189
- ? [placeholderItem, ...groupedItems]
190
- : groupedItems;
191
-
192
- return groupedItems;
193
- }, [
194
- filteredItems,
195
- placeholder,
196
- groupBy,
197
- data,
198
- includePlaceholderItem,
199
- isVirtualized,
200
- ]);
201
-
202
- // Filter out headings for downshift
203
- const itemsForCombobox = useMemo<(T | ToggleItem)[]>(() => {
204
- return items.filter(
205
- (item): item is T | ToggleItem => !isGroupHeading(item)
206
- );
207
- }, [items]);
208
-
209
- // Wrapper function to handle ToggleItem separately
210
- const itemToStringForDownshift = useMemo(
211
- () => createItemToStringForDownshift(itemToString),
212
- [itemToString]
213
- );
214
-
215
- const comboboxProps = useCombobox<T | ToggleItem>({
216
- inputId: id,
217
- items: itemsForCombobox,
218
- itemToString: itemToStringForDownshift,
219
- defaultHighlightedIndex: 0,
220
- selectedItem: selectedItem,
221
- inputValue,
222
- isOpen,
223
- stateReducer,
224
- onStateChange({
225
- inputValue: newInputValue,
226
- type,
227
- selectedItem: newSelectedItem,
228
- isOpen: newIsOpen,
229
- }) {
230
- // Handle isOpen changes for any state change type
231
- if (newIsOpen !== undefined) {
232
- updateIsOpen(newIsOpen);
233
- }
234
-
235
- switch (type) {
236
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
237
- case useCombobox.stateChangeTypes.ItemClick:
238
- if (newSelectedItem) {
239
- updateSelectedItem(newSelectedItem as T);
240
- if (newSelectedItem.id === "clear-selection") {
241
- // Clear input if placeholder item is selected
242
- updateInputValue("");
243
- return;
244
- }
245
- // Set input to selected item's string representation
246
- updateInputValue(itemToString(newSelectedItem as T));
247
- }
248
- break;
249
-
250
- case useCombobox.stateChangeTypes.InputChange:
251
- updateInputValue(newInputValue ?? "");
252
- if (newInputValue === "" && selectedItem && !isInputValueControlled) {
253
- // If the input is cleared and a selected item is present, clear the selection
254
- // This is to ensure that the input value is cleared when the selected item is cleared
255
- updateSelectedItem(null);
256
- }
257
- break;
258
-
259
- default:
260
- break;
261
- }
262
- },
263
- });
264
- const {
265
- isOpen: comboboxIsOpen,
266
- getToggleButtonProps,
267
- getLabelProps,
268
- getMenuProps,
269
- getInputProps,
270
- highlightedIndex: comboboxHighlightedIndex,
271
- getItemProps,
272
- selectedItem: comboboxSelectedItem,
273
- } = comboboxProps;
274
-
275
- // Map the highlighted index from combobox (which only knows about items) to the full items array (which includes headings)
276
- const highlightedIndex = useHighlightedIndex(
277
- items,
278
- itemsForCombobox,
279
- comboboxHighlightedIndex,
280
- !!groupBy,
281
- false // Single select components hardcode selectHeadings to false
282
- );
283
-
284
- const inputProps = getInputProps();
285
- // Downshift adds aria-labelledby by default, which can conflict with our Label usage
286
- inputProps["aria-labelledby"] = undefined;
287
- if (!comboboxIsOpen) {
288
- // Remove aria-controls when the menu is closed to avoid referencing a non-existent element
289
- // @ts-expect-error downshift types require aria-controls even though it is not required
290
- inputProps["aria-controls"] = undefined;
291
- }
292
- const { ref, id: inputId, ...restProps } = inputProps as any;
293
-
294
- return (
295
- <Popout
296
- open={comboboxIsOpen}
297
- onOpenChange={updateIsOpen}
298
- side={popoutPlacement}
299
- animationConfig={menuAnimationConfig}
300
- onOpenAutoFocus={(e) => {
301
- // Prevent default auto-focus behavior
302
- e.preventDefault();
303
- }}
304
- onCloseAutoFocus={(e) => {
305
- // Prevent default auto-focus behavior
306
- e.preventDefault();
307
- }}
308
- onPointerDownOutside={onPointerDownOutside}
309
- onInteractOutside={onInteractOutside}
310
- content={
311
- <ComboboxContent
312
- items={items}
313
- getMenuProps={getMenuProps}
314
- getItemProps={(props: { index: number }) => {
315
- // Only apply getItemProps to actual items, not headings or ToggleItems
316
- const item = items[props.index];
317
- const dataItem = item as T;
318
- // Find the index in itemsForCombobox using itemToKey for safer comparison
319
- const itemIndex = itemsForCombobox.findIndex((comboboxItem) => {
320
- if (isToggleItem(comboboxItem)) {
321
- return false;
322
- }
323
- return itemToKey(comboboxItem as T) === itemToKey(dataItem);
324
- });
325
- if (itemIndex === -1) {
326
- return {};
327
- }
328
- return getItemProps({ item: dataItem, index: itemIndex });
329
- }}
330
- selectedItems={selectedItem ? [selectedItem] : []}
331
- highlightedIndex={highlightedIndex ?? -1}
332
- renderItem={renderItem}
333
- renderGroupHeading={renderGroupHeading}
334
- EmptyState={EmptyState}
335
- itemToKey={itemToKey}
336
- itemToString={itemToString}
337
- inputType={inputType}
338
- selectHeadings={false}
339
- groupBy={groupBy}
340
- isVirtualized={isVirtualized}
341
- estimatedItemHeight={estimatedItemHeight}
342
- placeholderItem={includePlaceholderItem ? placeholderItem : undefined}
343
- {...menuProps}
344
- />
345
- }
346
- >
347
- <Input
348
- id={id}
349
- inputProps={{
350
- autoComplete: "off",
351
- }}
352
- innerRef={ref}
353
- placeholder={placeholder}
354
- elemAfter={!includePlaceholderItem ? <Input.ClearButton /> : undefined}
355
- clearButtonLabel="Clear selection"
356
- onClear={() => {
357
- // Note, by default the input clear button re-focuses the input
358
- // We don't want to do that, but that would require a breaking change to Input
359
- updateSelectedItem(null);
360
- updateInputValue("");
361
- }}
362
- {...restProps}
363
- />
364
- </Popout>
365
- );
366
- }
@@ -1,2 +0,0 @@
1
- export * from "./SingleAutocomplete";
2
- export * from "./MultiAutocomplete";