@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,59 @@
1
+ Feature: Menu Header
2
+
3
+ The top area of a Menu.
4
+
5
+ Rule: API
6
+ Background:
7
+ Given an engineer is implementing a MenuHeader
8
+
9
+ Scenario: Display MenuHeader with a title
10
+ When the "title" prop is provided
11
+ And the title is a String
12
+ Then the MenuHeader should display the title
13
+
14
+ Scenario: Display MenuHeader with an empty or non-string title
15
+ When an empty or non-string title is provided
16
+ Then the MenuHeader should not display the title
17
+
18
+ Scenario Outline: MenuHeader supports children
19
+ When <child component(s)> provided
20
+ Then the MenuHeader should render the <component(s)>
21
+
22
+ Examples: Child Components
23
+ | child component(s) | component(s) |
24
+ | Text is | Text |
25
+ | a Banner is | Banner |
26
+ | a MenuHeaderInput is | MenuHeaderInput |
27
+ | Text and a Banner are | Text and Banner |
28
+
29
+ Scenario Outline: Actions can be triggered from the MenuHeader
30
+ Given that actionable elements are provided to the MenuHeader
31
+ When a <action> is clicked
32
+ Then the corresponding onClick should be executed
33
+
34
+ Examples: Action Types
35
+ | action |
36
+ | back button |
37
+ | link button |
38
+
39
+ Scenario Outline: Actions can appear on the left or right of the MenuHeader title
40
+ When an action is provided <action position>
41
+ Then the action(s) should be rendered
42
+
43
+ Examples: action position
44
+ | before the header title |
45
+ | after the header title |
46
+ | before and after the header title |
47
+
48
+ Scenario: MenuHeader extends Box
49
+ When Box features are used
50
+ Then the MenuHeader responds as a Box would
51
+
52
+ Scenario Outline: MenuHeader checks its types
53
+ When <propName> is passed a value
54
+ Then MenuHeader expects it to be of type <propType>
55
+
56
+ Examples: Prop Types
57
+ | propName | propType |
58
+ | title | String |
59
+ | children | ReactNode |
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import {
3
+ MenuHeaderContainer,
4
+ MenuTitleText,
5
+ MenuChildrenContainer,
6
+ } from "./styles";
7
+ import type { TypeMenuHeaderProps } from "./MenuHeaderTypes";
8
+
9
+ const MenuHeader = ({
10
+ children,
11
+ title,
12
+ leftAction,
13
+ rightAction,
14
+ ...rest
15
+ }: TypeMenuHeaderProps) => {
16
+ return (
17
+ <>
18
+ <MenuHeaderContainer $leftAction={!!leftAction} {...rest}>
19
+ {leftAction && leftAction}
20
+ {title && (
21
+ <MenuTitleText $leftAction={!!leftAction}>{title}</MenuTitleText>
22
+ )}
23
+ {rightAction && rightAction}
24
+ {children && <MenuChildrenContainer>{children}</MenuChildrenContainer>}
25
+ </MenuHeaderContainer>
26
+ </>
27
+ );
28
+ };
29
+
30
+ export { MenuHeader };
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ import type {
3
+ TypeBorderSystemProps,
4
+ TypeColorSystemProps,
5
+ TypeFlexboxSystemProps,
6
+ TypeGridSystemProps,
7
+ TypeLayoutSystemProps,
8
+ TypePositionSystemProps,
9
+ TypeSpaceSystemProps,
10
+ TypeStyledComponentsCommonProps,
11
+ } from "@sproutsocial/seeds-react-system-props";
12
+
13
+ export interface TypeMenuHeaderProps
14
+ extends Omit<React.ComponentPropsWithoutRef<"div">, "color">,
15
+ TypeStyledComponentsCommonProps,
16
+ TypeBorderSystemProps,
17
+ TypeColorSystemProps,
18
+ TypeFlexboxSystemProps,
19
+ TypeGridSystemProps,
20
+ TypeLayoutSystemProps,
21
+ TypeSpaceSystemProps,
22
+ TypePositionSystemProps {
23
+ title?: string;
24
+ leftAction?: React.ReactNode;
25
+ rightAction?: React.ReactNode;
26
+ }
@@ -0,0 +1,67 @@
1
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
2
+ import { MenuHeader } from "../MenuHeader";
3
+ import Button from "@sproutsocial/seeds-react-button";
4
+
5
+ describe("MenuHeader API", () => {
6
+ it("renders a title when title prop is passed a string", () => {
7
+ render(<MenuHeader title="Menu Header" />);
8
+
9
+ expect(screen.getByText("Menu Header")).toBeInTheDocument();
10
+ });
11
+
12
+ it("doesn't render a title when title prop is empty", () => {
13
+ render(<MenuHeader />);
14
+ const menuHeader = screen.queryByRole("heading");
15
+
16
+ expect(menuHeader).not.toBeInTheDocument();
17
+ });
18
+
19
+ it("supports children", () => {
20
+ render(<MenuHeader>Hello World</MenuHeader>);
21
+
22
+ expect(screen.getByText("Hello World")).toBeInTheDocument();
23
+ });
24
+
25
+ it("supports clickable actions", async () => {
26
+ const handleTestClick = jest.fn();
27
+
28
+ const { user } = render(
29
+ <MenuHeader
30
+ leftAction={<Button onClick={handleTestClick}>Left Action</Button>}
31
+ rightAction={<Button onClick={handleTestClick}>Right Action</Button>}
32
+ >
33
+ Hello World
34
+ </MenuHeader>
35
+ );
36
+
37
+ const leftAction = screen.getByText("Left Action");
38
+ const rightAction = screen.getByText("Right Action");
39
+
40
+ await user.click(leftAction);
41
+
42
+ expect(handleTestClick).toHaveBeenCalledTimes(1);
43
+
44
+ await user.click(rightAction);
45
+
46
+ expect(handleTestClick).toHaveBeenCalledTimes(2);
47
+ });
48
+
49
+ it("can render actions on the left or right of the title", () => {
50
+ render(
51
+ <MenuHeader
52
+ leftAction={<Button onClick={() => {}}>Left Action</Button>}
53
+ rightAction={<Button onClick={() => {}}>Right Action</Button>}
54
+ title="Menu Header"
55
+ >
56
+ Hello World
57
+ </MenuHeader>
58
+ );
59
+
60
+ const menuHeader = screen.getByText("Menu Header");
61
+ const leftAction = screen.getByText("Left Action");
62
+ const rightAction = screen.getByText("Right Action");
63
+
64
+ expect(menuHeader.compareDocumentPosition(leftAction)).toBe(2);
65
+ expect(menuHeader.compareDocumentPosition(rightAction)).toBe(4);
66
+ });
67
+ });
@@ -0,0 +1,27 @@
1
+ import { MenuHeader } from "../MenuHeader";
2
+
3
+ export const MenuHeaderTypeTest = () => (
4
+ <>
5
+ <MenuHeader
6
+ title="Menu Header"
7
+ leftAction={<div>left action</div>}
8
+ rightAction={<div>right action</div>}
9
+ children={<div>children</div>}
10
+ />
11
+ {/* left and right action accept strings */}
12
+ <MenuHeader leftAction="left" rightAction="right" />
13
+ {/* left and right action accept booleans */}
14
+ <MenuHeader leftAction={false} rightAction={true} />
15
+ {/* left and right action accept undefined */}
16
+ <MenuHeader leftAction={undefined} rightAction={undefined} />
17
+ {/* left and right action accept numbers */}
18
+ <MenuHeader leftAction={123} rightAction={123} />
19
+ {/* left and right action accept null */}
20
+ <MenuHeader leftAction={null} rightAction={null} />
21
+
22
+ {/* @ts-expect-error - title should error with boolean */}
23
+ <MenuHeader title={true} />
24
+ {/* @ts-expect-error - title should error with numbers */}
25
+ <MenuHeader title={123123} />
26
+ </>
27
+ );
@@ -0,0 +1,2 @@
1
+ export { MenuHeader } from "./MenuHeader";
2
+ export type { TypeMenuHeaderProps } from "./MenuHeaderTypes";
@@ -0,0 +1,75 @@
1
+ import styled from "styled-components";
2
+ import {
3
+ border,
4
+ color,
5
+ flexbox,
6
+ grid,
7
+ layout,
8
+ position,
9
+ space,
10
+ } from "styled-system";
11
+ import type { TypeMenuHeaderProps } from "./MenuHeaderTypes";
12
+
13
+ interface TypeMenuHeaderContainerProps extends TypeMenuHeaderProps {
14
+ $leftAction?: boolean;
15
+ }
16
+
17
+ export const MenuHeaderContainer = styled.div<TypeMenuHeaderContainerProps>`
18
+ border-color: ${({ theme }) => theme.colors.container.border.base};
19
+ border-bottom-width: 1px;
20
+ border-bottom-style: solid;
21
+ box-sizing: border-box;
22
+ width: 100%;
23
+ font-size: ${({ theme }) => theme.typography[200].fontSize};
24
+ display: flex;
25
+ padding: ${({ theme }) => theme.space[350]} ${({ theme }) => theme.space[400]};
26
+
27
+ > a {
28
+ white-space: nowrap;
29
+ }
30
+
31
+ &:first-child {
32
+ flex-grow: 1;
33
+ justify-self: start;
34
+ }
35
+
36
+ &:last-child {
37
+ flex-grow: 1;
38
+ justify-self: end;
39
+ }
40
+
41
+ ${({ $leftAction, theme }) =>
42
+ $leftAction &&
43
+ `
44
+ padding-left: ${theme.space[350]};
45
+ `}
46
+
47
+ ${border}
48
+ ${color}
49
+ ${flexbox}
50
+ ${grid}
51
+ ${layout}
52
+ ${space}
53
+ ${position}
54
+ `;
55
+
56
+ export const MenuTitleText = styled.h3<TypeMenuHeaderContainerProps>`
57
+ color: ${({ theme }) => theme.colors.text.headline};
58
+ font-weight: ${({ theme }) => theme.fontWeights.bold};
59
+ margin: 0px;
60
+ flex-grow: 2;
61
+ justify-self: start;
62
+
63
+ ${({ $leftAction, theme }) =>
64
+ $leftAction &&
65
+ `
66
+ padding-left: ${theme.space[350]};
67
+ `}
68
+
69
+ ${({ theme }) => theme.typography[200]}
70
+ `;
71
+
72
+ export const MenuChildrenContainer = styled.div`
73
+ grid-column: 1 / -1;
74
+ margin-top: ${({ theme }) => theme.space[300]};
75
+ `;
@@ -0,0 +1,88 @@
1
+ Feature: MenuItem
2
+
3
+ A configurable and interactive item for inclusion in a `Menu` component.
4
+
5
+ Rule: User Behavior
6
+ Background:
7
+ Given an end user is using the component
8
+
9
+ Scenario: MenuItem is clickable
10
+ Given a MenuItem is not disabled
11
+ When the MenuItem is clicked
12
+ Then the MenuItem's onClick event should fire
13
+
14
+ Scenario Outline: MenuItem is disabled
15
+ Given a MenuItem is disabled
16
+ When the MenuItem is triggered via <interaction>
17
+ Then no events should fire
18
+
19
+ Examples:
20
+ | interaction |
21
+ | clicking |
22
+ | focusing and pressing the Enter key |
23
+ | focusing and pressing the Space key |
24
+
25
+ Scenario Outline: MenuItem can be triggered via keyboard
26
+ Given a MenuItem is focused
27
+ And the MenuItem is not disabled
28
+ When the MenuItem is triggered via <interaction>
29
+ Then the MenuItem's onClick event should fire
30
+
31
+ Examples:
32
+ | interaction |
33
+ | pressing the Enter key |
34
+ | pressing the Space key |
35
+
36
+ Rule: API
37
+ Background:
38
+ Given an engineer is developing using the component
39
+
40
+ Scenario Outline: MenuItem accepts children
41
+ When <child component(s)> provided
42
+ Then the MenuFooter should render the <component(s)>
43
+
44
+ Examples: child components
45
+ | child component(s) | component(s) |
46
+ | a Link is | Link |
47
+ | a Text is | Text |
48
+ | a Icon is | Icon |
49
+ | a Text then Icon are | Text and Icon |
50
+ | a Icon and Text are | Icon and Text |
51
+
52
+ Scenario Outline: A MenuItem accepts system props
53
+ When a system prop from the category <systemPropCategory> is passed a value
54
+ Then the MenuItem will have <systemPropName> styling
55
+
56
+ Examples: System Props
57
+ | systemPropCategory | systemPropName |
58
+ | color | bg |
59
+ | border | borderColor |
60
+ | layout | display |
61
+ | position | zIndex |
62
+ | flexbox | flex |
63
+ | grid | gridGap |
64
+ | space | p |
65
+
66
+ Scenario Outline: MenuItem validates prop types
67
+ When any <prop> is passed to the MenuItem
68
+ Then the MenuItem should check that each prop is of the expected <propType>
69
+
70
+ Examples: Prop Types
71
+ | prop | propType |
72
+ | children | node |
73
+ | onClick | func |
74
+ | disabled | bool |
75
+ | className | string |
76
+
77
+ Scenario Outline: Screen readers should read MenuItem states
78
+ Given a MenuItem is <state>
79
+ Then screen readers should read the MenuItem as <state>
80
+
81
+ Examples: MenuItem States
82
+ | state |
83
+ | enabled |
84
+ | disabled |
85
+ | hovered |
86
+ | focused |
87
+ | active |
88
+ | pressed |
@@ -0,0 +1,44 @@
1
+ import React, { useContext } from "react";
2
+ import { MenuContext } from "../MenuContext";
3
+ import { MENU_ITEM_ROLES } from "../constants";
4
+ import type { TypeDefaultItemProps } from "../types";
5
+ import { MenuItemContainer } from "./styles";
6
+ import type { TypeMenuItemProps } from "./MenuItemTypes";
7
+
8
+ export const MenuItem = ({
9
+ id,
10
+ children,
11
+ role = "menuitem",
12
+ label,
13
+ onClick,
14
+ onKeyDown,
15
+ disabled = false,
16
+ ref,
17
+ color,
18
+ ...props
19
+ }: TypeMenuItemProps) => {
20
+ const { getItemProps, itemsMap, highlightedIndex, isListbox } =
21
+ useContext(MenuContext);
22
+ const item = (itemsMap[id] || { index: 0, id }) as TypeDefaultItemProps;
23
+ const highlighted = highlightedIndex === item?.index;
24
+
25
+ return (
26
+ <MenuItemContainer
27
+ id={id}
28
+ tabIndex={role !== MENU_ITEM_ROLES.LISTITEM && !disabled ? -1 : undefined}
29
+ $highlighted={highlighted}
30
+ {...getItemProps?.({
31
+ ...(isListbox ? {} : { role: "menuitem" }),
32
+ item,
33
+ index: item.index,
34
+ // This prop no longer supported in downshift
35
+ // disabled,
36
+ onClick,
37
+ onKeyDown,
38
+ })}
39
+ {...props}
40
+ >
41
+ {children}
42
+ </MenuItemContainer>
43
+ );
44
+ };
@@ -0,0 +1,37 @@
1
+ import type { GetItemPropsOptions } from "downshift";
2
+ import type { TypeMenuItemRoles } from "../constants";
3
+ import type { TypeDefaultItemProps } from "../types";
4
+ import type {
5
+ TypeBorderSystemProps,
6
+ TypeColorSystemProps,
7
+ TypeFlexboxSystemProps,
8
+ TypeGridSystemProps,
9
+ TypeLayoutSystemProps,
10
+ TypePositionSystemProps,
11
+ TypeSpaceSystemProps,
12
+ TypeTypographySystemProps,
13
+ } from "@sproutsocial/seeds-react-system-props";
14
+
15
+ export interface TypeMenuItemStyledProps
16
+ extends TypeBorderSystemProps,
17
+ TypeColorSystemProps,
18
+ TypeFlexboxSystemProps,
19
+ TypeGridSystemProps,
20
+ TypeLayoutSystemProps,
21
+ TypePositionSystemProps,
22
+ TypeSpaceSystemProps,
23
+ TypeTypographySystemProps {}
24
+
25
+ export interface TypeMenuItemProps
26
+ extends TypeMenuItemStyledProps,
27
+ Omit<
28
+ GetItemPropsOptions<TypeDefaultItemProps>,
29
+ "as" | "item" | keyof TypeMenuItemStyledProps | keyof TypeDefaultItemProps
30
+ >,
31
+ Omit<TypeDefaultItemProps, "index"> {
32
+ label?: string;
33
+ children: React.ReactNode;
34
+ disabled?: boolean;
35
+ role?: TypeMenuItemRoles;
36
+ $highlighted?: boolean;
37
+ }
@@ -0,0 +1,228 @@
1
+ /* eslint-disable testing-library/no-unnecessary-act */
2
+ import {
3
+ screen,
4
+ render,
5
+ waitFor,
6
+ } from "@sproutsocial/seeds-react-testing-library";
7
+ import { PointerEventsCheckLevel } from "@testing-library/user-event";
8
+ import { theme } from "@sproutsocial/seeds-react-theme";
9
+ import {
10
+ MenuItem,
11
+ MenuToggleButton,
12
+ MenuContent,
13
+ ActionMenu,
14
+ } from "../../index";
15
+ import { TestWithMenuRoot } from "../../menuTestingUtils";
16
+
17
+ const menuItemProps = {
18
+ key: "book123",
19
+ id: "book-id",
20
+ children: "Book Title",
21
+ };
22
+ const testMenuContext = {
23
+ isOpen: true,
24
+ closeMenu: jest.fn(),
25
+ openMenu: jest.fn(),
26
+ getMenuProps: jest.fn((props) => props),
27
+ getToggleButtonProps: jest.fn((props) => ({
28
+ onKeyDown: jest.fn(),
29
+ ...props,
30
+ })),
31
+ getItemProps: jest.fn((props) => ({ ...props })),
32
+ isListbox: false,
33
+ };
34
+ const testTriggerElement = <MenuToggleButton>Open</MenuToggleButton>;
35
+
36
+ describe("MenuItem User Behavior", () => {
37
+ describe("MenuItem is clickable", () => {
38
+ it("fires onClick event when clicked and not disabled", async () => {
39
+ const handleClick = jest.fn();
40
+ const { user } = render(
41
+ <TestWithMenuRoot
42
+ triggerElement={testTriggerElement}
43
+ {...testMenuContext}
44
+ >
45
+ <MenuContent>
46
+ <MenuItem {...menuItemProps} onClick={handleClick} />
47
+ </MenuContent>
48
+ </TestWithMenuRoot>
49
+ );
50
+
51
+ await user
52
+ .setup({
53
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
54
+ })
55
+ .click(screen.getByRole("menuitem"));
56
+
57
+ await waitFor(() => expect(handleClick).toHaveBeenCalledTimes(1));
58
+ });
59
+ });
60
+
61
+ describe("MenuItem can be triggered via keyboard", () => {
62
+ it(`fires onClick event when enter and not disabled`, async () => {
63
+ const handleClick = jest.fn();
64
+ const handleStateChange = jest.fn();
65
+
66
+ const { user } = render(
67
+ <ActionMenu
68
+ triggerElement={testTriggerElement}
69
+ isOpen
70
+ onStateChange={handleStateChange}
71
+ >
72
+ <MenuContent>
73
+ <MenuItem
74
+ {...menuItemProps}
75
+ data-testid="menu-test"
76
+ key={0}
77
+ onKeyDown={handleClick}
78
+ tabIndex={0}
79
+ />
80
+ </MenuContent>
81
+ </ActionMenu>
82
+ );
83
+ const targetElem = screen.getByTestId("menu-test");
84
+
85
+ await user.tab();
86
+ expect(targetElem).toHaveFocus();
87
+
88
+ await user.keyboard("{enter}");
89
+
90
+ await waitFor(() =>
91
+ expect(handleStateChange).toHaveBeenCalledWith({
92
+ isOpen: false,
93
+ type: "__togglebutton_keydown_enter__",
94
+ })
95
+ );
96
+ expect(handleClick).toHaveBeenCalled();
97
+ });
98
+ it(`fires onClick event when space and not disabled`, async () => {
99
+ const handleClick = jest.fn();
100
+ const handleStateChange = jest.fn();
101
+
102
+ const { user } = render(
103
+ <ActionMenu
104
+ triggerElement={testTriggerElement}
105
+ isOpen
106
+ onStateChange={handleStateChange}
107
+ >
108
+ <MenuContent>
109
+ <MenuItem
110
+ {...menuItemProps}
111
+ data-testid="menu-test"
112
+ key={0}
113
+ onKeyDown={handleClick}
114
+ tabIndex={0}
115
+ />
116
+ </MenuContent>
117
+ </ActionMenu>
118
+ );
119
+ const targetElem = screen.getByTestId("menu-test");
120
+
121
+ await user.tab();
122
+ expect(targetElem).toHaveFocus();
123
+
124
+ await user.keyboard(" ");
125
+
126
+ await waitFor(() =>
127
+ expect(handleStateChange).toHaveBeenCalledWith({
128
+ isOpen: false,
129
+ type: "__togglebutton_keydown_space_button__",
130
+ })
131
+ );
132
+
133
+ expect(handleClick).toHaveBeenCalled();
134
+ });
135
+
136
+ it(`does not fire onClick event when disabled`, async () => {
137
+ const handleClick = jest.fn();
138
+ const handleStateChange = jest.fn();
139
+
140
+ const books = [
141
+ {
142
+ index: 0,
143
+ id: "book-1",
144
+ author: "Harper Lee",
145
+ title: "To Kill a Mockingbird",
146
+ },
147
+ {
148
+ index: 1,
149
+ id: "book-2",
150
+ author: "Lev Tolstoy",
151
+ title: "War and Peace",
152
+ },
153
+ ];
154
+
155
+ const testItem = books[0];
156
+
157
+ const { user } = render(
158
+ <TestWithMenuRoot
159
+ triggerElement={<MenuToggleButton>Open</MenuToggleButton>}
160
+ {...{
161
+ ...testMenuContext,
162
+ items: books,
163
+ selectedItem: testItem,
164
+ }}
165
+ >
166
+ <MenuItem
167
+ id={testItem.id}
168
+ disabled={true}
169
+ onClick={handleStateChange}
170
+ >
171
+ {testItem.title}
172
+ </MenuItem>
173
+ </TestWithMenuRoot>
174
+ );
175
+ await user
176
+ .setup({
177
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
178
+ })
179
+ .click(screen.getByRole("menuitem"));
180
+
181
+ expect(handleClick).not.toHaveBeenCalled();
182
+ });
183
+ });
184
+ });
185
+
186
+ describe("MenuItem API", () => {
187
+ it("accepts children", () => {
188
+ render(
189
+ <ActionMenu
190
+ id="test-menu-item-selectable"
191
+ triggerElement={testTriggerElement}
192
+ isOpen
193
+ >
194
+ <MenuContent>
195
+ <MenuItem onClick={() => {}} id="test">
196
+ Hello world!
197
+ <span>testing</span>
198
+ </MenuItem>
199
+ </MenuContent>
200
+ </ActionMenu>
201
+ );
202
+
203
+ const menuItem = screen.getByText("Hello world!");
204
+ const span = screen.getByText("testing");
205
+ expect(menuItem).toContainElement(span);
206
+ });
207
+
208
+ it("accepts box & system Props", () => {
209
+ render(
210
+ <ActionMenu
211
+ id="test-menu-item-selectable"
212
+ triggerElement={testTriggerElement}
213
+ isOpen
214
+ >
215
+ <MenuContent>
216
+ <MenuItem display="flex" p={300} onClick={() => {}} id="test">
217
+ Hello world!
218
+ <span>testing</span>
219
+ </MenuItem>
220
+ </MenuContent>
221
+ </ActionMenu>
222
+ );
223
+
224
+ const menuItem = screen.getByRole("menuitem");
225
+ expect(menuItem).toHaveStyle(`padding: ${theme.space[300]}`);
226
+ expect(menuItem).toHaveStyle(`display: flex`);
227
+ });
228
+ });