@sproutsocial/seeds-react-menu 0.2.3 → 0.4.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 (39) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +27 -0
  3. package/dist/esm/index.js +243 -75
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +81 -49
  6. package/dist/index.d.ts +81 -49
  7. package/dist/index.js +237 -67
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -1
  10. package/src/Autocomplete/Autocomplete.feature +14 -43
  11. package/src/Autocomplete/Autocomplete.stories.tsx +128 -0
  12. package/src/Autocomplete/Autocomplete.tsx +29 -11
  13. package/src/Autocomplete/AutocompleteInput.tsx +51 -0
  14. package/src/Autocomplete/AutocompleteTypes.ts +8 -1
  15. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +142 -89
  16. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +25 -43
  17. package/src/Autocomplete/index.ts +1 -0
  18. package/src/MenuContent/MenuContent.tsx +2 -2
  19. package/src/MenuContext.tsx +11 -5
  20. package/src/MenuGroup/MenuGroup.feature +19 -37
  21. package/src/MenuGroup/MenuGroup.tsx +18 -3
  22. package/src/MenuGroup/MenuGroupTypes.ts +6 -1
  23. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +167 -9
  24. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +27 -13
  25. package/src/MenuItem/MenuItem.tsx +7 -4
  26. package/src/MenuLabel.tsx +4 -4
  27. package/src/MenuRoot/MenuRoot.tsx +5 -3
  28. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +1 -1
  29. package/src/MultiSelectMenu/MultiSelectMenu.tsx +8 -0
  30. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +6 -6
  31. package/src/SingleSelectMenu/SingleSelectMenu.tsx +2 -2
  32. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +3 -3
  33. package/src/__fixtures__/menu-test-data.ts +36 -0
  34. package/src/__tests__/Menu.test.tsx +29 -96
  35. package/src/hooks/useItemsFromChildren.tsx +71 -21
  36. package/src/index.ts +1 -0
  37. package/src/reducers/useComboboxStateReducer.tsx +11 -8
  38. package/src/storybookUtilities/menu-storybook-components.tsx +39 -0
  39. package/src/types.ts +3 -0
@@ -12,7 +12,8 @@ export interface TypeMenuRootOwnProps {
12
12
  width?: TypePopoutProps["width"];
13
13
  }
14
14
  export interface TypeMenuRootProps
15
- extends Partial<TypeMenuProviderProps>,
15
+ extends Partial<TypeMenuProviderProps["menuProps"]>,
16
+ Pick<TypeMenuProviderProps, "children">,
16
17
  TypeMenuRootOwnProps {}
17
18
 
