@sproutsocial/seeds-react-menu 0.4.1 → 0.5.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 (37) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +17 -0
  3. package/dist/esm/index.js +221 -137
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +126 -101
  6. package/dist/index.d.ts +126 -101
  7. package/dist/index.js +236 -151
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/ActionMenu/ActionMenu.tsx +10 -3
  11. package/src/ActionMenu/ActionMenuTypes.ts +10 -6
  12. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +0 -22
  13. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
  14. package/src/Autocomplete/Autocomplete.stories.tsx +24 -0
  15. package/src/Autocomplete/Autocomplete.tsx +10 -4
  16. package/src/Autocomplete/AutocompleteTypes.ts +6 -3
  17. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +34 -3
  18. package/src/MenuContent/MenuContent.tsx +14 -1
  19. package/src/MenuContent/MenuContentTypes.ts +5 -5
  20. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +17 -1
  21. package/src/MenuGroup/MenuGroup.feature +6 -6
  22. package/src/MenuGroup/MenuGroup.tsx +2 -1
  23. package/src/MenuGroup/MenuGroupTypes.ts +4 -6
  24. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
  25. package/src/MenuItem/MenuItem.tsx +0 -1
  26. package/src/MultiSelectMenu/MultiSelectMenu.tsx +10 -3
  27. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -6
  28. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +0 -43
  29. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
  30. package/src/SingleSelectMenu/SingleSelectMenu.tsx +10 -3
  31. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +7 -5
  32. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
  33. package/src/__tests__/Menu.test.tsx +38 -0
  34. package/src/hooks/useMenuChildren.tsx +210 -0
  35. package/src/index.ts +1 -1
  36. package/src/types.ts +26 -1
  37. package/src/hooks/useItemsFromChildren.tsx +0 -131
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ import {
5
5
  type UseSelectStateChange,
6
6
  } from "downshift";
7
7
  import type { TypeDefaultItemProps } from "../types";
