@sproutsocial/seeds-react-menu 0.6.0 → 1.0.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 (34) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +63 -0
  3. package/dist/esm/index.js +74 -31
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +34 -7
  6. package/dist/index.d.ts +34 -7
  7. package/dist/index.js +70 -27
  8. package/dist/index.js.map +1 -1
  9. package/package.json +14 -14
  10. package/src/ActionMenu/ActionMenu.stories.tsx +20 -24
  11. package/src/Autocomplete/MultiAutocomplete.tsx +0 -1
  12. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +36 -0
  13. package/src/Menu.tsx +50 -0
  14. package/src/MenuContent/MenuContent.tsx +2 -0
  15. package/src/MenuContext.tsx +2 -1
  16. package/src/MenuFooter/styles.tsx +1 -1
  17. package/src/MenuGroup/MenuGroup.tsx +5 -1
  18. package/src/MenuGroup/styles.tsx +1 -1
  19. package/src/MenuHeader/MenuHeader.tsx +10 -7
  20. package/src/MenuHeader/styles.tsx +4 -0
  21. package/src/MenuItem/MenuItem.tsx +5 -1
  22. package/src/MenuItem/styles.tsx +4 -2
  23. package/src/MenuItem/useOptionProps.tsx +2 -2
  24. package/src/MenuRoot/MenuRoot.tsx +17 -3
  25. package/src/MenuSearchInput/MenuSearchInput.tsx +30 -8
  26. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +13 -3
  27. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +51 -43
  28. package/src/NestedMenu/NestedMenuContent.tsx +6 -2
  29. package/src/NestedMenu/NestedMenuItem.tsx +5 -1
  30. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +34 -28
  31. package/src/SubmenuItem/SubmenuItem.tsx +5 -1
  32. package/src/hooks/useDownshiftDefaults.tsx +11 -1
  33. package/src/hooks/useItemFiltering.tsx +2 -0
  34. package/src/hooks/useMenuChildren.tsx +11 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.6.0",
3
+ "version": "1.0.1",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -17,19 +17,19 @@
17
17
  "test:watch": "jest --watch --coverage=false"
18
18
  },
19
19
  "dependencies": {
20
- "@sproutsocial/seeds-react-box": "^1.1.1",
21
- "@sproutsocial/seeds-react-button": "^1.2.0",
22
- "@sproutsocial/seeds-react-checkbox": "^1.3.0",
23
- "@sproutsocial/seeds-react-icon": "^1.1.1",
24
- "@sproutsocial/seeds-react-input": "^1.3.1",
25
- "@sproutsocial/seeds-react-label": "^1.0.0",
26
- "@sproutsocial/seeds-react-popout": "^2.1.0",
27
- "@sproutsocial/seeds-react-radio": "^1.2.0",
28
- "@sproutsocial/seeds-react-switch": "^1.2.0",
29
- "@sproutsocial/seeds-react-system-props": "^3.0.1",
30
- "@sproutsocial/seeds-react-theme": "^2.2.0",
31
- "@sproutsocial/seeds-react-token-input": "^1.0.1",
32
- "@sproutsocial/seeds-react-utilities": "^4.1.0",
20
+ "@sproutsocial/seeds-react-box": "^1.1.2",
21
+ "@sproutsocial/seeds-react-button": "^1.2.1",
22
+ "@sproutsocial/seeds-react-checkbox": "^1.3.1",
23
+ "@sproutsocial/seeds-react-icon": "^1.1.2",
24
+ "@sproutsocial/seeds-react-input": "^1.4.1",
25
+ "@sproutsocial/seeds-react-label": "^1.0.1",
26
+ "@sproutsocial/seeds-react-popout": "^2.1.1",
27
+ "@sproutsocial/seeds-react-radio": "^1.3.0",
28
+ "@sproutsocial/seeds-react-switch": "^1.2.1",
29
+ "@sproutsocial/seeds-react-system-props": "^3.0.2",
30
+ "@sproutsocial/seeds-react-theme": "^2.3.0",
31
+ "@sproutsocial/seeds-react-token-input": "^1.3.0",
32
+ "@sproutsocial/seeds-react-utilities": "^4.2.0",
33
33
  "downshift": "9.0.4",
34
34
  "motion": "^11.11.17"
35
35
  },