18
19
  const CustomPopoutContent = styled(Popout.Content)`
@@ -30,7 +31,7 @@ const CustomPopoutContent = styled(Popout.Content)`
30
31
  export const MenuRoot = ({
31
32
  children,
32
33
  menuToggleElement,
33
- popoutProps,
34
+ popoutProps: { focusLockProps, ...popoutProps } = {},
34
35
  width = "300px",
35
36
  ...contextProps
36
37
  }: TypeMenuRootProps) => {
@@ -43,7 +44,7 @@ export const MenuRoot = ({
43
44
  );
44
45
 
45
46
  return (
46
- <MenuProvider {...contextProps}>
47
+ <MenuProvider menuProps={contextProps}>
47
48
  <Popout
48
49
  menuPopout
49
50
  isOpen={!!isOpen}
@@ -55,6 +56,7 @@ export const MenuRoot = ({
55
56
  // correct jerking motion when scrolling while the Popout is open
56
57
  // returnFocus: false,
57
58
  crossFrame: false,
59
+ ...focusLockProps,
58
60
  }}
59
61
  disableWrapperAria
60
62
  {...popoutProps}
@@ -219,7 +219,7 @@ describe("MenuRoot", () => {
219
219
  >
220
220
  <MenuHeader title="Menu Header" />
221
221
  <div>Custom Banner</div>
222
- <MenuGroup title="Menu Group">
222
+ <MenuGroup title="Menu Group" id="group">
223
223
  <MenuItem id="1">Item 1</MenuItem>
224
224
  </MenuGroup>
225
225
  <MenuFooter>Menu Footer</MenuFooter>
@@ -105,6 +105,14 @@ export const MultiSelectMenu = ({
105
105
  } else {
106
106
  addSelectedItem(newSelectedItem);
107
107
  }
108
+ // If the newly selected item's group is single select, remove any other selected items in that group.
109
+ if (newSelectedItem.menuGroupProps?.isSingleSelect) {
110
+ const groupItems = selectedItems.filter(
111
+ (item) =>
112
+ item.menuGroupProps?.id === newSelectedItem.menuGroupProps?.id
113
+ );
114
+ groupItems.forEach((item) => removeSelectedItem(item));
115
+ }
108
116
  break;
109
117
  default:
110
118
  break;
@@ -35,7 +35,7 @@ describe("MultiSelectMenu", () => {
35
35
  }
36
36
  >
37
37
  <MenuContent>
38
- <MenuGroup>
38
+ <MenuGroup id="group">
39
39
  {menuItems.map((item) => (
40
40
  <MenuItem key={item.id} id={item.id} label={item.label}>
41
41
  {item.label}
@@ -78,7 +78,7 @@ describe("MultiSelectMenu", () => {
78
78
  }
79
79
  >
80
80
  <MenuContent>
81
- <MenuGroup>
81
+ <MenuGroup id="group">
82
82
  {menuItems.map((item) => (
83
83
  <MenuItem
84
84
  key={item.id}
@@ -136,7 +136,7 @@ describe("MultiSelectMenu", () => {
136
136
  }
137
137
  >
138
138
  <MenuContent>
139
- <MenuGroup>
139
+ <MenuGroup id="group">
140
140
  {menuItems.map((item) => (
141
141
  <MenuItem
142
142
  key={item.id}
@@ -199,7 +199,7 @@ describe("MultiSelectMenu", () => {
199
199
  }
200
200
  >
201
201
  <MenuContent>
202
- <MenuGroup>
202
+ <MenuGroup id="group">
203
203
  {menuItems.map((item) => (
204
204
  <MenuItem
205
205
  key={item.id}
@@ -257,7 +257,7 @@ describe("MultiSelectMenu", () => {
257
257
  selectedItems={[menuItems[1], menuItems[2]]}
258
258
  >
259
259
  <MenuContent>
260
- <MenuGroup>
260
+ <MenuGroup id="group">
261
261
  {menuItems.map((item) => (
262
262
  <MenuItem
263
263
  key={item.id}
@@ -310,7 +310,7 @@ describe("MultiSelectMenu", () => {
310
310
  onSelectedItemsChange={mockSelectedItemsChange}
311
311
  >
312
312
  <MenuContent>
313
- <MenuGroup>
313
+ <MenuGroup id="group">
314
314
  {menuItems.map((item) => (
315
315
  <MenuItem
316
316
  key={item.id}
@@ -24,11 +24,11 @@ export const SingleSelectMenu = ({
24
24
  stateReducer: externalStateReducer,
25
25
  ...useSelectProps
26
26
  }: TypeSingleSelectMenuProps) => {
27
- /** Import the manipulated map of menu items from the context */
27
+ /** Get a list of items from the children */
28
28
  const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
29
29
 
30
30
  /**
31
- * Destructure Downshift props and call the core useSelect with the new menu items and state handler
31
+ * Destructure Downshift props and call the core useSelect with items and state handler
32
32
  */
33
33
  const { isOpen, ...useSelectReturnProps } = useSelect({
34
34
  ...defaultUseSelectProps,
@@ -34,7 +34,7 @@ describe("User Behavior", () => {
34
34
  }
35
35
  >
36
36
  <MenuContent>
37
- <MenuGroup title="Menu Title">
37
+ <MenuGroup title="Menu Title" id="menu-title">
38
38
  {menuItems.map(({ id, label }) => (
39
39
  <MenuItem key={id} id={id}>
40
40
  {label}
@@ -100,7 +100,7 @@ describe("User Behavior", () => {
100
100
  }
101
101
  >
102
102
  <MenuContent>
103
- <MenuGroup title="Menu Title">
103
+ <MenuGroup title="Menu Title" id="menu-title">
104
104
  {menuItems.map(({ id, label }) => (
105
105
  <MenuItem key={id} id={id}>
106
106
  {label}
@@ -135,7 +135,7 @@ describe("User Behavior", () => {
135
135
  menuToggleElement={<MenuToggleButton>Click me</MenuToggleButton>}
136
136
  >
137
137
  <MenuContent>
138
- <MenuGroup title="Menu Title">
138
+ <MenuGroup title="Menu Title" id="menu-title">
139
139
  {menuItems.map(({ id, label }) => (
140
140
  <MenuItem key={id} id={id}>
141
141
  {label}
@@ -0,0 +1,36 @@
1
+ interface TypeBook {
2
+ id: string;
3
+ author: string;
4
+ title: string;
5
+ }
6
+
7
+ export const TEST_BOOKS: TypeBook[] = [
8
+ { id: "book-1", author: "Harper Lee", title: "To Kill a Mockingbird" },
9
+ { id: "book-2", author: "Leo Tolstoy", title: "War and Peace" },
10
+ { id: "book-3", author: "Fyodor Dostoyevsy", title: "The Idiot" },
11
+ { id: "book-4", author: "Oscar Wilde", title: "A Picture of Dorian Gray" },
12
+ { id: "book-5", author: "George Orwell", title: "Nineteen Eighty Four" },
13
+ { id: "book-6", author: "Jane Austen", title: "Pride and Prejudice" },
14
+ { id: "book-7", author: "Marcus Aurelius", title: "Meditations" },
15
+ {
16
+ id: "book-8",
17
+ author: "Fyodor Dostoevsky",
18
+ title: "The Brothers Karamazov",
19
+ },
20
+ { id: "book-9", author: "Lev Tolstoy", title: "Anna Karenina" },
21
+ {
22
+ id: "book-10",
23
+ author: "Fyodor Dostoevsky",
24
+ title: "Crime and Punishment",
25
+ },
26
+ ];
27
+
28
+ export const EXTRA_BOOKS: TypeBook[] = [
29
+ { id: "book-11", author: "F. Scott Fitzgerald", title: "The Great Gatsby" },
30
+ { id: "book-12", author: "J. D. Salinger", title: "The Catcher in the Rye" },
31
+ { id: "book-13", author: "J. R. R. Tolkien", title: "The Lord of the Rings" },
32
+ { id: "book-14", author: "John Steinbeck", title: "The Grapes of Wrath" },
33
+ { id: "book-15", author: "George Orwell", title: "Animal Farm" },
34
+ { id: "book-16", author: "Jack Kerouac", title: "On the Road" },
35
+ { id: "book-17", author: "Aldous Huxley", title: "Brave New World" },
36
+ ];
@@ -9,6 +9,8 @@ import {
9
9
  ActionMenu,
10
10
  MultiSelectMenu,
11
11
  SingleSelectMenu,
12
+ Autocomplete,
13
+ AutocompleteInput,
12
14
  MenuToggleButton,
13
15
  MenuContent,
14
16
  MenuItem,
@@ -19,19 +21,18 @@ describe("Menu", () => {
19
21
  { name: "ActionMenu", Component: ActionMenu },
20
22
  { name: "MultiSelectMenu", Component: MultiSelectMenu },
21
23
  { name: "SingleSelectMenu", Component: SingleSelectMenu },
22
- ])("$name", ({ Component }) => {
24
+ { name: "Autocomplete", Component: Autocomplete },
25
+ ])("$name", ({ name, Component }) => {
26
+ const toggleElement = (
27
+ <>
28
+ {name === "Autocomplete" && <AutocompleteInput />}
29
+ <MenuToggleButton role="button" aria-label="Open Menu">
30
+ Open
31
+ </MenuToggleButton>
32
+ </>
33
+ );
23
34
  it("should render the trigger button", () => {
24
- render(
25
- <Component
26
- menuToggleElement={
27
- <MenuToggleButton role="button" aria-label="Open Menu">
28
- Open
29
- </MenuToggleButton>
30
- }
31
- >
32
- Menu
33
- </Component>
34
- );
35
+ render(<Component menuToggleElement={toggleElement}>Menu</Component>);
35
36
  const menuTrigger = screen.getByRole("button", { name: "Open Menu" });
36
37
  expect(menuTrigger).toBeInTheDocument();
37
38
  expect(screen.queryByText("Menu")).not.toBeInTheDocument();
@@ -39,14 +40,7 @@ describe("Menu", () => {
39
40
 
40
41
  it("should default to open if initialIsOpen is true", () => {
41
42
  render(
42
- <Component
43
- menuToggleElement={
44
- <MenuToggleButton role="button" aria-label="Open Menu">
45
- Open
46
- </MenuToggleButton>
47
- }
48
- initialIsOpen
49
- >
43
+ <Component menuToggleElement={toggleElement} initialIsOpen>
50
44
  Menu
51
45
  </Component>
52
46
  );
@@ -60,15 +54,7 @@ describe("Menu", () => {
60
54
 
61
55
  it("should open when trigger button is clicked", async () => {
62
56
  const { user } = render(
63
- <Component
64
- menuToggleElement={
65
- <MenuToggleButton role="button" aria-label="Open Menu">
66
- Open
67
- </MenuToggleButton>
68
- }
69
- >
70
- Menu
71
- </Component>
57
+ <Component menuToggleElement={toggleElement}>Menu</Component>
72
58
  );
73
59
 
74
60
  // Check that Menu is not open yet
@@ -85,15 +71,7 @@ describe("Menu", () => {
85
71
 
86
72
  it("should open when trigger button is focused and enter key pressed", async () => {
87
73
  const { user } = render(
88
- <Component
89
- menuToggleElement={
90
- <MenuToggleButton role="button" aria-label="Open Menu">
91
- Open
92
- </MenuToggleButton>
93
- }
94
- >
95
- Menu
96
- </Component>
74
+ <Component menuToggleElement={toggleElement}>Menu</Component>
97
75
  );
98
76
 
99
77
  // Check that Menu is not open yet
@@ -111,15 +89,7 @@ describe("Menu", () => {
111
89
 
112
90
  it("should open when trigger button is focused and space key pressed", async () => {
113
91
  const { user } = render(
114
- <Component
115
- menuToggleElement={
116
- <MenuToggleButton role="button" aria-label="Open Menu">
117
- Open
118
- </MenuToggleButton>
119
- }
120
- >
121
- Menu
122
- </Component>
92
+ <Component menuToggleElement={toggleElement}>Menu</Component>
123
93
  );
124
94
 
125
95
  // Check that Menu is not open yet
@@ -137,14 +107,7 @@ describe("Menu", () => {
137
107
 
138
108
  it("should close when trigger button is clicked while open", async () => {
139
109
  const { user } = render(
140
- <Component
141
- menuToggleElement={
142
- <MenuToggleButton role="button" aria-label="Open Menu">
143
- Open
144
- </MenuToggleButton>
145
- }
146
- initialIsOpen
147
- >
110
+ <Component menuToggleElement={toggleElement} initialIsOpen>
148
111
  Menu
149
112
  </Component>
150
113
  );
@@ -167,11 +130,7 @@ describe("Menu", () => {
167
130
  const mockOnIsOpenChange = jest.fn();
168
131
  const { user } = render(
169
132
  <Component
170
- menuToggleElement={
171
- <MenuToggleButton role="button" aria-label="Open Menu">
172
- Open
173
- </MenuToggleButton>
174
- }
133
+ menuToggleElement={toggleElement}
175
134
  onIsOpenChange={mockOnIsOpenChange}
176
135
  initialIsOpen
177
136
  >
@@ -199,11 +158,7 @@ describe("Menu", () => {
199
158
  const { user } = render(
200
159
  <div>
201
160
  <Component
202
- menuToggleElement={
203
- <MenuToggleButton role="button" aria-label="Open Menu">
204
- Open
205
- </MenuToggleButton>
206
- }
161
+ menuToggleElement={toggleElement}
207
162
  onIsOpenChange={mockOnIsOpenChange}
208
163
  initialIsOpen
209
164
  >
@@ -230,11 +185,7 @@ describe("Menu", () => {
230
185
  const mockOnSelectedItemChange = jest.fn();
231
186
  const { user } = render(
232
187
  <Component
233
- menuToggleElement={
234
- <MenuToggleButton role="button" aria-label="Open Menu">
235
- Open
236
- </MenuToggleButton>
237
- }
188
+ menuToggleElement={toggleElement}
238
189
  onSelectedItemChange={mockOnSelectedItemChange}
239
190
  >
240
191
  <MenuContent>
@@ -266,11 +217,7 @@ describe("Menu", () => {
266
217
  const mockOnSelectedItemChange = jest.fn();
267
218
  const { user } = render(
268
219
  <Component
269
- menuToggleElement={
270
- <MenuToggleButton role="button" aria-label="Open Menu">
271
- Open
272
- </MenuToggleButton>
273
- }
220
+ menuToggleElement={toggleElement}
274
221
  onSelectedItemChange={mockOnSelectedItemChange}
275
222
  >
276
223
  <MenuContent>
@@ -295,14 +242,12 @@ describe("Menu", () => {
295
242
  });
296
243
 
297
244
  it("should call onSelectedItemChange when item selected with space", async () => {
245
+ if (name === "Autocomplete") return;
246
+
298
247
  const mockOnSelectedItemChange = jest.fn();
299
248
  const { user } = render(
300
249
  <Component
301
- menuToggleElement={
302
- <MenuToggleButton role="button" aria-label="Open Menu">
303
- Open
304
- </MenuToggleButton>
305
- }
250
+ menuToggleElement={toggleElement}
306
251
  onSelectedItemChange={mockOnSelectedItemChange}
307
252
  >
308
253
  <MenuContent>
@@ -330,11 +275,7 @@ describe("Menu", () => {
330
275
  const mockOnIsOpenChange = jest.fn();
331
276
  const { user } = render(
332
277
  <Component
333
- menuToggleElement={
334
- <MenuToggleButton role="button" aria-label="Open Menu">
335
- Open
336
- </MenuToggleButton>
337
- }
278
+ menuToggleElement={toggleElement}
338
279
  onIsOpenChange={mockOnIsOpenChange}
339
280
  >
340
281
  <MenuContent>
@@ -362,11 +303,7 @@ describe("Menu", () => {
362
303
  const mockOnHighlightedIndexChange = jest.fn();
363
304
  const { user } = render(
364
305
  <Component
365
- menuToggleElement={
366
- <MenuToggleButton role="button" aria-label="Open Menu">
367
- Open
368
- </MenuToggleButton>
369
- }
306
+ menuToggleElement={toggleElement}
370
307
  onHighlightedIndexChange={mockOnHighlightedIndexChange}
371
308
  >
372
309
  <MenuContent>
@@ -396,11 +333,7 @@ describe("Menu", () => {
396
333
  const mockStateReducer = jest.fn((state, { changes }) => changes);
397
334
  const { user } = render(
398
335
  <Component
399
- menuToggleElement={
400
- <MenuToggleButton role="button" aria-label="Open Menu">
401
- Open
402
- </MenuToggleButton>
403
- }
336
+ menuToggleElement={toggleElement}
404
337
  stateReducer={mockStateReducer}
405
338
  >
406
339
  <MenuContent>
@@ -421,7 +354,7 @@ describe("Menu", () => {
421
354
  await user.keyboard("{arrowdown}{enter}");
422
355
 
423
356
  // Check that stateReducer is called
424
- await waitFor(() => expect(mockStateReducer).toHaveBeenCalledTimes(4));
357
+ await waitFor(() => expect(mockStateReducer).toHaveBeenCalled());
425
358
  });
426
359
  });
427
360
  });
@@ -1,13 +1,20 @@
1
1
  import React, { type ReactNode, useMemo } from "react";
2
- import type { TypeMenuItemProps } from "../MenuItem";
2
+ import { MenuItem, type TypeMenuItemProps } from "../MenuItem";
3
3
  import type { TypeMenuContext } from "../MenuContext";
4
+ import type { TypeMenuGroupProps } from "../MenuGroup/index";
5
+ import type { TypeDefaultItemProps } from "../types";
4
6
 
5
7
  export interface TypeUseItemsFromChildrenProps {
6
8
  children: ReactNode;
7
9
  }
8
- export interface TypeItemElements {
9
- element: React.ReactElement<TypeMenuItemProps>;
10
- parents: readonly React.ReactNode[];
10
+ export type TypeItemElement = React.ReactElement<TypeMenuItemProps>;
11
+ export type TypeMenuGroupElement = React.ReactElement<TypeMenuGroupProps>;
12
+
13
+ export interface TypeMenuChildren {
14
+ type: "item" | "group";
15
+ element:
16
+ | React.ReactElement<TypeMenuGroupProps>
17
+ | React.ReactElement<TypeMenuItemProps>;
11
18
  }
12
19
 
13
20
  // https://stackoverflow.com/questions/69119016/find-specific-children-components-regardless-of-depth-in-react
@@ -17,7 +24,6 @@ const walkAllChildren = (
17
24
  ) => {
18
25
  const walk = (element: ReactNode, parents: readonly ReactNode[]) => {
19
26
  if (element === null || element === undefined) return;
20
- // console.log("element", element);
21
27
  callback(element, parents);
22
28
  const newParents = [...parents, element];
23
29
  const children = (element as any).props?.children;
@@ -43,37 +49,81 @@ const checkIfIsElement = (element: ReactNode, name: string): boolean => {
43
49
  };
44
50
 
45
51
  const getAllMenuItems = (children: ReactNode) => {
46
- const allMenuItems: TypeItemElements[] = [];
47
- // console.log("children", children);
48
- walkAllChildren(children, (element, parents) => {
52
+ const orderedElements: TypeMenuChildren[] = [];
53
+ walkAllChildren(children, (element) => {
49
54
  if (checkIfIsElement(element, "MenuItem")) {
50
- allMenuItems.push({
51
- element: element as TypeItemElements["element"],
52
- parents,
55
+ orderedElements.push({
56
+ type: "item",
57
+ element: element as TypeItemElement,
58
+ });
59
+ } else if (checkIfIsElement(element, "MenuGroup")) {
60
+ orderedElements.push({
61
+ type: "group",
62
+ element: element as TypeItemElement,
53
63
  });
54
64
  }
55
65
  });
56
- return allMenuItems;
66
+ return { orderedElements };
57
67
  };
58
68
 
59
69
  export const useItemsFromChildren = ({
60
70
  children,
61
71
  }: TypeUseItemsFromChildrenProps) => {
62
72
  const { allMenuItems, menuItemsMap } = useMemo(() => {
63
- const allMenuItemElements = getAllMenuItems(children);
73
+ const { orderedElements } = getAllMenuItems(children);
64
74
  const menuItemsMap: TypeMenuContext["itemsMap"] = {};
65
- const allMenuItems: TypeMenuContext["items"] = allMenuItemElements.map(
66
- ({ element }, index) => {
75
+ const allMenuItems: TypeDefaultItemProps[] = [];
76
+ let index = 0;
77
+
78
+ orderedElements.forEach(({ type, element }) => {
79
+ if (type === "group") {
80
+ const menuGroup = element as TypeMenuGroupElement;
81
+ const items = menuGroup.props.items;
82
+ if (items) {
83
+ items.forEach((menuItem) => {
84
+ const item = {
85
+ id: menuItem.id,
86
+ index: index++,
87
+ disabled: menuItem.disabled,
88
+ menuItemProps: menuItem,
89
+ menuGroupProps: element.props,
90
+ };
91
+ allMenuItems.push(item);
92
+ menuItemsMap[`${item.id}`] = item;
93
+ });
94
+ } else if (menuGroup.props.children) {
95
+ let children = menuGroup.props.children as TypeItemElement[];
96
+ // If there is only one child, it will be an object. Make it an array.
97
+ if (!Array.isArray(children)) {
98
+ children = [children];
99
+ }
100
+ children.forEach((menuItem) => {
101
+ const item = {
102
+ id: menuItem.props.id,
103
+ index: index++,
104
+ disabled: menuItem.props.disabled,
105
+ menuItemProps: menuItem.props,
106
+ menuGroupProps: element.props,
107
+ };
108
+ allMenuItems.push(item);
109
+ menuItemsMap[`${item.id}`] = item;
110
+ });
111
+ }
112
+ } else {
113
+ // Items in groups would have already been added to the map.
114
+ if (menuItemsMap[`${element.props.id}`]) return;
115
+ const menuItem = element as TypeItemElement;
67
116
  const item = {
68
- id: element.props.id,
69
- disabled: element.props.disabled,
70
- index,
71
- menuItemProps: element.props,
117
+ id: menuItem.props.id,
118
+ index: index++,
119
+ disabled: menuItem.props.disabled,
120
+ menuItemProps: menuItem.props,
72
121
  };
122
+ allMenuItems.push(item);
73
123
  menuItemsMap[`${item.id}`] = item;
74
- return item;
75
124
  }
76
- );
125
+ });
126
+
77
127
  return { allMenuItems, menuItemsMap };
78
128
  }, [children]);
79
129
 
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from "./MenuToggleButton";
12
12
  export * from "./ActionMenu";
13
13
  export * from "./SingleSelectMenu";
14
14
  export * from "./MultiSelectMenu";
15
+ export * from "./Autocomplete";
15
16
 
16
17
  // Utilities
17
18
  export * from "./MenuContext";
@@ -8,12 +8,12 @@ export const useComboboxStateReducer: Required<
8
8
  const { type, changes } = actionAndChanges;
9
9
  switch (type) {
10
10
  /** If downshift is blurred by tabbing within the menu, stay open */
11
- // case useCombobox.stateChangeTypes.InputBlur:
12
- // return {
13
- // ...changes,
14
- // // keep menu open on blur
15
- // isOpen: true,
16
- // };
11
+ case useCombobox.stateChangeTypes.InputBlur:
12
+ return {
13
+ ...changes,
14
+ // keep menu open on blur
15
+ isOpen: true,
16
+ };
17
17
 
18
18
  /** Return standard change payload */
19
19
  default:
@@ -21,6 +21,9 @@ export const useComboboxStateReducer: Required<
21
21
  }
22
22
  };
23
23
 
24
- export const defaultUseComboboxProps = {
25
- isItemDisabled: (item) => !!item.disabled,
24
+ export const defaultUseComboboxProps: Partial<
25
+ UseComboboxProps<TypeDefaultItemProps>
26
+ > = {
27
+ // disabling while item.hidden so that hidden items are ignored by downshift
28
+ isItemDisabled: (item) => Boolean(item.disabled || item.hidden),
26
29
  };
@@ -0,0 +1,39 @@
1
+ import styled from "styled-components";
2
+ import { Box } from "@sproutsocial/seeds-react-box";
3
+ import { Icon } from "@sproutsocial/seeds-react-icon";
4
+ import { useMenuContext, MenuToggleButton } from "../index";
5
+
6
+ export interface TypeStyledChevronProps {
7
+ $isOpen?: boolean;
8
+ }
9
+
10
+ const StyledChevron = styled.div<TypeStyledChevronProps>`
11
+ margin-left: ${({ theme }) => theme.space[300]};
12
+ ${({ $isOpen }) =>
13
+ $isOpen &&
14
+ `
15
+ transform: rotate(-180deg);
16
+ `}
17
+ transition: transform 0.15s;
18
+ `;
19
+
20
+ interface StorybookMenuToggleButtonProps {
21
+ buttonText?: string;
22
+ }
23
+
24
+ export const StorybookMenuToggleButton = ({
25
+ buttonText,
26
+ }: StorybookMenuToggleButtonProps) => {
27
+ const { isOpen } = useMenuContext();
28
+
29
+ return (
30
+ <MenuToggleButton appearance="secondary">
31
+ <Box display="flex" justifyContent="space-between">
32
+ {buttonText ? buttonText : <span>Select book</span>}
33
+ <StyledChevron $isOpen={isOpen}>
34
+ <Icon name="chevron-down" />
35
+ </StyledChevron>
36
+ </Box>
37
+ </MenuToggleButton>
38
+ );
39
+ };
package/src/types.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { TypeMenuItemProps } from "./MenuItem";
2
+ import type { TypeMenuGroupProps } from "./MenuGroup";
2
3
 
3
4
  export interface TypeDefaultItemProps {
4
5
  id: string;
5
6
  index: number;
6
7
  disabled?: boolean;
8
+ hidden?: boolean;
7
9
  menuItemProps?: TypeMenuItemProps;
10
+ menuGroupProps?: TypeMenuGroupProps;
8
11
  }
9
12
 
10
13
  export type TypeNotEmptyChildren =