8
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
8
+ import { useMenuChildren } from "../hooks/useMenuChildren";
9
9
  import {
10
10
  defaultUseSelectProps,
11
11
  useSelectStateReducer,
@@ -13,6 +13,7 @@ import {
13
13
  import { MenuRoot } from "../MenuRoot";
14
14
  import { itemToString } from "../utils/itemToString";
15
15
  import type { TypeActionMenuProps } from "./ActionMenuTypes";
16
+ import { MenuItem } from "../MenuItem/index";
16
17
 
17
18
  // Manually fire the onClick handler when Enter or Space is pressed
18
19
  const handleStateChange = (
@@ -82,11 +83,17 @@ const handleStateChange = (
82
83
  * @see {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MultiSelectMenu | MultiSelectMenu}
83
84
  */
84
85
  export const ActionMenu = ({
85
- children,
86
+ children: childrenProp,
87
+ items,
88
+ MenuItemComponent = MenuItem,
86
89
  stateReducer: externalStateReducer,
87
90
  ...useSelectProps
88
91
  }: TypeActionMenuProps) => {
89
- const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
92
+ const { allMenuItems, menuItemsMap, children } = useMenuChildren({
93
+ children: childrenProp,
94
+ items,
95
+ MenuItemComponent,
96
+ });
90
97
  const [currentSelectedItem, setCurrentSelectedItem] =
91
98
  React.useState<UseSelectState<TypeDefaultItemProps>["selectedItem"]>(null);
92
99
 
@@ -1,12 +1,16 @@
1
- import { type TypeMenuRootProps, type TypeMenuRootOwnProps } from "../MenuRoot";
1
+ import { type TypeMenuRootOwnProps } from "../MenuRoot";
2
2
  import type { TypeDownshiftSelectProps } from "../MenuContext";
3
+ import type { TypeChildrenOrItems } from "../types";
3
4
 
4
5
  type TypeInvalidMenuProps =
5
6
  | "initialSelectedItem"
6
7
  | "defaultSelectedItem"
7
- | "selectedItem";
8
- export interface TypeActionMenuProps
8
+ | "selectedItem"
9
+ // We omit this so we can retype it.
10
+ | "items";
11
+
12
+ interface TypeActionMenuBaseProps
9
13
  extends TypeMenuRootOwnProps,
10
- Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {
11
- children: TypeMenuRootProps["children"];
12
- }
14
+ Omit<TypeDownshiftSelectProps, TypeInvalidMenuProps> {}
15
+
16
+ export type TypeActionMenuProps = TypeActionMenuBaseProps & TypeChildrenOrItems;
@@ -11,28 +11,6 @@ import { ActionMenu } from "../ActionMenu";
11
11
  import { MenuContent, MenuItem, MenuToggleButton } from "../../index";
12
12
 
13
13
  describe("ActionMenu", () => {
14
- it("should render the MenuItems when provided as children", async () => {
15
- render(
16
- <ActionMenu
17
- menuToggleElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
18
- >
19
- <MenuContent>
20
- <MenuItem id="test-item-1">Item 1</MenuItem>
21
- <MenuItem id="test-item-2">Item 2</MenuItem>
22
- </MenuContent>
23
- </ActionMenu>
24
- );
25
-
26
- expect(screen.queryByRole("menu")).not.toBeInTheDocument();
27
-
28
- const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
29
- fireEvent.click(toggleBtn);
30
-
31
- await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
32
- expect(screen.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
33
- expect(screen.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
34
- });
35
-
36
14
  test.each([
37
15
  {
38
16
  action: "click",
@@ -1,4 +1,10 @@
1
- import { ActionMenu, MenuContent, MenuHeader, MenuFooter } from "../../index";
1
+ import {
2
+ ActionMenu,
3
+ MenuContent,
4
+ MenuHeader,
5
+ MenuFooter,
6
+ MenuItem,
7
+ } from "../../index";
2
8
 
3
9
  export const ActionMenuTypeTest = () => (
4
10
  <>
@@ -14,6 +20,17 @@ export const ActionMenuTypeTest = () => (
14
20
  <MenuContent>Test</MenuContent>
15
21
  <MenuFooter>Test</MenuFooter>
16
22
  </ActionMenu>
23
+ {/* Valid ActionMenu allows items to be passed using the items prop */}
24
+ <ActionMenu
25
+ menuToggleElement={<>test</>}
26
+ items={[{ children: "Item Name", id: "item-1" }]}
27
+ />
28
+ {/* Valid ActionMenu allows items prop to be rendered using a MenuItemComponent prop */}
29
+ <ActionMenu
30
+ menuToggleElement={<>test</>}
31
+ items={[{ children: "Item Name", id: "item-1" }]}
32
+ MenuItemComponent={MenuItem}
33
+ />
17
34
  {/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
18
35
  <ActionMenu
19
36
  menuToggleElement={<>test</>}
@@ -34,8 +51,20 @@ export const ActionMenuTypeTest = () => (
34
51
  >
35
52
  Test
36
53
  </ActionMenu>
37
- {/* @ts-expect-error - when no children provided */}
38
- <ActionMenu menuToggleElement="test" />
54
+ {/* @ts-expect-error - when no children or items provided */}
55
+ <ActionMenu menuToggleElement={<>test</>} />
56
+ {/* @ts-expect-error - when both children and MenuItemComponent provided */}
57
+ <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
58
+ Children
59
+ </ActionMenu>
60
+ {/* @ts-expect-error - when both children and items provided */}
61
+ <ActionMenu menuToggleElement={<>test</>} items={[]}>
62
+ Children
63
+ </ActionMenu>
64
+ {/* @ts-expect-error - when MenuItemComponent is of the wrong type */}
65
+ <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuContent}>
66
+ Children
67
+ </ActionMenu>
39
68
 
40
69
  <ActionMenu
41
70
  menuToggleElement={<>test</>}
@@ -62,6 +62,30 @@ export const AutocompleteTextInputExample: Story = {
62
62
  },
63
63
  };
64
64
 
65
+ export const AutocompleteWithItemsAsProps: Story = {
66
+ name: "With Items as Props",
67
+ render: (args) => {
68
+ return (
69
+ <Autocomplete
70
+ itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
71
+ menuToggleElement={
72
+ <>
73
+ <MenuLabel htmlFor="my-combobox" mb={300}>
74
+ Choose a book
75
+ </MenuLabel>
76
+ <AutocompleteInput />
77
+ </>
78
+ }
79
+ items={TEST_BOOKS.map(({ id, title }) => ({
80
+ children: title,
81
+ id,
82
+ }))}
83
+ {...(args as object)}
84
+ />
85
+ );
86
+ },
87
+ };
88
+
65
89
  export const AutocompleteTextWithButtonExample: Story = {
66
90
  name: "Text Input w/ Button",
67
91
  render: (args) => {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { useCombobox, type UseComboboxState } from "downshift";
3
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
3
+ import { useMenuChildren } from "../hooks/useMenuChildren";
4
4
  import {
5
5
  defaultUseComboboxProps,
6
6
  useComboboxStateReducer,
@@ -9,6 +9,7 @@ import { itemToString as defaultItemToString } from "../utils/itemToString";
9
9
  import { MenuRoot } from "../MenuRoot";
10
10
  import type { TypeAutocompleteProps } from "./AutocompleteTypes";
11
11
  import type { TypeDefaultItemProps } from "../types";
12
+ import { MenuItem } from "../MenuItem/index";
12
13
 
13
14
  /**
14
15
  * @link https://seeds.sproutsocial.com/components/multiselect-menu/
@@ -19,15 +20,20 @@ import type { TypeDefaultItemProps } from "../types";
19
20
  */
20
21
 
21
22
  export const Autocomplete = ({
22
- children,
23
+ children: childrenProp,
23
24
  stateReducer: externalStateReducer,
25
+ items: itemsProp,
26
+ MenuItemComponent = MenuItem,
24
27
  itemToString = defaultItemToString,
25
28
  getIsItemVisible = (item, inputValue) =>
26
29
  itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase()),
27
30
  ...useComboboxProps
28
31
  }: TypeAutocompleteProps) => {
29
- /** Get a list of items from the children */
30
- const { allMenuItems } = useItemsFromChildren({ children });
32
+ const { allMenuItems, children } = useMenuChildren({
33
+ children: childrenProp,
34
+ items: itemsProp,
35
+ MenuItemComponent,
36
+ });
31
37
  const [items, setItems] = React.useState(allMenuItems);
32
38
 
33
39
  /**
@@ -3,13 +3,16 @@ import type {
3
3
  TypeDownshiftComboboxProps,
4
4
  TypeMenuContext,
5
5
  } from "../MenuContext";
6
+ import type { TypeChildrenOrItems } from "../types";
6
7
 
7
- export interface TypeAutocompleteProps
8
+ interface TypeAutocompleteBaseProps
8
9
  extends TypeMenuRootOwnProps,
9
- TypeDownshiftComboboxProps {
10
- children: TypeMenuRootProps["children"];
10
+ Omit<TypeDownshiftComboboxProps, "items"> {
11
11
  getIsItemVisible?: (
12
12
  item: TypeMenuContext["items"][number],
13
13
  inputValue: string
14
14
  ) => boolean;
15
15
  }
16
+
17
+ export type TypeAutocompleteProps = TypeAutocompleteBaseProps &
18
+ TypeChildrenOrItems;
@@ -1,5 +1,11 @@
1
1
  import * as React from "react";
2
- import { Autocomplete, MenuContent, MenuHeader, MenuFooter } from "../../index";
2
+ import {
3
+ Autocomplete,
4
+ MenuContent,
5
+ MenuHeader,
6
+ MenuFooter,
7
+ MenuItem,
8
+ } from "../../index";
3
9
 
4
10
  export const AutocompleteTypeTest = () => (
5
11
  <>
@@ -15,6 +21,17 @@ export const AutocompleteTypeTest = () => (
15
21
  <MenuContent>Test</MenuContent>
16
22
  <MenuFooter>Test</MenuFooter>
17
23
  </Autocomplete>
24
+ {/* Valid Autocomplete allows items to be passed using the items prop */}
25
+ <Autocomplete
26
+ menuToggleElement={<>test</>}
27
+ items={[{ children: "Item Name", id: "item-1" }]}
28
+ />
29
+ {/* Valid Autocomplete allows items to be rendered using MenuItemComponent */}
30
+ <Autocomplete
31
+ menuToggleElement={<>test</>}
32
+ items={[{ children: "Item Name", id: "item-1" }]}
33
+ MenuItemComponent={MenuItem}
34
+ />
18
35
  {/* Valid Autocomplete allows getIsItemVisible function */}
19
36
  <Autocomplete menuToggleElement={<>test</>} getIsItemVisible={() => true}>
20
37
  Test
@@ -39,8 +56,22 @@ export const AutocompleteTypeTest = () => (
39
56
  >
40
57
  Test
41
58
  </Autocomplete>
42
- {/* @ts-expect-error - when no children provided */}
43
- <Autocomplete menuToggleElement="test" />
59
+ {/* @ts-expect-error - when no children or items provided */}
60
+ <Autocomplete menuToggleElement={<>test</>} />
61
+ {/* @ts-expect-error - when both children and items provided */}
62
+ <Autocomplete menuToggleElement={<>test</>} items={[]}>
63
+ Test
64
+ </Autocomplete>
65
+ {/* @ts-expect-error - when MenuItemComponent is passed with children */}
66
+ <Autocomplete menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
67
+ Test
68
+ </Autocomplete>
69
+ <Autocomplete
70
+ menuToggleElement={<>test</>}
71
+ items={[]}
72
+ /* @ts-expect-error - when MenuItemComponent has the wrong type */
73
+ MenuItemComponent={MenuFooter}
74
+ ></Autocomplete>
44
75
  <Autocomplete
45
76
  menuToggleElement={<>test</>}
46
77
  /* @ts-expect-error - isOpen can't be a string */
@@ -1,6 +1,8 @@
1
1
  import type { TypeMenuContentProps } from "./MenuContentTypes";
2
2
  import { StyledMenuContent } from "./styles";
3
3
  import { useMenuContext } from "../MenuContext";
4
+ import { MenuItem } from "../MenuItem/index";
5
+ import { mapMenuItems } from "../hooks/useMenuChildren";
4
6
 
5
7
  /**
6
8
  * @link https://seeds.sproutsocial.com/components/menu-content/
@@ -27,11 +29,22 @@ import { useMenuContext } from "../MenuContext";
27
29
 
28
30
  const MenuContent = ({
29
31
  id = "menu_content",
30
- children,
32
+ children: childrenProp,
33
+ items,
34
+ MenuItemComponent = MenuItem,
31
35
  ...rest
32
36
  }: TypeMenuContentProps) => {
33
37
  const { getToggleButtonProps, getMenuProps, isListbox } = useMenuContext();
34
38
 
39
+ const children =
40
+ childrenProp || !items || !items.length
41
+ ? childrenProp
42
+ : mapMenuItems({ items, MenuItemComponent });
43
+
44
+ if (!children) {
45
+ return null;
46
+ }
47
+
35
48
  return (
36
49
  <StyledMenuContent
37
50
  autoFocus
@@ -1,7 +1,7 @@
1
- import React from "react";
2
1
  import { type HTMLMotionProps } from "motion/react";
3
- import type { ReactNode, RefObject } from "react";
2
+ import type { TypeChildrenOrItems } from "../types";
4
3
 
5
- export interface TypeMenuContentProps extends HTMLMotionProps<"ul"> {
6
- children: ReactNode;
7
- }
4
+ interface TypeMenuContentBaseProps extends HTMLMotionProps<"ul"> {}
5
+
6
+ export type TypeMenuContentProps = TypeMenuContentBaseProps &
7
+ TypeChildrenOrItems;
@@ -4,6 +4,13 @@ export const MenuContentTypeTest = () => (
4
4
  <>
5
5
  {/* MenuContent allows children */}
6
6
  <MenuContent>Test</MenuContent>
7
+ {/* MenuContent allows items prop*/}
8
+ <MenuContent items={[{ id: "1", children: "1" }]} />
9
+ {/* MenuContent allows items prop with MenuItemComponent*/}
10
+ <MenuContent
11
+ items={[{ id: "1", children: "1" }]}
12
+ MenuItemComponent={MenuItem}
13
+ />
7
14
  {/* MenuContent allows children with MenuItems */}
8
15
  <MenuContent>
9
16
  <MenuItem id="1">Test 1</MenuItem>
@@ -12,7 +19,16 @@ export const MenuContentTypeTest = () => (
12
19
  {/* MenuContent accepts ul props */}
13
20
  <MenuContent aria-label="test label">Test</MenuContent>
14
21
 
15
- {/* @ts-expect-error - MenuContent requires children */}
22
+ {/* @ts-expect-error - MenuContent requires children or items */}
16
23
  <MenuContent />
24
+ {/* @ts-expect-error - when both children and items provided */}
25
+ <MenuContent items={[]}>Test</MenuContent>
26
+ {/* @ts-expect-error - when MenuItemComponent is passed with children */}
27
+ <MenuContent MenuItemComponent={MenuItem}>Test</MenuContent>
28
+ <MenuContent
29
+ items={[]}
30
+ /* @ts-expect-error - when MenuItemComponent has the wrong type */
31
+ MenuItemComponent={({ text }) => <div>{text}</div>}
32
+ ></MenuContent>
17
33
  </>
18
34
  );
@@ -76,9 +76,9 @@ Feature: Menu Group
76
76
  Then MenuRoot expects it to be of type <propType>
77
77
 
78
78
  Examples: Prop Types
79
- | propName | propType |
80
- | children | ReactNode |
81
- | title | string |
82
- | items | TypeMenuItemProps[] |
83
- | isSingleSelect | boolean |
84
- | MenuItemComponent | tyepof MenuItem |
79
+ | propName | propType |
80
+ | children | ReactNode |
81
+ | title | string |
82
+ | items | TypeMenuItemProps[] |
83
+ | isSingleSelect | boolean |
84
+ | MenuItemComponent | React.ComponentType<TypeMenuItemProps> |
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import type { TypeMenuGroupProps } from "./MenuGroupTypes";
3
3
  import { StyledMenuGroup, StyledTitle, StyledMenuGroupUl } from "./styles";
4
4
  import { MenuItem } from "../MenuItem";
5
+ import { mapMenuItems } from "../hooks/useMenuChildren";
5
6
 
6
7
  /**
7
8
  * @link https://seeds.sproutsocial.com/components/menu-group/
@@ -40,7 +41,7 @@ export const MenuGroup = ({
40
41
  const children =
41
42
  childrenProp || !items || !items.length
42
43
  ? childrenProp
43
- : items.map((item) => <MenuItemComponent key={item.id} {...item} />);
44
+ : mapMenuItems({ items, MenuItemComponent });
44
45
 
45
46
  if (!children) {
46
47
  return null;
@@ -1,4 +1,3 @@
1
- import type { ReactNode } from "react";
2
1
  import type {
3
2
  TypeBorderSystemProps,
4
3
  TypeColorSystemProps,
@@ -9,7 +8,7 @@ import type {
9
8
  TypeSpaceSystemProps,
10
9
  TypeTypographySystemProps,
11
10
  } from "@sproutsocial/seeds-react-system-props";
12
- import { MenuItem, type TypeMenuItemProps } from "../MenuItem/index";
11
+ import type { TypeChildrenOrItems } from "../types";
13
12
 
14
13
  export interface TypeMenuGroupStyledProps
15
14
  extends TypeBorderSystemProps,
@@ -21,14 +20,13 @@ export interface TypeMenuGroupStyledProps
21
20
  TypeSpaceSystemProps,
22
21
  TypeTypographySystemProps {}
23
22
 
24
- export interface TypeMenuGroupProps
23
+ interface TypeBaseMenuGroupProps
25
24
  extends TypeMenuGroupStyledProps,
26
25
  Omit<React.ComponentPropsWithoutRef<"ul">, "color"> {
27
26
  id: string;
28
27
  isSingleSelect?: boolean;
29
28
  title?: string;
30
- children?: ReactNode;
31
- items?: TypeMenuItemProps[];
32
- MenuItemComponent?: typeof MenuItem;
33
29
  titleAs?: string | React.ComponentType<any>;
34
30
  }
31
+
32
+ export type TypeMenuGroupProps = TypeBaseMenuGroupProps & TypeChildrenOrItems;
@@ -10,6 +10,14 @@ export const MenuGroupTypeTest = () => (
10
10
  <MenuItem id="1">Test 1</MenuItem>
11
11
  <MenuItem id="2">Test 2</MenuItem>
12
12
  </MenuGroup>
13
+ {/* MenuGroup allows items prop */}
14
+ <MenuGroup id="group" items={[{ id: "1", children: "1" }]} />
15
+ {/* MenuGroup allows items prop with MenuItemComponent */}
16
+ <MenuGroup
17
+ id="group"
18
+ items={[{ id: "1", children: "1" }]}
19
+ MenuItemComponent={MenuItem}
20
+ />
13
21
  {/* MenuGroup allows isSingleSelect prop */}
14
22
  <MenuGroup id="group-1" isSingleSelect={true}>
15
23
  <MenuItem id="1">Test 1</MenuItem>
@@ -34,10 +42,21 @@ export const MenuGroupTypeTest = () => (
34
42
  <MenuGroup id="group" border="1px solid black" bg="white" width="150px">
35
43
  Test
36
44
  </MenuGroup>
37
- {/* MenuGroup allows children prop */}
38
- <MenuGroup id="group">Test</MenuGroup>
39
- {/* MenuGroup allows items prop */}
40
- <MenuGroup id="group" items={[{ id: "1", children: "1" }]} />
45
+
46
+ {/* @ts-expect-error - MenuGroup requires children or items */}
47
+ <MenuGroup id="group" />
48
+ {/* @ts-expect-error - MenuGroup does not allow children and items together */}
49
+ <MenuGroup id="group" items={[{ id: "1", children: "1" }]}>
50
+ Test
51
+ </MenuGroup>
52
+ {/* @ts-expect-error - MenuGroup does not allow MenuItemComponent without items */}
53
+ <MenuGroup id="group" MenuItemComponent={MenuItem}>
54
+ Test
55
+ </MenuGroup>
56
+ {/* @ts-expect-error - MenuGroup validates the type of MenuItemComponent */}
57
+ <MenuGroup id="group" MenuItemComponent={Text}>
58
+ Test
59
+ </MenuGroup>
41
60
 
42
61
  {/* @ts-expect-error - MenuGroup title is invalid as number */}
43
62
  <MenuGroup id="group" title={123}>
@@ -1,4 +1,3 @@
1
- import { useMemo } from "react";
2
1
  import { useMenuContext } from "../MenuContext";
3
2
  import type { TypeDefaultItemProps } from "../types";
4
3
  import type { TypeMenuItemProps } from "./MenuItemTypes";
@@ -4,7 +4,7 @@ import {
4
4
  useMultipleSelection,
5
5
  type UseSelectState,
6
6
  } from "downshift";
7
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
7
+ import { useMenuChildren } from "../hooks/useMenuChildren";
8
8
  import {
9
9
  defaultUseSelectProps,
10
10
  useSelectStateReducer,
@@ -13,6 +13,7 @@ import { itemToString } from "../utils/itemToString";
13
13
  import { MenuRoot } from "../MenuRoot";
14
14
  import type { TypeMultiSelectMenuProps } from "./MultiSelectMenuTypes";
15
15
  import type { TypeDefaultItemProps } from "../types";
16
+ import { MenuItem } from "../MenuItem/index";
16
17
 
17
18
  const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
18
19
  state,
@@ -43,14 +44,20 @@ const selectReducerForMultiSelect: TypeMultiSelectMenuProps["stateReducer"] = (
43
44
  */
44
45
 
45
46
  export const MultiSelectMenu = ({
46
- children,
47
+ children: childrenProp,
48
+ items,
49
+ MenuItemComponent = MenuItem,
47
50
  stateReducer: externalStateReducer,
48
51
  getA11yMultiStatusMessage,
49
52
  onMultiSelectStateChange,
50
53
  multiSelectStateReducer,
51
54
  ...useSelectProps
52
55
  }: TypeMultiSelectMenuProps) => {
53
- const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
56
+ const { allMenuItems, menuItemsMap, children } = useMenuChildren({
57
+ children: childrenProp,
58
+ items,
59
+ MenuItemComponent,
60
+ });
54
61
 
55
62
  const multiSelectOverrides = Object.entries({
56
63
  ...useSelectProps,
@@ -1,12 +1,14 @@
1
- import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
1
+ import type { TypeMenuRootOwnProps } from "../MenuRoot";
2
2
  import type {
3
3
  TypeDownshiftSelectProps,
4
4
  TypeDownshiftMultiSelectProps,
5
5
  } from "../MenuContext";
6
+ import type { TypeChildrenOrItems } from "../types";
6
7
 
7
- export interface TypeMultiSelectMenuProps
8
+ interface TypeMultiSelectMenuBaseProps
8
9
  extends TypeMenuRootOwnProps,
9
- TypeDownshiftSelectProps,
10
- TypeDownshiftMultiSelectProps {
11
- children: TypeMenuRootProps["children"];
12
- }
10
+ Omit<TypeDownshiftSelectProps, "items">,
11
+ Omit<TypeDownshiftMultiSelectProps, "items"> {}
12
+
13
+ export type TypeMultiSelectMenuProps = TypeMultiSelectMenuBaseProps &
14
+ TypeChildrenOrItems;
@@ -25,49 +25,6 @@ describe("MultiSelectMenu", () => {
25
25
  cleanup();
26
26
  });
27
27
 
28
- it("should render the MenuItems when provided as children", async () => {
29
- const { user } = render(
30
- <MultiSelectMenu
31
- menuToggleElement={
32
- <MenuToggleButton aria-label="testable menu" role="button">
33
- Testable Menu
34
- </MenuToggleButton>
35
- }
36
- >
37
- <MenuContent>
38
- <MenuGroup id="group">
39
- {menuItems.map((item) => (
40
- <MenuItem key={item.id} id={item.id} label={item.label}>
41
- {item.label}
42
- </MenuItem>
43
- ))}
44
- </MenuGroup>
45
- </MenuContent>
46
- </MultiSelectMenu>
47
- );
48
-
49
- // Check that the menu is closed
50
- expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
51
-
52
- const toggleButton = screen.getByRole("button", { name: "testable menu" });
53
-
54
- // Open Menu
55
- await user.click(toggleButton);
56
-
57
- await waitFor(() => {
58
- // Check that the menu is open
59
- expect(screen.getByRole("listbox")).toBeInTheDocument();
60
- // Check that the menu items are present
61
- expect(
62
- screen.getByRole("option", { name: "Item 1" })
63
- ).toBeInTheDocument();
64
- expect(
65
- // Check that the menu items are present
66
- screen.getByRole("option", { name: "Item 2" })
67
- ).toBeInTheDocument();
68
- });
69
- });
70
-
71
28
  it("should allow for multiple items to be selected", async () => {
72
29
  const { user } = render(
73
30
  <MultiSelectMenu
@@ -3,6 +3,7 @@ import {
3
3
  MultiSelectMenu,
4
4
  MenuContent,
5
5
  MenuHeader,
6
+ MenuItem,
6
7
  MenuFooter,
7
8
  } from "../../index";
8
9
 
@@ -20,6 +21,17 @@ export const MultiSelectMenuTypeTest = () => (
20
21
  <MenuContent>Test</MenuContent>
21
22
  <MenuFooter>Test</MenuFooter>
22
23
  </MultiSelectMenu>
24
+ {/* Valid MultiSelectMenu allows items to be passed using the items prop */}
25
+ <MultiSelectMenu
26
+ menuToggleElement={<>test</>}
27
+ items={[{ children: "Item Name", id: "item-1" }]}
28
+ />
29
+ {/* Valid MultiSelectMenu allows items prop to be rendered using a MenuItemComponent prop */}
30
+ <MultiSelectMenu
31
+ menuToggleElement={<>test</>}
32
+ items={[{ children: "Item Name", id: "item-1" }]}
33
+ MenuItemComponent={MenuItem}
34
+ />
23
35
  {/* Valid MultiSelectMenu accepts Downshift props and functions for useSelect */}
24
36
  <MultiSelectMenu
25
37
  menuToggleElement={<>test</>}
@@ -52,8 +64,23 @@ export const MultiSelectMenuTypeTest = () => (
52
64
  >
53
65
  Test
54
66
  </MultiSelectMenu>
55
- {/* @ts-expect-error - when no children provided */}
56
- <MultiSelectMenu menuToggleElement="test" />
67
+ {/* @ts-expect-error - when no children or items provided */}
68
+ <MultiSelectMenu menuToggleElement={<>test</>} />
69
+ {/* @ts-expect-error - when both children and MenuItemComponent provided */}
70
+ <MultiSelectMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
71
+ Children
72
+ </MultiSelectMenu>
73
+ {/* @ts-expect-error - when both children and items provided */}
74
+ <MultiSelectMenu menuToggleElement={<>test</>} items={[]}>
75
+ Children
76
+ </MultiSelectMenu>
77
+ <MultiSelectMenu
78
+ menuToggleElement={<>test</>}
79
+ /* @ts-expect-error - when MenuItemComponent is of the wrong type */
80
+ MenuItemComponent={MenuContent}
81
+ >
82
+ Children
83
+ </MultiSelectMenu>
57
84
 
58
85
  <MultiSelectMenu
59
86
  menuToggleElement={<>test</>}