@sproutsocial/seeds-react-menu 0.1.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 (91) hide show
  1. package/.eslintrc.js +4 -0
  2. package/.turbo/turbo-build.log +21 -0
  3. package/CHANGELOG.md +46 -0
  4. package/dist/esm/index.js +2651 -0
  5. package/dist/esm/index.js.map +1 -0
  6. package/dist/index.d.mts +197 -0
  7. package/dist/index.d.ts +197 -0
  8. package/dist/index.js +2670 -0
  9. package/dist/index.js.map +1 -0
  10. package/jest.config.js +12 -0
  11. package/package.json +50 -0
  12. package/src/ActionMenu/ActionMenu.feature +20 -0
  13. package/src/ActionMenu/ActionMenu.tsx +113 -0
  14. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +99 -0
  15. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +73 -0
  16. package/src/ActionMenu/index.ts +1 -0
  17. package/src/ComboBox/ComboBox.feature +96 -0
  18. package/src/ComboBox/TokenInput.feature +84 -0
  19. package/src/MenuContent/MenuContent.tsx +28 -0
  20. package/src/MenuContent/MenuContentTypes.ts +7 -0
  21. package/src/MenuContent/__tests__/MenuContent.test.tsx +59 -0
  22. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -0
  23. package/src/MenuContent/index.tsx +2 -0
  24. package/src/MenuContent/styles.tsx +10 -0
  25. package/src/MenuContext.tsx +116 -0
  26. package/src/MenuFooter/MenuFooter.feature +30 -0
  27. package/src/MenuFooter/MenuFooter.tsx +7 -0
  28. package/src/MenuFooter/MenuFooterTypes.ts +1 -0
  29. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +76 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.typetest.tsx +20 -0
  31. package/src/MenuFooter/index.ts +2 -0
  32. package/src/MenuFooter/styles.tsx +7 -0
  33. package/src/MenuGroup/MenuGroup.feature +114 -0
  34. package/src/MenuGroup/MenuGroup.tsx +33 -0
  35. package/src/MenuGroup/MenuGroupTypes.ts +29 -0
  36. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +95 -0
  37. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +45 -0
  38. package/src/MenuGroup/index.tsx +8 -0
  39. package/src/MenuGroup/styles.tsx +37 -0
  40. package/src/MenuHeader/MenuHeader.feature +59 -0
  41. package/src/MenuHeader/MenuHeader.tsx +30 -0
  42. package/src/MenuHeader/MenuHeaderTypes.ts +26 -0
  43. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +67 -0
  44. package/src/MenuHeader/__tests__/MenuHeader.typetest.tsx +27 -0
  45. package/src/MenuHeader/index.ts +2 -0
  46. package/src/MenuHeader/styles.tsx +75 -0
  47. package/src/MenuItem/MenuItem.feature +88 -0
  48. package/src/MenuItem/MenuItem.tsx +44 -0
  49. package/src/MenuItem/MenuItemTypes.ts +37 -0
  50. package/src/MenuItem/__tests__/MenuItem.test.tsx +228 -0
  51. package/src/MenuItem/__tests__/MenuItem.typetest.tsx +41 -0
  52. package/src/MenuItem/index.ts +5 -0
  53. package/src/MenuItem/styles.tsx +71 -0
  54. package/src/MenuItemSelectable/MenuItemSelectable.feature +99 -0
  55. package/src/MenuItemSelectable/MenuItemSelectable.tsx +136 -0
  56. package/src/MenuItemSelectable/MenuItemSelectableTypes.ts +15 -0
  57. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.test.tsx +303 -0
  58. package/src/MenuItemSelectable/__tests__/MenuItemSelectable.typetest.tsx +56 -0
  59. package/src/MenuItemSelectable/index.ts +5 -0
  60. package/src/MenuItemSelectable/styles.tsx +21 -0
  61. package/src/MenuLabel.tsx +12 -0
  62. package/src/MenuRoot/MenuRoot.feature +93 -0
  63. package/src/MenuRoot/MenuRoot.tsx +70 -0
  64. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +197 -0
  65. package/src/MenuRoot/__tests__/MenuRoot.typetest.tsx +76 -0
  66. package/src/MenuRoot/index.ts +1 -0
  67. package/src/MenuToggleButton.tsx +29 -0
  68. package/src/MultiSelectMenu/MultiSelectMenu.feature +70 -0
  69. package/src/MultiSelectMenu/MultiSelectMenu.tsx +133 -0
  70. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +8 -0
  71. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +349 -0
  72. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +78 -0
  73. package/src/MultiSelectMenu/index.ts +1 -0
  74. package/src/SingleSelectMenu/SingleSelectMenu.feature +23 -0
  75. package/src/SingleSelectMenu/SingleSelectMenu.tsx +56 -0
  76. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -0
  77. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.test.tsx +167 -0
  78. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +95 -0
  79. package/src/SingleSelectMenu/index.ts +3 -0
  80. package/src/__tests__/Menu.test.tsx +423 -0
  81. package/src/constants.ts +17 -0
  82. package/src/hooks/useItemsFromChildren.tsx +81 -0
  83. package/src/index.ts +20 -0
  84. package/src/menuTestingUtils.tsx +72 -0
  85. package/src/reducers/__tests__/useSelectStateReducer.test.tsx +37 -0
  86. package/src/reducers/useSelectStateReducer.tsx +27 -0
  87. package/src/styled.d.ts +7 -0
  88. package/src/types.ts +16 -0
  89. package/src/utils/itemToString.ts +5 -0
  90. package/tsconfig.json +12 -0
  91. package/tsup.config.ts +11 -0