@@ -160,6 +160,7 @@ export const ActionMenuWithIcons: Story = {
160
160
  "pencil-to-square-outline",
161
161
  "envelope-outline",
162
162
  "chart-area-outline",
163
+ "circle-question-outline",
163
164
  ],
164
165
  "Trigger Appearance": "secondary",
165
166
  },
@@ -181,13 +182,9 @@ export const ActionMenuWithIcons: Story = {
181
182
  key={0}
182
183
  id="Compose"
183
184
  >
184
- <Box
185
- display="flex"
186
- alignItems="center"
187
- justifyContent="space-between"
188
- >
189
- Compose
185
+ <Box display="flex" alignItems="center" gap="300">
190
186
  <Icon name={args["icons"][0] as TypeIconName} />
187
+ Compose
191
188
  </Box>
192
189
  </MenuItem>
193
190
  <MenuItem
@@ -195,13 +192,9 @@ export const ActionMenuWithIcons: Story = {
195
192
  key={1}
196
193
  id="View Messages"
197
194
  >
198
- <Box
199
- display="flex"
200
- alignItems="center"
201
- justifyContent="space-between"
202
- >
203
- View Messages
195
+ <Box display="flex" alignItems="center" gap="300">
204
196
  <Icon name={args["icons"][1] as TypeIconName} />
197
+ View Messages
205
198
  </Box>
206
199
  </MenuItem>
207
200
  <MenuItem
@@ -209,13 +202,20 @@ export const ActionMenuWithIcons: Story = {
209
202
  key={2}
210
203
  id="Analyze post"
211
204
  >
212
- <Box
213
- display="flex"
214
- alignItems="center"
215
- justifyContent="space-between"
216
- >
217
- Analyze Post
205
+ <Box display="flex" alignItems="center" gap="300">
218
206
  <Icon name={args["icons"][2] as TypeIconName} />
207
+ Analyze Post
208
+ </Box>
209
+ </MenuItem>
210
+ <MenuItem href="https://sproutsocial.com" target="_blank" id="help">
211
+ <Box display="flex" alignItems="center" gap="300">
212
+ <Icon name={args["icons"][3] as TypeIconName} />
213
+ Visit Help Center
214
+ <Icon
215
+ name="arrow-right-up"
216
+ aria-label="open in a new window"
217
+ style={{ marginLeft: "auto" }}
218
+ />
219
219
  </Box>
220
220
  </MenuItem>
221
221
  </MenuGroup>
@@ -225,13 +225,9 @@ export const ActionMenuWithIcons: Story = {
225
225
  key={3}
226
226
  id="Settings"
227
227
  >
228
- <Box
229
- display="flex"
230
- alignItems="center"
231
- justifyContent="space-between"
232
- >
233
- Settings
228
+ <Box display="flex" alignItems="center" gap="300">
234
229
  <Icon name="gear-outline" />
230
+ Settings
235
231
  </Box>
236
232
  </MenuItem>
237
233
  </MenuGroup>
@@ -17,7 +17,6 @@ import { getSelectedItemIds } from "../MenuContext";
17
17
  import { MenuRoot } from "../MenuRoot";
18
18
 
19
19
  import type { TypeMenuItemProps } from "../MenuItem";
20
- import type { TypeMenuRootProps } from "../MenuRoot";
21
20
  import type { TypeMultiAutocompleteProps } from "./AutocompleteTypes";
22
21
 
23
22
  export const MultiAutocomplete = <
@@ -174,6 +174,21 @@ describe("Autocomplete Component", () => {
174
174
  expect(customFilter).toHaveBeenCalled();
175
175
  });
176
176
 
177
+ it("should close the dropdown when no items match the input", async () => {
178
+ const { user } = render(<Autocomplete {...defaultAutocompleteProps} />);
179
+ const inputElement = screen.getByRole("combobox");
180
+
181
+ await act(async () => {
182
+ await user.click(inputElement);
183
+ await user.type(inputElement, "something not in the list");
184
+ });
185
+
186
+ await waitFor(() => {
187
+ const listbox = screen.queryByRole("listbox");
188
+ expect(listbox).not.toBeInTheDocument();
189
+ });
190
+ });
191
+
177
192
  describe("in multiSelect mode", () => {
178
193
  it("should render multiple selected items", () => {
179
194
  render(
@@ -330,5 +345,26 @@ describe("Autocomplete Component", () => {
330
345
  })
331
346
  ).toBeInTheDocument();
332
347
  });
348
+
349
+ it("should close the dropdown when no items match the input", async () => {
350
+ const { user } = render(
351
+ <Autocomplete
352
+ {...defaultAutocompleteProps}
353
+ menuToggleElement={<AutocompleteTokenInput />}
354
+ multiSelect
355
+ />
356
+ );
357
+ const inputElement = screen.getByRole("combobox");
358
+
359
+ await act(async () => {
360
+ await user.click(inputElement);
361
+ await user.type(inputElement, "something not in the list");
362
+ });
363
+
364
+ await waitFor(() => {
365
+ const listbox = screen.queryByRole("listbox");
366
+ expect(listbox).not.toBeInTheDocument();
367
+ });
368
+ });
333
369
  });
334
370
  });
package/src/Menu.tsx ADDED
@@ -0,0 +1,50 @@
1
+ // This file exists so that react-docgen knows what components to look for.
2
+ // If we refactor index.ts to manually import and export the components,
3
+ // we could point generatePropsDocs to index.ts and delete this file.
4
+
5
+ // Primitives
6
+ import { MenuItem } from "./MenuItem";
7
+ import { MenuHeader } from "./MenuHeader";
8
+ import { MenuContent } from "./MenuContent";
9
+ import { MenuFooter } from "./MenuFooter";
10
+ import { MenuGroup } from "./MenuGroup";
11
+ import { MenuRoot } from "./MenuRoot";
12
+ import { MenuLabel } from "./MenuLabel";
13
+ import { MenuToggleButton } from "./MenuToggleButton";
14
+ import { MenuSearchInput } from "./MenuSearchInput";
15
+ import { MenuTokenInput } from "./MenuTokenInput";
16
+ import { MenuSearchTokenInput } from "./MenuSearchTokenInput";
17
+ import { SubmenuItem } from "./SubmenuItem";
18
+
19
+ // Menus
20
+ import { ActionMenu } from "./ActionMenu";
21
+ import { SingleSelectMenu } from "./SingleSelectMenu";
22
+ import { MultiSelectMenu } from "./MultiSelectMenu";
23
+ import { Autocomplete } from "./Autocomplete";
24
+ /** Not intended for individual use; use Autocomplete without multiselect prop*/
25
+ import { SingleAutocomplete } from "./Autocomplete/SingleAutocomplete";
26
+ /** Not intended for individual use; use Autocomplete with multiselect prop */
27
+ import { MultiAutocomplete } from "./Autocomplete/MultiAutocomplete";
28
+ import { NestedMenu } from "./NestedMenu";
29
+
30
+ export {
31
+ MenuItem,
32
+ MenuHeader,
33
+ MenuContent,
34
+ MenuFooter,
35
+ MenuGroup,
36
+ MenuRoot,
37
+ MenuLabel,
38
+ MenuToggleButton,
39
+ MenuSearchInput,
40
+ MenuTokenInput,
41
+ MenuSearchTokenInput,
42
+ SubmenuItem,
43
+ ActionMenu,
44
+ SingleSelectMenu,
45
+ MultiSelectMenu,
46
+ Autocomplete,
47
+ SingleAutocomplete,
48
+ MultiAutocomplete,
49
+ NestedMenu,
50
+ };
@@ -82,4 +82,6 @@ const MenuContent = <I extends TypeMenuItemProps = TypeMenuItemProps>({
82
82
  );
83
83
  };
84
84
 
85
+ MenuContent.__MENU_PRIMITIVE_TYPE = "MenuContent";
86
+
85
87
  export { MenuContent };
@@ -57,6 +57,7 @@ export interface TypeMenuContextBaseProps {
57
57
  itemsMap?: { [key: string]: TypeInternalItemProps };
58
58
  isListbox?: boolean;
59
59
  hiddenItemsMap?: Record<string, boolean>;
60
+ hiddenItemsCount?: number;
60
61
  setHiddenItemsMap?: React.Dispatch<
61
62
  React.SetStateAction<Record<string, boolean>>
62
63
  >;
@@ -202,7 +203,7 @@ export const useMenu = ({
202
203
 
203
204
  const isItemSelected = React.useCallback(
204
205
  (id: string) => checkIfItemSelected({ id, selectedItem, selectedItemIds }),
205
- [selectedItem, selectedItemIds]
206
+ [selectedItem?.id, selectedItemIds]
206
207
  );
207
208
 
208
209
  const getInputComponentProps = React.useCallback<
@@ -2,6 +2,6 @@ import styled from "styled-components";
2
2
  import { Box } from "@sproutsocial/seeds-react-box";
3
3
 
4
4
  export const StyledMenuFooterBox = styled(Box)`
5
- padding: ${(p) => p.theme.space[400]};
5
+ padding: ${({ theme }) => theme.space[350]} ${({ theme }) => theme.space[400]};
6
6
  border-top: 1px solid ${(p) => p.theme.colors.container.border.base};
7
7
  `;
@@ -25,7 +25,7 @@ import { mapMenuItems } from "../hooks/useMenuChildren";
25
25
  * @see {@link https://seeds.sproutsocial.com/components/menu-item/ | MenuItem}
26
26
  */
27
27
 
28
- export const MenuGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
28
+ const MenuGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
29
29
  titleAs = "h3",
30
30
  children: childrenProp,
31
31
  title,
@@ -68,3 +68,7 @@ export const MenuGroup = <I extends TypeMenuItemProps = TypeMenuItemProps>({
68
68
  </StyledMenuGroup>
69
69
  );
70
70
  };
71
+
72
+ MenuGroup.__MENU_PRIMITIVE_TYPE = "MenuGroup";
73
+
74
+ export { MenuGroup };
@@ -29,7 +29,7 @@ export const StyledMenuGroup = styled.li<TypeMenuGroupBaseProps>`
29
29
  `;
30
30
 
31
31
  export const StyledTitle = styled(Text.SmallSubHeadline)`
32
- font-size: ${({ theme }) => theme.typography[200].fontSize};
32
+ font-size: ${({ theme }) => theme.typography[100].fontSize};
33
33
  margin: ${({ theme }) => theme.space[400]} ${({ theme }) => theme.space[400]}
34
34
  0px ${({ theme }) => theme.space[400]};
35
35
  `;
@@ -22,17 +22,20 @@ const MenuHeader = ({
22
22
  rightAction,
23
23
  ...rest
24
24
  }: TypeMenuHeaderProps) => {
25
+ const showTopRow = title || leftAction || rightAction;
25
26
  return (
26
27
  <>
27
28
  <MenuHeaderContainer $leftAction={!!leftAction} {...rest}>
28
- <MenuHeaderFeatures $leftAction={!!leftAction}>
29
- {leftAction && leftAction}
30
- {title && (
31
- <MenuTitleText $leftAction={!!leftAction}>{title}</MenuTitleText>
32
- )}
29
+ {showTopRow && (
30
+ <MenuHeaderFeatures $leftAction={!!leftAction}>
31
+ {leftAction && leftAction}
32
+ {title && (
33
+ <MenuTitleText $leftAction={!!leftAction}>{title}</MenuTitleText>
34
+ )}
33
35
 
34
- {rightAction && <div className="right-action">{rightAction}</div>}
35
- </MenuHeaderFeatures>
36
+ {rightAction && <div className="right-action">{rightAction}</div>}
37
+ </MenuHeaderFeatures>
38
+ )}
36
39
 
37
40
  {children && <MenuChildrenContainer>{children}</MenuChildrenContainer>}
38
41
  </MenuHeaderContainer>
@@ -52,6 +52,10 @@ export const MenuTitleText = styled.h3<TypeMenuHeaderContainerProps>`
52
52
  export const MenuChildrenContainer = styled.div`
53
53
  grid-column: 1 / -1;
54
54
  margin-top: ${({ theme }) => theme.space[300]};
55
+
56
+ &:first-child {
57
+ margin-top: 0;
58
+ }
55
59
  `;
56
60
 
57
61
  export const MenuHeaderFeatures = styled.div<TypeMenuHeaderContainerProps>`
@@ -11,7 +11,7 @@ import { useOptionProps } from "./useOptionProps";
11
11
  // radio: "menuitemradio",
12
12
  // } as const;
13
13
 
14
- export const MenuItem = ({
14
+ const MenuItem = ({
15
15
  id,
16
16
  children,
17
17
  onClick,
@@ -69,3 +69,7 @@ export const MenuItem = ({
69
69
  </MenuItemContainer>
70
70
  );
71
71
  };
72
+
73
+ MenuItem.__MENU_PRIMITIVE_TYPE = "MenuItem";
74
+
75
+ export { MenuItem };
@@ -19,8 +19,10 @@ const highlightedStyles = css`
19
19
  `;
20
20
 
21
21
  const activeStyles = css`
22
- background-color: ${({ theme }) => theme.colors.listItem.background.active};
23
- color: ${({ theme }) => theme.colors.text.inverse};
22
+ border: 1px solid ${({ theme }) => theme.colors.listItem.border.base};
23
+ background-color: ${({ theme }) => theme.colors.menuItem.background.active};
24
+ color: ${({ theme }) => theme.colors.text.body};
25
+ text-decoration: none;
24
26
  `;
25
27
 
26
28
  export const MenuItemContainer = styled.li`
@@ -83,11 +83,11 @@ export const useOptionProps = ({
83
83
  inputLabelId,
84
84
  ...props
85
85
  }: TypeMenuItemProps) => {
86
- const { selectedItem, selectedItems, isListbox, isItemSelected } =
86
+ const { selectedItem, selectedItemIds, isListbox, isItemSelected } =
87
87
  useMenuContentContext();
88
88
  const selected = useMemo(
89
89
  () => isItemSelected(id),
90
- [selectedItem, selectedItems]
90
+ [selectedItem?.id, selectedItemIds]
91
91
  );
92
92
 
93
93
  const label = inputLabelId || `menu-item-child-${id}`;
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from "react";
1
+ import React, { useCallback, useEffect } from "react";
2
2
  import styled from "styled-components";
3
3
  import { Popout, type TypePopoutProps } from "@sproutsocial/seeds-react-popout";
4
4
 
@@ -9,7 +9,6 @@ import {
9
9
  type TypeMenuContextProps,
10
10
  type TypeMenuProviderProps,
11
11
  } from "../MenuContext";
12
- import type { TypeInternalItemProps } from "../types";
13
12
 
14
13
  export interface TypeMenuRootOwnProps {
15
14
  menuToggleElement: React.ReactElement<any>;
@@ -42,7 +41,14 @@ export const MenuRoot = ({
42
41
  width = "300px",
43
42
  ...contextProps
44
43
  }: TypeMenuRootProps) => {
45
- const { isOpen, openMenu, closeMenu } = contextProps;
44
+ const {
45
+ isOpen,
46
+ openMenu,
47
+ closeMenu,
48
+ inputValue,
49
+ hiddenItemsCount = 0,
50
+ items,
51
+ } = contextProps;
46
52
  const setIsOpen = useCallback(
47
53
  (newIsOpen: boolean) => {
48
54
  newIsOpen ? openMenu?.() : closeMenu?.();
@@ -50,6 +56,14 @@ export const MenuRoot = ({
50
56
  [openMenu, closeMenu]
51
57
  );
52
58
 
59
+ useEffect(() => {
60
+ if (!inputValue) return;
61
+
62
+ if (items && hiddenItemsCount >= items.length && isOpen) {
63
+ closeMenu?.();
64
+ }
65
+ }, [hiddenItemsCount, items?.length, isOpen, inputValue]);
66
+
53
67
  const menuContext = useMenu(contextProps);
54
68
 
55
69
  return (
@@ -4,22 +4,34 @@ import {
4
4
  useItemFiltering,
5
5
  type TypeGetIsItemVisibleFn,
6
6
  } from "../hooks/useItemFiltering";
7
- import { useMenuContentContext } from "../MenuContext";
7
+ import {
8
+ useMenuContentContext,
9
+ type TypeDownshiftSelectReturnValue,
10
+ } from "../MenuContext";
8
11
 
9
12
  export interface TypeMenuSearchInputProps extends TypeInputProps {
10
13
  /* A function that can be passed to override the default filtering logic */
11
14
  getIsItemVisible?: TypeGetIsItemVisibleFn;
12
15
  }
13
16
 
17
+ /**
18
+ * <input
19
+ * aria-activedescendant=""
20
+ * aria-autocomplete="list"
21
+ * aria-controls="downshift-:r1:-menu"
22
+ * aria-expanded="true"
23
+ * />
24
+ */
25
+
14
26
  /**
15
27
  * @link https://seeds.sproutsocial.com/components/menu-header/
16
28
  *
17
29
  * @description MenuSearchInput is search input that can be added to the MenuHeader to filter the items.
18
30
  */
19
-
20
31
  export const MenuSearchInput = ({
21
32
  getIsItemVisible,
22
- ...inputProps
33
+ inputProps,
34
+ ...restInputProps
23
35
  }: TypeMenuSearchInputProps) => {
24
36
  const {
25
37
  items,
@@ -37,16 +49,20 @@ export const MenuSearchInput = ({
37
49
 
38
50
  React.useEffect(() => {
39
51
  // handles when open/close state changes or if input is controlled
40
- updateFilteredItems({ inputValue: inputProps.value || "" });
41
- }, [isOpen, inputProps.value]);
52
+ updateFilteredItems({ inputValue: restInputProps.value || "" });
53
+ }, [isOpen, restInputProps.value]);
54
+
55
+ const toggleButtonProps: Partial<
56
+ ReturnType<Required<TypeDownshiftSelectReturnValue>["getToggleButtonProps"]>
57
+ > = getToggleButtonProps?.() || {};
42
58
 
43
59
  const handleKeyDown = React.useCallback(
44
60
  (e: React.KeyboardEvent<HTMLInputElement>) => {
45
61
  // prevents selecting items with space key when input is focused
46
62
  if (e.key === " ") return;
47
- getToggleButtonProps?.()["onKeyDown"](e);
63
+ toggleButtonProps.onKeyDown?.(e);
48
64
  },
49
- [getToggleButtonProps]
65
+ [toggleButtonProps.onKeyDown]
50
66
  );
51
67
 
52
68
  return (
@@ -57,7 +73,13 @@ export const MenuSearchInput = ({
57
73
  // apply onKeyDown logic for downshift keyboard accessibility support
58
74
  onKeyDown={handleKeyDown}
59
75
  autoComplete={false}
60
- {...inputProps}
76
+ aria-autocomplete="list"
77
+ inputProps={{
78
+ ["aria-activedescendant"]: toggleButtonProps["aria-activedescendant"],
79
+ ["aria-controls"]: toggleButtonProps["aria-controls"],
80
+ ...inputProps,
81
+ }}
82
+ {...restInputProps}
61
83
  />
62
84
  );
63
85
  };
@@ -3,7 +3,11 @@ import {
3
3
  useItemFiltering,
4
4
  type TypeGetIsItemVisibleFn,
5
5
  } from "../hooks/useItemFiltering";
6
- import { MenuContentContext, useMenuContentContext } from "../MenuContext";
6
+ import {
7
+ MenuContentContext,
8
+ useMenuContentContext,
9
+ type TypeDownshiftSelectReturnValue,
10
+ } from "../MenuContext";
7
11
  import {
8
12
  MenuTokenInput,
9
13
  type TypeMenuTokenInputProps,
@@ -70,13 +74,17 @@ export const MenuSearchTokenInput = ({
70
74
  updateFilteredState(controlledValue);
71
75
  }, [isOpen]);
72
76
 
77
+ const toggleButtonProps: Partial<
78
+ ReturnType<Required<TypeDownshiftSelectReturnValue>["getToggleButtonProps"]>
79
+ > = getToggleButtonProps?.() || {};
80
+
73
81
  const handleKeyDown = React.useCallback(
74
82
  (e: React.KeyboardEvent<HTMLInputElement>) => {
75
83
  // prevents selecting items with space key when input is focused
76
84
  if (e.key === " ") return;
77
- getToggleButtonProps?.()["onKeyDown"](e);
85
+ toggleButtonProps.onKeyDown?.(e);
78
86
  },
79
- [getToggleButtonProps]
87
+ [toggleButtonProps.onKeyDown]
80
88
  );
81
89
 
82
90
  return (
@@ -89,6 +97,8 @@ export const MenuSearchTokenInput = ({
89
97
  onKeyDown={handleKeyDown}
90
98
  inputProps={{
91
99
  value: internalValue,
100
+ ["aria-activedescendant"]: toggleButtonProps["aria-activedescendant"],
101
+ ["aria-controls"]: toggleButtonProps["aria-controls"],
92
102
  ...inputProps,
93
103
  }}
94
104
  MenuContext={MenuContentContext}
@@ -2,6 +2,7 @@
2
2
  import React from "react";
3
3
 
4
4
  import { Box } from "@sproutsocial/seeds-react-box";
5
+ import { action } from "@storybook/addon-actions";
5
6
  import type { Meta, StoryObj } from "@storybook/react";
6
7
  import { StorybookMenuToggleButton } from "../storybookUtilities/menu-storybook-components";
7
8
  import { TEST_BOOKS, EXTRA_BOOKS } from "../__fixtures__/menu-test-data";
@@ -15,6 +16,7 @@ import {
15
16
  MenuSearchTokenInput,
16
17
  type TypeInternalItemProps,
17
18
  type TypeMultiSelectMenuProps,
19
+ type TypeMultiSelectMenuBaseProps,
18
20
  } from "../index";
19
21
 
20
22
  type TypeMultiSelectMenuStoryProps = TypeMultiSelectMenuProps & {
@@ -135,54 +137,60 @@ export const MultiSelectMenuWithGroupsExample: Story = {
135
137
  },
136
138
  };
137
139
 
140
+ const TestControlledMultiSelectMenu = ({
141
+ inputType,
142
+ ...args
143
+ }: TypeMultiSelectMenuBaseProps & {
144
+ inputType: "checkbox" | "switch";
145
+ }) => {
146
+ const [isOpen, setIsOpen] = React.useState<boolean>(false);
147
+ const [selectedItems, setSelectedItems] = React.useState<
148
+ TypeInternalItemProps[]
149
+ >([]);
150
+ return (
151
+ <MultiSelectMenu
152
+ {...args}
153
+ isOpen={isOpen}
154
+ onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
155
+ selectedItems={selectedItems}
156
+ onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
157
+ setSelectedItems(newSelectedItems);
158
+ }}
159
+ menuToggleElement={<StorybookMenuToggleButton />}
160
+ >
161
+ <MenuContent>
162
+ <MenuGroup title="Classics" id="classics">
163
+ {TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
164
+ <MenuItem
165
+ inputType={inputType}
166
+ key={itemsToRender.id}
167
+ id={itemsToRender.id}
168
+ >
169
+ {itemsToRender.title}
170
+ </MenuItem>
171
+ ))}
172
+ </MenuGroup>
173
+ <MenuGroup title="Essentials" id="essentials">
174
+ {EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
175
+ <MenuItem
176
+ inputType={inputType}
177
+ key={itemsToRender.id}
178
+ id={itemsToRender.id}
179
+ >
180
+ {itemsToRender.title}
181
+ </MenuItem>
182
+ ))}
183
+ </MenuGroup>
184
+ </MenuContent>
185
+ </MultiSelectMenu>
186
+ );
187
+ };
138
188
  export const MultiSelectMenuControlled: Story = {
139
189
  name: "Controlled State",
140
190
  args: {
141
191
  inputType: "checkbox",
142
192
  },
143
- render: ({ inputType, ...args }) => {
144
- const [isOpen, setIsOpen] = React.useState<boolean>(false);
145
- const [selectedItems, setSelectedItems] = React.useState<
146
- TypeInternalItemProps[]
147
- >([]);
148
- return (
149
- <MultiSelectMenu
150
- isOpen={isOpen}
151
- onIsOpenChange={({ isOpen }) => setIsOpen(isOpen)}
152
- selectedItems={selectedItems}
153
- onSelectedItemsChange={({ selectedItems: newSelectedItems }) => {
154
- setSelectedItems(newSelectedItems);
155
- }}
156
- menuToggleElement={<StorybookMenuToggleButton />}
157
- {...args}
158
- >
159
- <MenuContent>
160
- <MenuGroup title="Classics" id="classics">
161
- {TEST_BOOKS.slice(0, 5).map((itemsToRender) => (
162
- <MenuItem
163
- inputType={inputType}
164
- key={itemsToRender.id}
165
- id={itemsToRender.id}
166
- >
167
- {itemsToRender.title}
168
- </MenuItem>
169
- ))}
170
- </MenuGroup>
171
- <MenuGroup title="Essentials" id="essentials">
172
- {EXTRA_BOOKS.slice(0, 5).map((itemsToRender) => (
173
- <MenuItem
174
- inputType={inputType}
175
- key={itemsToRender.id}
176
- id={itemsToRender.id}
177
- >
178
- {itemsToRender.title}
179
- </MenuItem>
180
- ))}
181
- </MenuGroup>
182
- </MenuContent>
183
- </MultiSelectMenu>
184
- );
185
- },
193
+ render: (args) => <TestControlledMultiSelectMenu {...args} />,
186
194
  };
187
195
 
188
196
  export const MultiSelectMenuWithSearch: Story = {