@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,395 +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, 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 BaseSingleAutocompleteProps,
26
- } from "../Common-V2";
27
-
28
- function defaultReducer(state, actionAndChanges) {
29
- const { changes, type } = actionAndChanges;
30
-
31
- switch (type) {
32
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
33
- case useCombobox.stateChangeTypes.ItemClick:
34
- return {
35
- ...changes,
36
- inputValue: "",
37
- isOpen: false, // close the menu after selection for single select
38
- highlightedIndex: 0, // reset highlight
39
- };
40
- default:
41
- return changes;
42
- }
43
- }
44
-
45
- export interface SearchableSelectProps<T extends DataWithId>
46
- extends BaseSingleAutocompleteProps<T> {
47
- name: string;
48
- renderSelection?: (item: T | null) => ReactNode;
49
- triggerButtonProps?: any;
50
- }
51
-
52
- export function SearchableSelect<T extends DataWithId>({
53
- id,
54
- data,
55
- selectedItemId: controlledSelectedItemId,
56
- inputValue: controlledInputValue,
57
- isOpen: controlledIsOpen,
58
- itemToKey = (item: T) => item?.id,
59
- itemToString,
60
- itemToSearchString,
61
- renderSelection,
62
- renderItem,
63
- groupBy,
64
- renderGroupHeading,
65
- isVirtualized = false,
66
- EmptyState,
67
- name,
68
- placeholder,
69
- placeholderItem,
70
- onSelectedItemIdChange,
71
- onInputValueChange,
72
- onIsOpenChange,
73
- stateReducer = defaultReducer,
74
- triggerButtonProps,
75
- onPointerDownOutside,
76
- onInteractOutside,
77
- popoutPlacement,
78
- }: SearchableSelectProps<T>) {
79
- const [uncontrolledInputValue, setUncontrolledInputValue] =
80
- useState<string>("");
81
- const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
82
- T["id"] | null
83
- >(null);
84
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState<boolean>(false);
85
-
86
- // Use controlled selectedItemId if provided, otherwise use internal state
87
- const isSelectedItemIdControlled = controlledSelectedItemId !== undefined;
88
- const selectedItemId = isSelectedItemIdControlled
89
- ? controlledSelectedItemId
90
- : uncontrolledSelectedItemId;
91
-
92
- // Convert selectedItemId to selectedItem for internal use
93
- const selectedItem = useMemo<T | null>(() => {
94
- if (selectedItemId === null || selectedItemId === undefined) {
95
- return null;
96
- }
97
- return data.find((item) => itemToKey(item) === selectedItemId) ?? null;
98
- }, [selectedItemId, 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 updateSelectedItem = (newSelectedItem: T | null) => {
111
- const newSelectedItemId = newSelectedItem
112
- ? itemToKey(newSelectedItem)
113
- : null;
114
- if (!isSelectedItemIdControlled) {
115
- setUncontrolledSelectedItemId(newSelectedItemId);
116
- }
117
- onSelectedItemIdChange?.(newSelectedItemId);
118
- };
119
-
120
- const updateInputValue = (newInputValue: string) => {
121
- if (!isInputValueControlled) {
122
- setUncontrolledInputValue(newInputValue);
123
- }
124
- onInputValueChange?.(newInputValue);
125
- };
126
-
127
- const updateIsOpen = (newIsOpen: boolean) => {
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
- selectedItem,
142
- }),
143
- [data, selectedItem, inputValue, searchStringFn, itemToString]
144
- );
145
-
146
- const items = useMemo<GroupedItem<T>[]>(() => {
147
- let groupedItems: GroupedItem<T>[];
148
-
149
- if (groupBy) {
150
- // Group first, then add placeholderItem at the beginning
151
- groupedItems = groupItemsWithHeadings(filteredItems, groupBy, data);
152
- } else {
153
- groupedItems = filteredItems;
154
- }
155
-
156
- // Add placeholderItem at the beginning if provided
157
- if (placeholderItem) {
158
- return [placeholderItem, ...groupedItems];
159
- }
160
-
161
- return groupedItems;
162
- }, [filteredItems, groupBy, placeholderItem, data]);
163
-
164
- // Filter out headings for downshift
165
- const itemsForCombobox = useMemo<(T | ToggleItem)[]>(() => {
166
- return items.filter(
167
- (item): item is T | ToggleItem => !isGroupHeading(item)
168
- );
169
- }, [items]);
170
-
171
- // Wrapper function to handle ToggleItem separately
172
- const itemToStringForDownshift = useMemo(
173
- () => createItemToStringForDownshift(itemToString),
174
- [itemToString]
175
- );
176
-
177
- const comboboxProps = useCombobox<T | ToggleItem>({
178
- inputId: id,
179
- items: itemsForCombobox,
180
- itemToString: itemToStringForDownshift,
181
- defaultHighlightedIndex: 0,
182
- selectedItem: selectedItem,
183
- inputValue,
184
- isOpen,
185
- stateReducer,
186
- onStateChange({
187
- inputValue: newInputValue,
188
- type,
189
- selectedItem: newSelectedItem,
190
- isOpen: newIsOpen,
191
- }) {
192
- // Handle isOpen changes for any state change type
193
- if (
194
- newIsOpen !== undefined &&
195
- type !== useCombobox.stateChangeTypes.InputBlur
196
- ) {
197
- updateIsOpen(newIsOpen);
198
- }
199
-
200
- switch (type) {
201
- case useCombobox.stateChangeTypes.InputKeyDownEnter:
202
- case useCombobox.stateChangeTypes.ItemClick:
203
- if (newSelectedItem) {
204
- updateInputValue("");
205
- // If placeholderItem is selected, set selectedItem to null
206
- // Note: placeholderItem won't be in itemsForCombobox, so this check is for safety
207
- if (
208
- placeholderItem &&
209
- isToggleItem(newSelectedItem) &&
210
- newSelectedItem.id === placeholderItem.id
211
- ) {
212
- updateSelectedItem(null);
213
- } else {
214
- updateSelectedItem(newSelectedItem as T);
215
- }
216
- }
217
- break;
218
-
219
- case useCombobox.stateChangeTypes.InputChange:
220
- updateInputValue(newInputValue ?? "");
221
- break;
222
-
223
- default:
224
- break;
225
- }
226
- },
227
- });
228
- const {
229
- isOpen: comboboxIsOpen,
230
- getLabelProps,
231
- getMenuProps,
232
- getInputProps,
233
- highlightedIndex: comboboxHighlightedIndex,
234
- getItemProps,
235
- } = comboboxProps;
236
-
237
- // Map the highlighted index from combobox (which only knows about items) to the full items array (which includes headings)
238
- const highlightedIndex = useHighlightedIndex(
239
- items,
240
- itemsForCombobox,
241
- comboboxHighlightedIndex,
242
- !!groupBy,
243
- false // Single select components hardcode selectHeadings to false
244
- );
245
-
246
- const buttonContent = renderSelection
247
- ? renderSelection(selectedItem)
248
- : selectedItem
249
- ? itemToString(selectedItem)
250
- : placeholder;
251
-
252
- // Handle button click to toggle popout
253
- const handleButtonClick = () => {
254
- updateIsOpen(!comboboxIsOpen);
255
- };
256
-
257
- return (
258
- <Popout
259
- open={comboboxIsOpen}
260
- onOpenChange={updateIsOpen}
261
- side={popoutPlacement}
262
- animationConfig={menuAnimationConfig}
263
- onOpenAutoFocus={(e) => {
264
- // Prevent default auto-focus behavior - we'll focus the input manually
265
- e.preventDefault();
266
- // Focus the input when popout opens
267
- const inputElement = document.querySelector(
268
- `input[name="${name}"]`
269
- ) as HTMLInputElement;
270
- if (inputElement) {
271
- setTimeout(() => {
272
- inputElement.focus();
273
- }, 0);
274
- }
275
- }}
276
- onCloseAutoFocus={(e) => {
277
- // Prevent default auto-focus behavior
278
- e.preventDefault();
279
- }}
280
- onPointerDownOutside={onPointerDownOutside}
281
- onInteractOutside={onInteractOutside}
282
- content={
283
- <div>
284
- {/* Input inside the Popout */}
285
- <div style={{ marginBottom: "8px" }}>
286
- {(() => {
287
- const inputProps = getInputProps();
288
- const { ref, id, ...restProps } = inputProps as any;
289
- return (
290
- <Input
291
- id={id}
292
- name={name}
293
- inputProps={{
294
- autoComplete: "off",
295
- ...restProps,
296
- }}
297
- innerRef={ref}
298
- placeholder={placeholder}
299
- />
300
- );
301
- })()}
302
- </div>
303
- {/* Menu inside the Popout */}
304
- {isVirtualized ? (
305
- <VirtualizedAutocompleteContent
306
- items={items}
307
- getMenuProps={getMenuProps}
308
- getItemProps={(props: { index: number }) => {
309
- // Only apply getItemProps to actual items, not headings or ToggleItems
310
- const item = items[props.index];
311
- if (isGroupHeading(item)) {
312
- return {};
313
- }
314
- if (isToggleItem(item)) {
315
- // Handle ToggleItem (placeholderItem) clicks manually
316
- return {
317
- onClick: () => {
318
- if (placeholderItem && item.id === placeholderItem.id) {
319
- updateSelectedItem(null);
320
- }
321
- },
322
- };
323
- }
324
- const dataItem = item as T;
325
- // Find the index in itemsForCombobox
326
- const itemIndex = itemsForCombobox.indexOf(dataItem);
327
- if (itemIndex === -1) {
328
- return {};
329
- }
330
- return getItemProps({ item: dataItem, index: itemIndex });
331
- }}
332
- selectedItems={selectedItem ? [selectedItem] : []}
333
- highlightedIndex={highlightedIndex ?? -1}
334
- renderItem={renderItem}
335
- renderGroupHeading={renderGroupHeading}
336
- EmptyState={EmptyState}
337
- itemToKey={itemToKey}
338
- itemToString={itemToString}
339
- inputType="radio"
340
- />
341
- ) : (
342
- <NonVirtualizedAutocompleteContent
343
- items={items}
344
- getMenuProps={getMenuProps}
345
- getItemProps={(props: { index: number }) => {
346
- // Only apply getItemProps to actual items, not headings or ToggleItems
347
- const item = items[props.index];
348
- if (isGroupHeading(item)) {
349
- return {};
350
- }
351
- if (isToggleItem(item)) {
352
- // Handle ToggleItem (placeholderItem) clicks manually
353
- return {
354
- onClick: () => {
355
- if (placeholderItem && item.id === placeholderItem.id) {
356
- updateSelectedItem(null);
357
- }
358
- },
359
- };
360
- }
361
- const dataItem = item as T;
362
- // Find the index in itemsForCombobox
363
- const itemIndex = itemsForCombobox.indexOf(dataItem);
364
- if (itemIndex === -1) {
365
- return {};
366
- }
367
- return getItemProps({ item: dataItem, index: itemIndex });
368
- }}
369
- selectedItems={selectedItem ? [selectedItem] : []}
370
- highlightedIndex={highlightedIndex ?? -1}
371
- renderItem={renderItem}
372
- renderGroupHeading={renderGroupHeading}
373
- EmptyState={EmptyState}
374
- itemToKey={itemToKey}
375
- itemToString={itemToString}
376
- inputType="radio"
377
- />
378
- )}
379
- </div>
380
- }
381
- >
382
- <div>
383
- <SelectToggleButton
384
- onClick={handleButtonClick}
385
- {...triggerButtonProps}
386
- name={name}
387
- isOpen={comboboxIsOpen}
388
- type="button"
389
- >
390
- {buttonContent}
391
- </SelectToggleButton>
392
- </div>
393
- </Popout>
394
- );
395
- }
@@ -1,12 +0,0 @@
1
- // These are currently experimental components
2
- // These use the combobox pattern, but have a "dialog" popup.
3
- // The dialog content includes an inline autocomplete which is always open.
4
- // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
5
- // Still need to determine wether we should have
6
- // - The Root component be the "Combobox" and the popup be a dialog with another combobox inside
7
- // - Or the Root component simply be a "Dialog" with a combobox inside
8
- // currently it is the latter.
9
- export { SearchableSelect } from "./SearchableSelect";
10
- export type { SearchableSelectProps } from "./SearchableSelect";
11
- export { SearchableMultiSelect } from "./SearchableMultiSelect";
12
- export type { SearchableMultiSelectProps } from "./SearchableMultiSelect";