@@ -0,0 +1,10 @@
1
+ import styled from "styled-components";
2
+ import { focusRing } from "@sproutsocial/seeds-react-mixins";
3
+
4
+ export const StyledMenuContent = styled.ul`
5
+ margin: 0;
6
+ padding: ${({ theme }) => theme.space[300]} 0;
7
+ &:focus {
8
+ ${focusRing}
9
+ }
10
+ `;
@@ -0,0 +1,116 @@
1
+ import React, {
2
+ createContext,
3
+ useMemo,
4
+ useContext,
5
+ type RefObject,
6
+ } from "react";
7
+ import type {
8
+ UseSelectReturnValue,
9
+ UseSelectProps,
10
+ UseMultipleSelectionReturnValue,
11
+ UseMultipleSelectionProps,
12
+ } from "downshift";
13
+ import type { TypeDefaultItemProps, TypeNotEmptyChildren } from "./types";
14
+
15
+ export interface TypeDownshiftMultiSelectProps
16
+ extends Partial<UseSelectProps<TypeDefaultItemProps>>,
17
+ Partial<
18
+ Omit<
19
+ UseMultipleSelectionProps<TypeDefaultItemProps>,
20
+ "stateReducer" | "onStateChange" | "getA11yStatusMessage"
21
+ >
22
+ > {
23
+ // Multi Select Props with conflicting useSelect prop names
24
+ getA11yMultiStatusMessage?: UseMultipleSelectionProps<TypeDefaultItemProps>["getA11yStatusMessage"];
25
+ onMultiSelectStateChange?: UseMultipleSelectionProps<TypeDefaultItemProps>["onStateChange"];
26
+ multiSelectStateReducer?: UseMultipleSelectionProps<TypeDefaultItemProps>["stateReducer"];
27
+ }
28
+
29
+ export interface TypeMenuContextProps
30
+ extends Partial<UseSelectReturnValue<TypeDefaultItemProps>>,
31
+ Partial<UseMultipleSelectionReturnValue<TypeDefaultItemProps>>,
32
+ TypeDownshiftMultiSelectProps {
33
+ items: TypeDefaultItemProps[];
34
+ isListbox?: boolean;
35
+ }
36
+
37
+ export interface TypeMenuContext extends TypeMenuContextProps {
38
+ itemsMap: { [key: string]: TypeDefaultItemProps };
39
+ }
40
+
41
+ export interface TypeMenuProviderProps extends Partial<TypeMenuContextProps> {
42
+ children: TypeNotEmptyChildren;
43
+ }
44
+
45
+ /**
46
+ * Object for Initializing the Menu Context
47
+ */
48
+ const defaultMenuContext: TypeMenuContext = {
49
+ items: [],
50
+ itemsMap: {},
51
+ };
52
+
53
+ /**
54
+ * Hook for initializing Menu context
55
+ */
56
+ export const useMenu = ({
57
+ items = [],
58
+ isListbox = false,
59
+ isOpen = false,
60
+ ...rest
61
+ }: Partial<TypeMenuContextProps> = {}): TypeMenuContext => {
62
+ const menuProps = useMemo(() => ({ ...rest }), [rest]);
63
+
64
+ const itemsMap = useMemo(
65
+ () =>
66
+ items.reduce((map, item) => {
67
+ map[`${item.id}`] = item;
68
+ return map;
69
+ }, {} as TypeMenuContext["itemsMap"]),
70
+ [items]
71
+ );
72
+
73
+ return {
74
+ isOpen,
75
+ isListbox,
76
+ items,
77
+ itemsMap,
78
+ ...menuProps,
79
+ };
80
+ };
81
+
82
+ /**
83
+ * MenuContext factory, sets the type and the initial object
84
+ */
85
+ export const MenuContext = createContext<TypeMenuContext>(defaultMenuContext);
86
+
87
+ /**
88
+ * Provider Component sets the context values into the provider
89
+ */
90
+ export const MenuProvider = ({
91
+ children,
92
+ ...overrides
93
+ }: TypeMenuProviderProps) => {
94
+ const filterContext = useMenu(overrides);
95
+
96
+ return (
97
+ <MenuContext.Provider value={{ ...filterContext }}>
98
+ {children}
99
+ </MenuContext.Provider>
100
+ );
101
+ };
102
+
103
+ /**
104
+ * Utility hook for consuming MenuContext
105
+ */
106
+ export const useMenuContext = (): TypeMenuContext => {
107
+ const menuContextValue = useContext(MenuContext);
108
+
109
+ if (menuContextValue === null) {
110
+ throw new Error(
111
+ "useMenuContext must be used within a <MenuContext.Provider />"
112
+ );
113
+ }
114
+
115
+ return menuContextValue;
116
+ };
@@ -0,0 +1,30 @@
1
+ Feature: Menu Footer
2
+
3
+ The bottom area of a Menu.
4
+
5
+ Rule: API
6
+ Background:
7
+ Given an engineer is implementing a MenuFooter
8
+
9
+ Scenario Outline: MenuFooter contains children
10
+ When <child component(s)> provided
11
+ Then the MenuFooter should render the <component(s)>
12
+
13
+ Examples: child components
14
+ | child component(s) | component(s) |
15
+ | a Link is | Link |
16
+ | a Switch is | Switch |
17
+ | a Button is | Button |
18
+ | a Link and a Button are | Link and Button |
19
+
20
+ Scenario: MenuFooter extends Box
21
+ When Box features are used
22
+ Then the MenuFooter responds as a Box would
23
+
24
+ Scenario Outline: MenuFooter checks its types
25
+ When <propName> is passed a value
26
+ Then MenuFooter expects it to be of type <propType>
27
+
28
+ Examples: Prop Types
29
+ | propName | propType |
30
+ | children | ReactNode |
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { TypeMenuFooterProps } from "./MenuFooterTypes";
3
+ import { StyledMenuFooterBox } from "./styles";
4
+
5
+ export const MenuFooter = ({ children, ...rest }: TypeMenuFooterProps) => {
6
+ return <StyledMenuFooterBox {...rest}>{children}</StyledMenuFooterBox>;
7
+ };
@@ -0,0 +1 @@
1
+ export type { TypeBoxProps as TypeMenuFooterProps } from "@sproutsocial/seeds-react-box";
@@ -0,0 +1,76 @@
1
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
2
+ import { MenuFooter, MenuRoot } from "../../index";
3
+ import { Button } from "@sproutsocial/seeds-react-button";
4
+ import { Switch } from "@sproutsocial/seeds-react-switch";
5
+
6
+ describe("MenuFooter API", () => {
7
+ const contextTests = [
8
+ {
9
+ child: "Button",
10
+ TestComponent: <Button role="button">Find me!</Button>,
11
+ },
12
+ {
13
+ child: "Switcher",
14
+ TestComponent: (
15
+ <Switch
16
+ role="button"
17
+ checked={false}
18
+ onClick={() => {}}
19
+ aria-label="Find me!"
20
+ />
21
+ ),
22
+ },
23
+ {
24
+ child: "Link",
25
+ TestComponent: <button role="button">Find me!</button>,
26
+ },
27
+ ];
28
+
29
+ it.each(contextTests)(
30
+ "$child contains children",
31
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
+ ({ child, TestComponent }) => {
33
+ render(
34
+ <MenuFooter>
35
+ Hello world!
36
+ {TestComponent}
37
+ </MenuFooter>
38
+ );
39
+
40
+ const testBtn = screen.getByRole("button");
41
+
42
+ const parentComponent = screen.getByText("Hello world!");
43
+ expect(parentComponent).toContainElement(testBtn);
44
+ }
45
+ );
46
+
47
+ it.each(contextTests)(
48
+ "$child contains children when in MenuRoot",
49
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
50
+ ({ child, TestComponent }) => {
51
+ render(
52
+ <MenuRoot
53
+ isOpen
54
+ triggerElement={<Button onClick={jest.fn()}>Open</Button>}
55
+ >
56
+ <MenuFooter>
57
+ Hello world!
58
+ {TestComponent}
59
+ </MenuFooter>
60
+ </MenuRoot>
61
+ );
62
+
63
+ const testBtn = screen.getByRole("button", { name: "Find me!" });
64
+
65
+ const parentComponent = screen.getByText("Hello world!");
66
+ expect(parentComponent).toContainElement(testBtn);
67
+ }
68
+ );
69
+
70
+ it("extends Box", () => {
71
+ render(<MenuFooter bg="#ff0000">Hello world!</MenuFooter>);
72
+
73
+ const menuFooter = screen.getByText("Hello world!");
74
+ expect(menuFooter).toHaveStyle("background: #ff0000");
75
+ });
76
+ });
@@ -0,0 +1,20 @@
1
+ import { MenuFooter } from "../MenuFooter";
2
+
3
+ export const MenuFooterTypeTest = () => (
4
+ <>
5
+ <MenuFooter>Test...</MenuFooter>
6
+ {/* @ts-expect-error - test that invalid flexDirection is rejected */}
7
+ <MenuFooter flexDirection="invalid" />
8
+ {/* @ts-expect-error - test that invalid type is rejected */}
9
+ <MenuFooter type="invalid" />
10
+ <MenuFooter gap={100}>Test...</MenuFooter>
11
+ <MenuFooter gap={1} rowGap={400} columnGap={999} />;
12
+ <MenuFooter gap="string" rowGap="string" columnGap="string" />;
13
+ <MenuFooter
14
+ gap={[1, "string"]}
15
+ rowGap={[1, "string"]}
16
+ columnGap={[1, "string"]}
17
+ />
18
+ ;
19
+ </>
20
+ );
@@ -0,0 +1,2 @@
1
+ export * from "./MenuFooter";
2
+ export * from "./MenuFooterTypes";
@@ -0,0 +1,7 @@
1
+ import styled from "styled-components";
2
+ import { Box } from "@sproutsocial/seeds-react-box";
3
+
4
+ export const StyledMenuFooterBox = styled(Box)`
5
+ padding: ${(p) => p.theme.space[350]};
6
+ border-top: 1px solid ${(p) => p.theme.colors.container.border.base};
7
+ `;
@@ -0,0 +1,114 @@
1
+ Feature: Menu Group
2
+
3
+ The component responsible for grouping items for a Menu.
4
+
5
+ Rule: User Behavior
6
+ Background:
7
+ Given an end user is using the component
8
+
9
+ Scenario: MenuGroup is accessible to screen readers when menu opened
10
+ Given a screen reader is being used
11
+ When a menu is opened
12
+ And the first group receives focus
13
+ Then the first group is announced by the screen reader
14
+
15
+ Scenario: MenuGroup is accessible to screen readers when there are 2 groups
16
+ Given a screen reader is being used
17
+ When a navigating between two groups
18
+ Then the new group is announced by the screen reader
19
+
20
+ Scenario Outline: MenuGroup uses select type
21
+ When the MenuGroup has <group type>
22
+ And MenuContext is set to <menu type>
23
+ Then <selection behavior>
24
+
25
+ Examples:
26
+ | group type | menu type | selection behavior |
27
+ | single | single | the menu uses the default behavior |
28
+ | multiple | multiple | the menu uses the default behavior |
29
+ | single | multiple | the group behaves like a single select |
30
+ | multiple | single | the group throws an error and uses default behavior |
31
+ | default | single | the menu uses the default behavior |
32
+ | default | multiple | the menu uses the default behavior |
33
+
34
+ Rule: API
35
+ Background:
36
+ Given an engineer is implementing the component
37
+
38
+ Scenario: MenuGroup has a title
39
+ When the "title" prop is set
40
+ Then the title is visible at the top of the group
41
+
42
+ Scenario: MenuGroup has no title
43
+ When the "title" prop is not set
44
+ Then no title is visible at the top of the group
45
+
46
+ Scenario: MenuGroup has no items provided
47
+ When no children are provided to MenuGroup
48
+ And no items prop is provided
49
+ Then the MenuGroup will show a warning in the console
50
+ And will not render the empty group
51
+
52
+ Scenario: MenuGroup can take items as children
53
+ When MenuItem components are passed as children to a MenuGroup
54
+ Then the MenuGroup will render them inside of the group
55
+
56
+ Scenario: MenuGroup items prop might be empty
57
+ Given a list of items is empty
58
+ When that list of items is provided to the items prop on a MenuGroup
59
+ Then the MenuGroup will show a warning in the console
60
+ And will not render the empty group
61
+
62
+ Scenario: MenuGroup can take items via a prop
63
+ Given a list of items is not empty
64
+ And the items are a valid object shape
65
+ When that list of items is provided to the items prop on a MenuGroup
66
+ Then the MenuGroup will render the items using the MenuItem component
67
+
68
+ Scenario: MenuGroup can use the items prop with a custom MenuItem
69
+ Given a list of items is not empty
70
+ And the items are a valid object shape
71
+ When that list of items is provided to the items prop on a MenuGroup
72
+ And a valid component is provided to the customMenuItem prop
73
+ Then the MenuGroup will render the items using the customMenuItem component
74
+
75
+ Scenario: MenuGroup ignores the menuItemType without items prop
76
+ When a MenuGroup has menuItemType set
77
+ And is not provided with a valid items prop
78
+ Then the menuItemType will be ignored
79
+
80
+ Scenario Outline: MenuGroup can set the type for all MenuItems
81
+ When menuItemType is set to <menu item type>
82
+ And MenuGroup is provided with a valid items prop
83
+ Then each MenuItem rendered by the MenuGroup with use this type
84
+
85
+ Examples: Prop Types
86
+ | menu item type |
87
+ | undefined (default) |
88
+ | checkbox |
89
+ | radio |
90
+ | switch |
91
+
92
+ Scenario Outline: MenuGroup has custom content
93
+ When <custom content> is present at the <location>
94
+ Then <displayed content> will render
95
+
96
+ Examples:
97
+ | custom content | location | displayed content |
98
+ | a red Banner | top | a red Banner at the top of the group |
99
+ | hint text | bottom | hint text at the bottom of the group |
100
+ | text | top and bottom | text at the top and bottom of the group |
101
+ | no content | top | no content at the top of the group |
102
+ | no content | bottom | no content at the bottom of the group |
103
+
104
+ Scenario Outline: MenuRoot checks its types
105
+ When <propName> is passed a value
106
+ Then MenuRoot expects it to be of type <propType>
107
+
108
+ Examples: Prop Types
109
+ | propName | propType |
110
+ | children | ReactNode |
111
+ | title | string |
112
+ | items | TypeItem[] |
113
+ | menuItemType | TypeMenuItemProps['type'] |
114
+ | customMenuItem | tyepof MenuItem |
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import type { TypeMenuGroupProps } from "./MenuGroupTypes";
3
+ import { StyledMenuGroup, StyledTitle, StyledMenuGroupUl } from "./styles";
4
+
5
+ export const MenuGroup = ({
6
+ titleAs = "h3",
7
+ children,
8
+ title,
9
+ color,
10
+ ...rest
11
+ }: TypeMenuGroupProps) => {
12
+ // Generate a unique ID for the title
13
+ const titleId = `menu-group-title-${Math.random().toString(36).slice(2, 11)}`;
14
+
15
+ return (
16
+ <StyledMenuGroup
17
+ role="group"
18
+ aria-labelledby={title ? titleId : undefined}
19
+ // TODO: fix this type since `color` should be valid here. TS can't resolve the correct type.
20
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
21
+ // @ts-ignore
22
+ color={color}
23
+ {...rest}
24
+ >
25
+ {title && (
26
+ <StyledTitle as={titleAs} id={titleId}>
27
+ {title}
28
+ </StyledTitle>
29
+ )}
30
+ <StyledMenuGroupUl>{children}</StyledMenuGroupUl>
31
+ </StyledMenuGroup>
32
+ );
33
+ };
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from "react";
2
+ import type {
3
+ TypeBorderSystemProps,
4
+ TypeColorSystemProps,
5
+ TypeFlexboxSystemProps,
6
+ TypeGridSystemProps,
7
+ TypeLayoutSystemProps,
8
+ TypePositionSystemProps,
9
+ TypeSpaceSystemProps,
10
+ TypeTypographySystemProps,
11
+ } from "@sproutsocial/seeds-react-system-props";
12
+
13
+ export interface TypeMenuGroupStyledProps
14
+ extends TypeBorderSystemProps,
15
+ TypeColorSystemProps,
16
+ TypeFlexboxSystemProps,
17
+ TypeGridSystemProps,
18
+ TypeLayoutSystemProps,
19
+ TypePositionSystemProps,
20
+ TypeSpaceSystemProps,
21
+ TypeTypographySystemProps {}
22
+
23
+ export interface TypeMenuGroupProps
24
+ extends TypeMenuGroupStyledProps,
25
+ Omit<React.ComponentPropsWithoutRef<"ul">, "color"> {
26
+ title?: string;
27
+ children: ReactNode;
28
+ titleAs?: string | React.ComponentType<any>;
29
+ }
@@ -0,0 +1,95 @@
1
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
2
+ import {
3
+ SingleSelectMenu,
4
+ MenuContent,
5
+ MenuGroup,
6
+ MenuToggleButton,
7
+ MenuItem,
8
+ } from "../../index";
9
+
10
+ const menuItems = [
11
+ { id: "item-1", label: "Item 1" },
12
+ { id: "item-2", label: "Item 2" },
13
+ { id: "item-3", label: "Item 3" },
14
+ ];
15
+
16
+ describe("MenuGroup", () => {
17
+ xdescribe("User Behavior", () => {
18
+ xit("The first group should be accessible to screen readers", () => {});
19
+ xit("User is able to navigate between groups", () => {});
20
+ });
21
+ describe("API", () => {
22
+ it("renders a group with a title", () => {
23
+ const menuTitle = "menu-title-test";
24
+ render(
25
+ <SingleSelectMenu
26
+ defaultIsOpen
27
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
28
+ >
29
+ <MenuContent>
30
+ <MenuGroup title={menuTitle}>
31
+ {menuItems.map(({ id, label }) => (
32
+ <MenuItem key={id} id={id}>
33
+ {label}
34
+ </MenuItem>
35
+ ))}
36
+ </MenuGroup>
37
+ </MenuContent>
38
+ </SingleSelectMenu>
39
+ );
40
+
41
+ // Find associated label for the group. (Menu Group uses aria-labelledby to point to heading element)
42
+ const menuGroup = screen.getByRole("group", { name: menuTitle });
43
+ expect(menuGroup).toBeInTheDocument();
44
+ });
45
+
46
+ it("when no title is provided, it should not render a title", () => {
47
+ render(
48
+ <SingleSelectMenu
49
+ defaultIsOpen
50
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
51
+ >
52
+ <MenuContent>
53
+ <MenuGroup>
54
+ {menuItems.map(({ id, label }) => (
55
+ <MenuItem key={id} id={id}>
56
+ {label}
57
+ </MenuItem>
58
+ ))}
59
+ </MenuGroup>
60
+ </MenuContent>
61
+ </SingleSelectMenu>
62
+ );
63
+
64
+ // Find a heading element within the MenuGroup component
65
+ const menuTitle = screen.queryByRole("heading");
66
+ expect(menuTitle).not.toBeInTheDocument();
67
+ });
68
+
69
+ it("Renders children correctly", () => {
70
+ render(
71
+ <SingleSelectMenu
72
+ defaultIsOpen
73
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
74
+ >
75
+ <MenuContent>
76
+ <MenuGroup>
77
+ {menuItems.map(({ id, label }) => (
78
+ <MenuItem key={id} id={id}>
79
+ {label}
80
+ </MenuItem>
81
+ ))}
82
+ </MenuGroup>
83
+ </MenuContent>
84
+ </SingleSelectMenu>
85
+ );
86
+
87
+ expect(
88
+ screen.getByRole("option", { name: menuItems[0].label })
89
+ ).toBeInTheDocument();
90
+ expect(
91
+ screen.getByRole("option", { name: menuItems[1].label })
92
+ ).toBeInTheDocument();
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,45 @@
1
+ import { MenuGroup, MenuItem } from "../../index";
2
+ import { Text } from "@sproutsocial/seeds-react-text";
3
+
4
+ export const MenuGroupTypeTest = () => (
5
+ <>
6
+ {/* MenuGroup allows children */}
7
+ <MenuGroup>Test</MenuGroup>
8
+ {/* MenuGroup allows children with MenuItems */}
9
+ <MenuGroup>
10
+ <MenuItem id="1">Test 1</MenuItem>
11
+ <MenuItem id="2">Test 2</MenuItem>
12
+ </MenuGroup>
13
+ {/* MenuGroup allows title as a string */}
14
+ <MenuGroup title="title">Test</MenuGroup>
15
+ {/* MenuGroup allows titleAs as a string */}
16
+ <MenuGroup title="title" titleAs="h3">
17
+ Test
18
+ </MenuGroup>
19
+ {/* MenuGroup allows titleAs as a component */}
20
+ <MenuGroup title="title" titleAs={Text}>
21
+ Test
22
+ </MenuGroup>
23
+ {/* MenuGroup accepts ul props */}
24
+ <MenuGroup aria-label="test label">Test</MenuGroup>
25
+ {/* MenuGroup accepts style props */}
26
+ <MenuGroup border="1px solid black" bg="white" width="150px">
27
+ Test
28
+ </MenuGroup>
29
+
30
+ {/* @ts-expect-error - MenuGroup requires children */}
31
+ <MenuGroup />
32
+ {/* @ts-expect-error - MenuGroup title is invalid as number */}
33
+ <MenuGroup title={123}>Test</MenuGroup>
34
+ {/* @ts-expect-error - MenuGroup title is invalid as boolean */}
35
+ <MenuGroup title={false}>Test</MenuGroup>
36
+ {/* @ts-expect-error - MenuGroup titleAs is invalid as number */}
37
+ <MenuGroup title="title" titleAs={123}>
38
+ Test
39
+ </MenuGroup>
40
+ {/* @ts-expect-error - MenuGroup titleAs is invalid as boolean */}
41
+ <MenuGroup title="title" titleAs={false}>
42
+ Test
43
+ </MenuGroup>
44
+ </>
45
+ );
@@ -0,0 +1,8 @@
1
+ // MenuGroup component code
2
+ export * from "./MenuGroup";
3
+
4
+ // MenuGroup types
5
+ export * from "./MenuGroupTypes";
6
+
7
+ // MenuGroup styles
8
+ export * from "./styles";
@@ -0,0 +1,37 @@
1
+ import styled from "styled-components";
2
+ import Text from "@sproutsocial/seeds-react-text";
3
+ import {
4
+ border,
5
+ layout,
6
+ position,
7
+ flexbox,
8
+ grid,
9
+ color,
10
+ space,
11
+ } from "styled-system";
12
+ import type { TypeMenuGroupProps } from "./MenuGroupTypes";
13
+
14
+ export const StyledMenuGroup = styled.li<TypeMenuGroupProps>`
15
+ &:not(:last-child) {
16
+ border-bottom: 1px solid
17
+ ${({ theme }) => theme.colors.container.border.base};
18
+ }
19
+ list-style: none;
20
+ margin: 0;
21
+ ${border}
22
+ ${position}
23
+ ${color}
24
+ ${flexbox}
25
+ ${grid}
26
+ ${layout}
27
+ ${space}
28
+ `;
29
+
30
+ export const StyledTitle = styled(Text.SmallSubHeadline)`
31
+ margin: ${({ theme }) => theme.space[200]} 0px
32
+ ${({ theme }) => theme.space[300]} ${({ theme }) => theme.space[300]};
33
+ `;
34
+ export const StyledMenuGroupUl = styled.ul`
35
+ margin: 0;
36
+ padding: ${({ theme }) => theme.space[300]};
37
+ `;