@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,41 @@
1
+ import { MenuItem } from "../MenuItem";
2
+
3
+ export const MenuItemTypeTest = () => (
4
+ <>
5
+ {/* Valid MenuItem with minimal required props */}
6
+ <MenuItem id="test">Test</MenuItem>
7
+ {/* Valid MenuItem allows disabled prop */}
8
+ <MenuItem id="test" disabled>
9
+ Test
10
+ </MenuItem>
11
+ {/* Valid MenuItem allows disabled prop false */}
12
+ <MenuItem id="test" disabled={false}>
13
+ Test
14
+ </MenuItem>
15
+
16
+ {/* @ts-expect-error - no children is invalid */}
17
+ <MenuItem id="foo" />
18
+ {/* @ts-expect-error - test empty states are invalid */}
19
+ <MenuItem />
20
+ {/* @ts-expect-error - test missing index is invalid */}
21
+ <MenuItem id="foo" />
22
+ {/* @ts-expect-error - test missing id is invalid */}
23
+ <MenuItem />
24
+ {/* @ts-expect-error - test that types are invalid */}
25
+ <MenuItem id={0} />
26
+ {/* @ts-expect-error -test that wrong highlighted type fails */}
27
+ <MenuItem $highlighted="string">Hello world!</MenuItem>
28
+ {/* @ts-expect-error -test that wrong role type fails */}
29
+ <MenuItem role="wrong">Hello world!</MenuItem>
30
+ {/* @ts-expect-error - missing id is invalid */}
31
+ <MenuItem>Test</MenuItem>
32
+ <MenuItem
33
+ /* @ts-expect-error - id may not be a number */
34
+ id={0}
35
+ /* @ts-expect-error - disabled may not be a string */
36
+ disabled="true"
37
+ >
38
+ Test
39
+ </MenuItem>
40
+ </>
41
+ );
@@ -0,0 +1,5 @@
1
+ export * from "./MenuItem";
2
+
3
+ export * from "./MenuItemTypes";
4
+
5
+ export * from "./styles";
@@ -0,0 +1,71 @@
1
+ import styled, { css } from "styled-components";
2
+ import { focusRing, disabled } from "@sproutsocial/seeds-react-mixins";
3
+ import { color, border, flexbox, grid, layout, space } from "styled-system";
4
+ import {
5
+ CheckboxContainer,
6
+ InputWrapper,
7
+ PillInput,
8
+ checkboxContainerHoverStyles,
9
+ inputWrapperHoverStyles,
10
+ pillInputHoverStyles,
11
+ } from "@sproutsocial/seeds-react-checkbox";
12
+
13
+ const highlightedStyles = css`
14
+ cursor: pointer;
15
+ border: 1px solid ${({ theme }) => theme.colors.listItem.border.base};
16
+ background-color: ${({ theme }) => theme.colors.listItem.background.hover};
17
+ `;
18
+
19
+ export const MenuItemContainer = styled.li<{ $highlighted: boolean }>`
20
+ border-radius: ${({ theme }) => theme.radii[500]};
21
+ background-color: ${({ theme }) => theme.colors.listItem.background.base};
22
+ border: 1px solid ${({ theme }) => theme.colors.listItem.background.base};
23
+ color: ${({ theme }) => theme.colors.text.body};
24
+ font-family: ${({ theme }) => theme.fontFamily};
25
+ font-weight: ${({ theme }) => theme.fontWeights.normal};
26
+ padding: ${({ theme }) => theme.space[300]};
27
+ list-style-type: none;
28
+ outline: 0;
29
+ ${({ theme }) => theme.typography[200]};
30
+ transition: all ${({ theme }) => theme.duration["fast"]};
31
+
32
+ /* Highlighted */
33
+ ${({ $highlighted }) => $highlighted && highlightedStyles}
34
+
35
+ /* Hovered, but not focused */
36
+ &:hover {
37
+ ${highlightedStyles}
38
+ ${CheckboxContainer} {
39
+ ${checkboxContainerHoverStyles}
40
+ }
41
+ ${InputWrapper} {
42
+ ${inputWrapperHoverStyles}
43
+ }
44
+ ${PillInput} {
45
+ ${pillInputHoverStyles}
46
+ }
47
+ }
48
+
49
+ /* focused */
50
+ &:focus {
51
+ ${focusRing}
52
+ }
53
+
54
+ /* Pressed state*/
55
+ &:active {
56
+ background-color: ${({ theme }) => theme.colors.listItem.background.active};
57
+ color: ${({ theme }) => theme.colors.text.inverse};
58
+ }
59
+
60
+ &[aria-disabled="true"] {
61
+ cursor: not-allowed;
62
+ ${disabled}
63
+ }
64
+
65
+ ${border}
66
+ ${color}
67
+ ${flexbox}
68
+ ${grid}
69
+ ${layout}
70
+ ${space}
71
+ `;
@@ -0,0 +1,99 @@
1
+ Feature: MenuItemSelectable
2
+
3
+ A selectable item designed to be included within a Menu component, providing different input types
4
+
5
+ Rule: User Behavior
6
+
7
+ Background:
8
+ Given an end user is interacting with the component
9
+
10
+ Scenario Outline: MenuItemSelectable can be checked
11
+ Given the MenuItemSelectable is in the unchecked state
12
+ And the MenuItemSelectable is focused
13
+ And the MenuItemSelectable is not disabled
14
+ When the MenuItemSelectable is triggered via <interaction>
15
+ Then the MenuItemSelectable should be checked
16
+
17
+ Examples: Interactions
18
+ | interaction |
19
+ | clicking with mouse |
20
+ | pressing the Enter key |
21
+ | pressing the Space key |
22
+
23
+ Scenario Outline: MenuItemSelectable can be unchecked
24
+ Given the MenuItemSelectable is in the checked state
25
+ And the MenuItemSelectable is focused
26
+ And the MenuItemSelectable is not disabled
27
+ When the MenuItemSelectable is triggered via <interaction>
28
+ Then the MenuItemSelectable should be unchecked
29
+
30
+ Examples: Interactions
31
+ | interaction |
32
+ | clicking with mouse |
33
+ | pressing the Enter key |
34
+ | pressing the Space key |
35
+
36
+ Scenario Outline: MenuItemSelectable is disabled
37
+ Given the MenuItemSelectable is disabled
38
+ And the MenuItemSelectable is focused
39
+ When the MenuItemSelectable is triggered via <interaction>
40
+ Then no events should fire
41
+
42
+ Examples: Interactions
43
+ | interaction |
44
+ | clicking with mouse |
45
+ | pressing the Enter key |
46
+ | pressing the Space key |
47
+
48
+ Rule: API
49
+ Background:
50
+ Given an engineer is developing using the component
51
+
52
+ Scenario: MenuItemSelectable accepts children
53
+ When <child component(s)> are provided
54
+ Then the MenuItemSelectable should render the <component(s)>
55
+
56
+ Examples: child components
57
+ | child component(s) | component(s) |
58
+ | a Link is | Link |
59
+ | a Text is | Text |
60
+ | a Icon is | Icon |
61
+ | a Text then Icon are | Text and Icon |
62
+ | a Icon and Text are | Icon and Text |
63
+
64
+ Scenario Outline: MenuItemSelectable supports different input types
65
+ When an <inputTypeProp> is passed to the MenuItemSelectable component
66
+ Then the MenuItemSelectable renders with the appropriate <component> type
67
+
68
+ Examples: Input Types
69
+ | inputTypeProp | component |
70
+ | checkbox | Checkbox |
71
+ | radio | Radio |
72
+ | switch | Switch |
73
+
74
+ Scenario: MenuItemSelectable defaults to an icon input type
75
+ When the MenuItemSelectable is implemented without an inputType prop specified
76
+ Then the MenuItemSelectable defaults to an icon input type
77
+
78
+ Scenario: MenuItemSelectable checked state can be enabled via a prop
79
+ When the selected prop is set to true
80
+ Then the MenuItemSelectable should appear in the checked state
81
+
82
+ Scenario Outline: MenuItemSelectable validates prop types
83
+ When any <propName> is passed to the MenuItemSelectable
84
+ Then the MenuItemSelectable should check that each <propName> is of the expected <propType>
85
+
86
+ Examples: Prop Types
87
+ | propName | propType |
88
+ | children | ReactNode |
89
+ | disabled | boolean |
90
+ | elevation | low, medium, or high |
91
+ | href | string |
92
+ | onClick | React.MouseEventHandler |
93
+ | role | string |
94
+ | selected | boolean |
95
+ | inputType | checkbox, radio, or switch |
96
+
97
+ Scenario: MenuItemSelectable extends MenuItem
98
+ When MenuItemSelectable features are used
99
+ Then the MenuItemSelectable responds as a MenuItem would
@@ -0,0 +1,136 @@
1
+ import React, { useContext, useMemo } from "react";
2
+ import { Icon } from "@sproutsocial/seeds-react-icon";
3
+ import { Checkbox } from "@sproutsocial/seeds-react-checkbox";
4
+ import { Switch } from "@sproutsocial/seeds-react-switch";
5
+ import { Radio } from "@sproutsocial/seeds-react-radio";
6
+ import { useTheme } from "styled-components";
7
+ import { Box } from "@sproutsocial/seeds-react-box";
8
+ import { MenuContext } from "../MenuContext";
9
+ import { MenuItem } from "../MenuItem";
10
+
11
+ // import { type TypeMenuItemRoles } from "../constants";
12
+ import {
13
+ MenuItemSelectableAction,
14
+ MenuItemSelectableRow,
15
+ MenuItemSelectableText,
16
+ } from "./styles";
17
+ import type {
18
+ TypeMenuItemSelectableProps,
19
+ InputType,
20
+ } from "./MenuItemSelectableTypes";
21
+
22
+ /**
23
+ * SelectionIndicator - handles logic for appropriate rendering of inputs
24
+ * @returns Checkbox | Icon | Radio | Switch
25
+ */
26
+ export const SelectionIndicator = ({
27
+ id,
28
+ inputType = "icon",
29
+ "aria-labelledby": labeledBy,
30
+ selected,
31
+ }: {
32
+ id: string;
33
+ "aria-labelledby": string;
34
+ selected: boolean;
35
+ inputType: InputType;
36
+ }) => {
37
+ const { space } = useTheme();
38
+
39
+ const inputStub = (e: React.SyntheticEvent) => {
40
+ e.stopPropagation();
41
+ };
42
+
43
+ switch (inputType) {
44
+ case "checkbox":
45
+ return (
46
+ <Checkbox
47
+ onChange={() => {}}
48
+ checked={selected}
49
+ aria-labelledby={labeledBy}
50
+ id={`MenuItemSelectable-${inputType}-${id}`}
51
+ />
52
+ );
53
+ case "switch":
54
+ return (
55
+ <Switch
56
+ onClick={inputStub}
57
+ checked={!!selected}
58
+ aria-labelledby={labeledBy}
59
+ id={`MenuItemSelectable-${inputType}-${id}`}
60
+ />
61
+ );
62
+ case "radio":
63
+ return (
64
+ <Radio
65
+ checked={selected}
66
+ id={`MenuItemSelectable-${inputType}-${id}`}
67
+ name={`MenuItemSelectable-${inputType}`}
68
+ aria-labelledby={labeledBy}
69
+ />
70
+ );
71
+ case "icon":
72
+ if (selected) {
73
+ return <Icon name="check-solid" aria-hidden size="small" />;
74
+ } else {
75
+ return <Box width={space["400"]}></Box>;
76
+ }
77
+ default:
78
+ return null;
79
+ }
80
+ };
81
+
82
+ /**
83
+ * MenuItemSelectable - primitive with controlled input states
84
+ */
85
+ export const MenuItemSelectable = ({
86
+ id,
87
+ children,
88
+ inputType = "icon",
89
+ inputLabel,
90
+ ...props
91
+ }: TypeMenuItemSelectableProps) => {
92
+ const { selectedItem, selectedItems, isListbox } = useContext(MenuContext);
93
+ const selected = useMemo(
94
+ () =>
95
+ selectedItem?.id === id ||
96
+ !!selectedItems?.find((item) => item?.id === id),
97
+ [selectedItem, selectedItems]
98
+ );
99
+
100
+ if (isListbox === false) {
101
+ console.warn(
102
+ "MenuItemSelectable is invalid when isListbox is false (Menu)"
103
+ );
104
+ }
105
+
106
+ // const roles: Record<InputType, TypeMenuItemRoles> = {
107
+ // checkbox: "menuitemcheckbox",
108
+ // switch: "menuitemcheckbox",
109
+ // radio: "menuitemradio",
110
+ // icon: "option",
111
+ // };
112
+
113
+ const label = inputLabel || `menu-item-child-${id}`;
114
+
115
+ return (
116
+ <MenuItem
117
+ active={selected}
118
+ id={id}
119
+ // role={roles["icon"]}
120
+ aria-selected={selected}
121
+ {...props}
122
+ >
123
+ <MenuItemSelectableRow>
124
+ <MenuItemSelectableAction>
125
+ <SelectionIndicator
126
+ id={id}
127
+ aria-labelledby={label}
128
+ inputType={inputType}
129
+ selected={!!selected}
130
+ />
131
+ </MenuItemSelectableAction>
132
+ <MenuItemSelectableText id={label}>{children}</MenuItemSelectableText>
133
+ </MenuItemSelectableRow>
134
+ </MenuItem>
135
+ );
136
+ };
@@ -0,0 +1,15 @@
1
+ import type { TypeMenuItemProps } from "../MenuItem";
2
+
3
+ export const INPUT_TYPES = {
4
+ CHECKBOX: "checkbox",
5
+ RADIO: "radio",
6
+ SWITCH: "switch",
7
+ ICON: "icon",
8
+ } as const;
9
+ export type InputType = (typeof INPUT_TYPES)[keyof typeof INPUT_TYPES];
10
+
11
+ export interface TypeMenuItemSelectableProps extends TypeMenuItemProps {
12
+ active?: boolean;
13
+ inputType?: InputType;
14
+ inputLabel?: string;
15
+ }
@@ -0,0 +1,303 @@
1
+ /* eslint-disable testing-library/no-unnecessary-act */
2
+ import React from "react";
3
+ import { render, screen, act } from "@sproutsocial/seeds-react-testing-library";
4
+ import { PointerEventsCheckLevel } from "@testing-library/user-event";
5
+ import { type UserEvent } from "@testing-library/user-event";
6
+ import {
7
+ MenuContent,
8
+ MenuToggleButton,
9
+ MenuItemSelectable,
10
+ MultiSelectMenu,
11
+ INPUT_TYPES,
12
+ } from "../../index";
13
+ import { Button } from "@sproutsocial/seeds-react-button";
14
+ import { TestWithMenuRoot, TestWithMockedMenu } from "../../menuTestingUtils";
15
+
16
+ const books = [
17
+ {
18
+ index: 0,
19
+ id: "book-1",
20
+ author: "Harper Lee",
21
+ title: "To Kill a Mockingbird",
22
+ },
23
+ { index: 1, id: "book-2", author: "Lev Tolstoy", title: "War and Peace" },
24
+ ];
25
+
26
+ const testByType = {
27
+ [INPUT_TYPES.CHECKBOX]: {
28
+ isSelected: () => {
29
+ expect(
30
+ screen.getByRole("checkbox", { checked: true })
31
+ ).toBeInTheDocument();
32
+ },
33
+ isNotSelected: () => {
34
+ expect(
35
+ screen.queryByRole("checkbox", { checked: true })
36
+ ).not.toBeInTheDocument();
37
+ },
38
+ },
39
+ [INPUT_TYPES.RADIO]: {
40
+ isSelected: () => {
41
+ expect(screen.getByRole("radio", { checked: true })).toBeInTheDocument();
42
+ },
43
+ isNotSelected: () => {
44
+ expect(
45
+ screen.queryByRole("radio", { checked: true })
46
+ ).not.toBeInTheDocument();
47
+ },
48
+ },
49
+ [INPUT_TYPES.SWITCH]: {
50
+ isSelected: () => {
51
+ expect(screen.getByRole("switch", { checked: true })).toBeInTheDocument();
52
+ },
53
+ isNotSelected: () => {
54
+ expect(
55
+ screen.queryByRole("switch", { checked: true })
56
+ ).not.toBeInTheDocument();
57
+ },
58
+ },
59
+ [INPUT_TYPES.ICON]: {
60
+ isSelected: () => {
61
+ expect(
62
+ screen.getByDataQaLabel({ icon: "check-solid" })
63
+ ).toBeInTheDocument();
64
+ },
65
+ isNotSelected: () => {
66
+ expect(
67
+ screen.queryByDataQaLabel({ icon: "check-solid" })
68
+ ).not.toBeInTheDocument();
69
+ },
70
+ },
71
+ };
72
+
73
+ describe("MenuItemSelectable ", () => {
74
+ describe("User Behavior", () => {
75
+ xdescribe.each([
76
+ INPUT_TYPES.CHECKBOX,
77
+ INPUT_TYPES.RADIO,
78
+ INPUT_TYPES.SWITCH,
79
+ INPUT_TYPES.ICON,
80
+ ])("when the input type is %s", (inputType) => {
81
+ test.each([
82
+ {
83
+ action: "clicking with mouse",
84
+ activate: async (user: UserEvent) => {
85
+ const firstItem = screen.getByRole("option", {
86
+ name: books[0].title,
87
+ });
88
+ await user
89
+ .setup({
90
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
91
+ })
92
+ .click(firstItem);
93
+ },
94
+ deactivate: async (user: UserEvent) => {
95
+ const firstItem = screen.getByRole("option", {
96
+ name: books[0].title,
97
+ });
98
+ await user
99
+ .setup({
100
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
101
+ })
102
+ .click(firstItem);
103
+ },
104
+ },
105
+ {
106
+ action: "pressing the Enter key",
107
+ activate: async (user: UserEvent) => {
108
+ await user.keyboard("{arrowdown}{enter}");
109
+ },
110
+ deactivate: async (user: UserEvent) => {
111
+ await user.keyboard("{enter}");
112
+ },
113
+ },
114
+ {
115
+ action: "pressing the Space key",
116
+ activate: async (user: UserEvent) => {
117
+ await user.keyboard("{arrowdown}{space}");
118
+ },
119
+ deactivate: async (user: UserEvent) => {
120
+ await user.keyboard("{space}");
121
+ },
122
+ },
123
+ ])(
124
+ "can be checked and unchecked when $action",
125
+ async ({ activate, deactivate }) => {
126
+ const { user } = render(
127
+ <MultiSelectMenu
128
+ triggerElement={<MenuToggleButton>Open</MenuToggleButton>}
129
+ defaultIsOpen
130
+ >
131
+ <MenuContent>
132
+ {books.map((book) => {
133
+ return (
134
+ <MenuItemSelectable
135
+ key={book.id}
136
+ id={book.id}
137
+ inputType={inputType}
138
+ >
139
+ {book.title}
140
+ </MenuItemSelectable>
141
+ );
142
+ })}
143
+ </MenuContent>
144
+ </MultiSelectMenu>
145
+ );
146
+
147
+ // check that menu is visible
148
+ expect(
149
+ screen.getByRole("option", { name: books[0].title })
150
+ ).toBeInTheDocument();
151
+
152
+ // Check that checkboxes are rendered
153
+ if (inputType !== INPUT_TYPES.ICON)
154
+ expect(screen.getAllByRole(inputType)).toHaveLength(books.length);
155
+
156
+ // Perform the action to select the item
157
+ await activate(user);
158
+
159
+ expect(
160
+ await screen.findByRole("option", { selected: true })
161
+ ).toBeInTheDocument();
162
+
163
+ testByType[inputType].isSelected();
164
+
165
+ // Perform the action to deselect the item
166
+ deactivate(user);
167
+
168
+ expect(
169
+ await screen.findByRole("option", {
170
+ name: books[0].title,
171
+ selected: false,
172
+ })
173
+ ).toBeInTheDocument();
174
+
175
+ testByType[inputType].isNotSelected();
176
+ }
177
+ );
178
+ });
179
+ });
180
+
181
+ describe("API", () => {
182
+ it("accepts children", () => {
183
+ render(
184
+ <MenuItemSelectable id="test-menu-item-selectable">
185
+ Hello world!
186
+ <Button>Hello menu item!</Button>
187
+ </MenuItemSelectable>
188
+ );
189
+
190
+ const menuItemSelectable = screen.getByText("Hello world!");
191
+ const button = screen.getByText("Hello menu item!");
192
+ expect(menuItemSelectable).toContainElement(button);
193
+ });
194
+
195
+ test.each([INPUT_TYPES.CHECKBOX, INPUT_TYPES.RADIO, INPUT_TYPES.SWITCH])(
196
+ "supports %s input types",
197
+ (inputType) => {
198
+ render(
199
+ <MenuItemSelectable
200
+ id="test-menu-item-selectable"
201
+ inputType={inputType}
202
+ >
203
+ Hello world!
204
+ </MenuItemSelectable>
205
+ );
206
+
207
+ expect(screen.getByRole(inputType)).toBeInTheDocument();
208
+ }
209
+ );
210
+
211
+ it("uses the check icon by default when no inputType provided", () => {
212
+ const testItem = books[0];
213
+ render(
214
+ <TestWithMockedMenu context={{ items: books, selectedItem: testItem }}>
215
+ <MenuItemSelectable id={testItem.id}>
216
+ {testItem.title}
217
+ </MenuItemSelectable>
218
+ </TestWithMockedMenu>
219
+ );
220
+
221
+ expect(
222
+ screen.getByDataQaLabel({ icon: "check-solid" })
223
+ ).toBeInTheDocument();
224
+ });
225
+
226
+ describe.each([
227
+ INPUT_TYPES.CHECKBOX,
228
+ INPUT_TYPES.RADIO,
229
+ INPUT_TYPES.SWITCH,
230
+ INPUT_TYPES.ICON,
231
+ ])("when the input type is %s", (inputType) => {
232
+ it("enables it's checked state when selected in the context", () => {
233
+ const testItem = books[0];
234
+ render(
235
+ <TestWithMockedMenu
236
+ context={{ items: books, selectedItem: testItem }}
237
+ >
238
+ <MenuItemSelectable id={testItem.id} inputType={inputType}>
239
+ {testItem.title}
240
+ </MenuItemSelectable>
241
+ </TestWithMockedMenu>
242
+ );
243
+
244
+ screen.getByRole("option", { selected: true });
245
+ testByType[inputType].isSelected();
246
+ });
247
+
248
+ it("enables it's checked state when selected by default", () => {
249
+ const testItem = books[0];
250
+ render(
251
+ <TestWithMenuRoot
252
+ triggerElement={<MenuToggleButton>Open</MenuToggleButton>}
253
+ {...{
254
+ isOpen: true,
255
+ items: books,
256
+ selectedItem: testItem,
257
+ }}
258
+ >
259
+ <MenuItemSelectable id={testItem.id} inputType={inputType}>
260
+ {testItem.title}
261
+ </MenuItemSelectable>
262
+ </TestWithMenuRoot>
263
+ );
264
+
265
+ screen.getByRole("option", { selected: true });
266
+ testByType[inputType].isSelected();
267
+ });
268
+
269
+ it("allows its checked state to be deselected when using the context", async () => {
270
+ const testItem = books[0];
271
+ const TestItemWithState = () => {
272
+ const [selectedItem, setSelectedItem] = React.useState<
273
+ typeof testItem | undefined
274
+ >(testItem);
275
+ return (
276
+ <TestWithMockedMenu context={{ items: books, selectedItem }}>
277
+ <MenuItemSelectable
278
+ id={testItem.id}
279
+ inputType={inputType}
280
+ onClick={() => setSelectedItem(undefined)}
281
+ >
282
+ {testItem.title}
283
+ </MenuItemSelectable>
284
+ </TestWithMockedMenu>
285
+ );
286
+ };
287
+ const { user } = render(<TestItemWithState />);
288
+
289
+ const menuItem = screen.getByRole("option", { selected: true });
290
+ testByType[inputType].isSelected();
291
+
292
+ await user.click(menuItem);
293
+
294
+ expect(
295
+ await screen.findByRole("option", {
296
+ selected: false,
297
+ })
298
+ ).toBeInTheDocument();
299
+ testByType[inputType].isNotSelected();
300
+ });
301
+ });
302
+ });
303
+ });