@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
package/jest.config.js ADDED
@@ -0,0 +1,12 @@
1
+ const baseConfig = require("@sproutsocial/seeds-testing");
2
+
3
+ /**
4
+ * @type {import('jest').Config}
5
+ */
6
+ const config = {
7
+ ...baseConfig,
8
+ displayName: "seeds-react-menu",
9
+ setupFilesAfterEnv: ["@sproutsocial/seeds-testing/setupAfterEnv"],
10
+ };
11
+
12
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sproutsocial/seeds-react-menu",
3
+ "version": "0.1.0",
4
+ "description": "Seeds React Menu",
5
+ "author": "Sprout Social, Inc.",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/esm/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsup --dts",
12
+ "dev": "tsup --watch --dts",
13
+ "clean": "rm -rf .turbo dist",
14
+ "clean:modules": "rm -rf node_modules",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "jest",
17
+ "test:watch": "jest --watch --coverage=false"
18
+ },
19
+ "dependencies": {
20
+ "@sproutsocial/seeds-react-box": "*",
21
+ "@sproutsocial/seeds-react-button": "*",
22
+ "@sproutsocial/seeds-react-icon": "*",
23
+ "@sproutsocial/seeds-react-checkbox": "*",
24
+ "@sproutsocial/seeds-react-popout": "*",
25
+ "@sproutsocial/seeds-react-radio": "*",
26
+ "@sproutsocial/seeds-react-switch": "*",
27
+ "@sproutsocial/seeds-react-system-props": "*",
28
+ "@sproutsocial/seeds-react-theme": "*",
29
+ "downshift": "9.0.4"
30
+ },
31
+ "devDependencies": {
32
+ "@sproutsocial/eslint-config-seeds": "*",
33
+ "@sproutsocial/seeds-react-testing-library": "*",
34
+ "@sproutsocial/seeds-testing": "*",
35
+ "@types/react": "^18.0.0",
36
+ "@types/react-dom": "^18.0.0",
37
+ "@types/styled-components": "^5.1.26",
38
+ "react": "^18.0.0",
39
+ "react-dom": "^18.0.0",
40
+ "typescript": "^5.2.2",
41
+ "tsup": "^8.0.2"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^17.0.0 || ^18.0.0",
45
+ "styled-components": "^5.2.3"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ }
@@ -0,0 +1,20 @@
1
+ Feature: Action Menu
2
+
3
+ A simple Menu for triggering a single action from a list of actions.
4
+
5
+ Rule: API
6
+ Background:
7
+ Given an engineer is implementing a ActionMenu
8
+
9
+ Scenario: ActionMenu extends Menu
10
+ When Menu features are used in ActionMenu
11
+ Then the ActionMenu responds as a Menu would
12
+
13
+ Scenario: ActionMenu accepts a list of MenuItems
14
+ When an array of MenuItems is passed to the ActionMenu as children
15
+ Then the ActionMenu should render the MenuItems
16
+
17
+ Scenario: ActionMenu automatically closes after an item is clicked
18
+ When a MenuItem is clicked
19
+ And the corresponding onClick executes
20
+ Then the ActionMenu should no longer render
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import {
3
+ useSelect,
4
+ type UseSelectProps,
5
+ type UseSelectState,
6
+ type UseSelectSelectedItemChange,
7
+ } from "downshift";
8
+ import type { TypeDefaultItemProps } from "../types";
9
+ import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
10
+ import {
11
+ defaultUseSelectProps,
12
+ useSelectStateReducer,
13
+ } from "../reducers/useSelectStateReducer";
14
+ import {
15
+ MenuRoot,
16
+ type TypeMenuRootProps,
17
+ type TypeMenuRootOwnProps,
18
+ } from "../MenuRoot";
19
+ import { itemToString } from "../utils/itemToString";
20
+
21
+ type TypeInvalidMenuProps =
22
+ | "initialSelectedItem"
23
+ | "defaultSelectedItem"
24
+ | "selectedItem";
25
+ export interface TypeActionMenuProps
26
+ extends TypeMenuRootOwnProps,
27
+ Partial<Omit<UseSelectProps<TypeDefaultItemProps>, TypeInvalidMenuProps>> {
28
+ children: TypeMenuRootProps["children"];
29
+ }
30
+
31
+ export const ActionMenu = ({
32
+ children,
33
+ stateReducer: externalStateReducer,
34
+ ...useSelectProps
35
+ }: TypeActionMenuProps) => {
36
+ const { allMenuItems, menuItemsMap } = useItemsFromChildren({ children });
37
+
38
+ // Manually fire the onClick handler when Enter or Space is pressed
39
+ const handleStateChange = (
40
+ changes: UseSelectSelectedItemChange<TypeDefaultItemProps>
41
+ ) => {
42
+ const { selectedItem } = changes;
43
+
44
+ // if selectedItem is disabled do nothing
45
+ if (selectedItem.element?.props?.disabled) {
46
+ return;
47
+ }
48
+
49
+ // Listen for Enter or Space key
50
+ if (
51
+ changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter ||
52
+ changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton
53
+ ) {
54
+ // Determine if Enter key was pressed
55
+ const isEnterKeyDown =
56
+ changes.type === useSelect.stateChangeTypes.ToggleButtonKeyDownEnter;
57
+
58
+ // selectedItem props
59
+ const selectedItemProps = selectedItem.element?.props;
60
+
61
+ // Create a synthetic event to pass into the onClick
62
+ const syntheticKeyboardEvent = new KeyboardEvent("keydown", {
63
+ key: isEnterKeyDown ? "Enter" : " ",
64
+ code: isEnterKeyDown ? "Enter" : "Space",
65
+ bubbles: true,
66
+ cancelable: true,
67
+ view: window,
68
+ });
69
+
70
+ // Manually call the onClick handler with a simulated event
71
+ // Cast it to React.MouseEvent to match onClick expectations
72
+ selectedItemProps?.onClick?.(
73
+ syntheticKeyboardEvent as unknown as React.MouseEvent<HTMLElement>
74
+ );
75
+ }
76
+ };
77
+
78
+ const { isOpen, ...useSelectReturnProps } = useSelect({
79
+ ...defaultUseSelectProps,
80
+ items: allMenuItems,
81
+ itemToString,
82
+ onSelectedItemChange: handleStateChange,
83
+ stateReducer: (state, actionAndChanges) => {
84
+ let newState: Partial<UseSelectState<TypeDefaultItemProps>>;
85
+ newState = useSelectStateReducer(state, actionAndChanges);
86
+ return externalStateReducer
87
+ ? externalStateReducer(state, {
88
+ ...actionAndChanges,
89
+ changes: newState,
90
+ })
91
+ : newState;
92
+ },
93
+ ...useSelectProps,
94
+ });
95
+
96
+ return (
97
+ <MenuRoot
98
+ {...{
99
+ isListbox: false,
100
+ items: allMenuItems,
101
+ itemsMap: menuItemsMap,
102
+ itemToString,
103
+ isOpen,
104
+ ...useSelectProps,
105
+ ...useSelectReturnProps,
106
+ }}
107
+ >
108
+ {children}
109
+ </MenuRoot>
110
+ );
111
+ };
112
+
113
+ export default ActionMenu;
@@ -0,0 +1,99 @@
1
+ import React from "react";
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@sproutsocial/seeds-react-testing-library";
8
+ import { ActionMenu } from "../ActionMenu";
9
+ import { MenuContent, MenuItem, MenuToggleButton } from "../../index";
10
+
11
+ describe("ActionMenu", () => {
12
+ it.skip("extends Menu", () => {
13
+ // run shared tests for all menus
14
+ });
15
+
16
+ it("should render the MenuItems when provided as children", async () => {
17
+ render(
18
+ <ActionMenu
19
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
20
+ >
21
+ <MenuContent>
22
+ <MenuItem id="test-item-1">Item 1</MenuItem>
23
+ <MenuItem id="test-item-2">Item 2</MenuItem>
24
+ </MenuContent>
25
+ </ActionMenu>
26
+ );
27
+
28
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
29
+
30
+ const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
31
+ fireEvent.click(toggleBtn);
32
+
33
+ await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
34
+ expect(screen.getByRole("menuitem", { name: "Item 1" })).toBeVisible();
35
+ expect(screen.getByRole("menuitem", { name: "Item 2" })).toBeVisible();
36
+ });
37
+
38
+ it("should close after a MenuItem is clicked", async () => {
39
+ const item1MockClick = jest.fn();
40
+ render(
41
+ <ActionMenu
42
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
43
+ >
44
+ <MenuContent>
45
+ <MenuItem id="test-item-1" onClick={item1MockClick}>
46
+ Item 1
47
+ </MenuItem>
48
+ <MenuItem id="test-item-2">Item 2</MenuItem>
49
+ </MenuContent>
50
+ </ActionMenu>
51
+ );
52
+
53
+ const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
54
+ fireEvent.click(toggleBtn);
55
+
56
+ await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
57
+
58
+ const item1 = screen.getByRole("menuitem", { name: "Item 1" });
59
+ expect(item1).toBeVisible();
60
+ fireEvent.click(item1);
61
+
62
+ await waitFor(() => {
63
+ expect(item1MockClick).toHaveBeenCalled();
64
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
65
+ });
66
+ });
67
+
68
+ // TODO: Should be fixed after onClick bug fixed on menu items
69
+ it.skip("should close after a MenuItem is accessed by the keyboard", async () => {
70
+ const item1MockClick = jest.fn();
71
+ const { user } = render(
72
+ <ActionMenu
73
+ triggerElement={<MenuToggleButton>Test Menu</MenuToggleButton>}
74
+ >
75
+ <MenuContent>
76
+ <MenuItem id="test-item-1" onClick={item1MockClick}>
77
+ Item 1
78
+ </MenuItem>
79
+ <MenuItem id="test-item-2">Item 2</MenuItem>
80
+ </MenuContent>
81
+ </ActionMenu>
82
+ );
83
+
84
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
85
+ const toggleBtn = screen.getByRole("button", { name: "Test Menu" });
86
+ fireEvent.click(toggleBtn);
87
+
88
+ await waitFor(() => expect(screen.getByRole("menu")).toBeVisible());
89
+
90
+ await user.keyboard("{arrowdown}{enter}");
91
+
92
+ await waitFor(() => {
93
+ expect(item1MockClick).toHaveBeenCalled();
94
+ });
95
+ await waitFor(() => {
96
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument();
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,73 @@
1
+ import { ActionMenu, MenuContent, MenuHeader, MenuFooter } from "../../index";
2
+
3
+ export const ActionMenuTypeTest = () => (
4
+ <>
5
+ {/* Valid ActionMenu with minimal required props */}
6
+ <ActionMenu triggerElement={<>test</>}>Test</ActionMenu>
7
+ {/* Valid ActionMenu allows MenuContent by itself as a child */}
8
+ <ActionMenu triggerElement={<>test</>}>
9
+ <MenuContent>Test</MenuContent>
10
+ </ActionMenu>
11
+ {/* Valid ActionMenu allows multiple children without fragment */}
12
+ <ActionMenu triggerElement={<>test</>}>
13
+ <MenuHeader>Test</MenuHeader>
14
+ <MenuContent>Test</MenuContent>
15
+ <MenuFooter>Test</MenuFooter>
16
+ </ActionMenu>
17
+ {/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
18
+ <ActionMenu
19
+ triggerElement={<>test</>}
20
+ onSelectedItemChange={() => {}}
21
+ stateReducer={(c) => c}
22
+ isOpen
23
+ >
24
+ Test
25
+ </ActionMenu>
26
+ {/* Valid ActionMenu accepts Downshift props and functions for useMultipleSelection */}
27
+ <ActionMenu triggerElement={<>test</>}>Test</ActionMenu>
28
+
29
+ {/* @ts-expect-error - triggerElement is a required prop */}
30
+ <ActionMenu>Test</ActionMenu>
31
+ <ActionMenu
32
+ /* @ts-expect-error - triggerElement must be a valid react element */
33
+ triggerElement="test"
34
+ >
35
+ Test
36
+ </ActionMenu>
37
+ {/* @ts-expect-error - when no children provided */}
38
+ <ActionMenu triggerElement="test" />
39
+
40
+ <ActionMenu
41
+ triggerElement={<>test</>}
42
+ /* @ts-expect-error - isOpen can't be a string */
43
+ isOpen="false"
44
+ /* @ts-expect-error - onSelectedItemChange can't be a string */
45
+ onSelectedItemChange="test"
46
+ /* @ts-expect-error - stateReducer must have a return value */
47
+ stateReducer={() => {}}
48
+ >
49
+ Test
50
+ </ActionMenu>
51
+ <ActionMenu
52
+ triggerElement={<>test</>}
53
+ /* @ts-expect-error - selectedItem is never allowed for ActionMenu */
54
+ selectedItem={null}
55
+ >
56
+ Test
57
+ </ActionMenu>
58
+ <ActionMenu
59
+ triggerElement={<>test</>}
60
+ /* @ts-expect-error - initialSelectedItem is never allowed for ActionMenu */
61
+ initialSelectedItem={null}
62
+ >
63
+ Test
64
+ </ActionMenu>
65
+ <ActionMenu
66
+ triggerElement={<>test</>}
67
+ /* @ts-expect-error - defaultSelectedItem is never allowed for ActionMenu */
68
+ defaultSelectedItem={null}
69
+ >
70
+ Test
71
+ </ActionMenu>
72
+ </>
73
+ );
@@ -0,0 +1 @@
1
+ export * from "./ActionMenu";
@@ -0,0 +1,96 @@
1
+ Feature: ComboBox
2
+
3
+ A component that allows users to search, select, and manage items with autocomplete functionality.
4
+
5
+ Rule: User behavior
6
+
7
+ Background:
8
+ Given an end user is using the component
9
+
10
+ Scenario: User can autocomplete text inputs
11
+ Given the ComboBox Input is focused
12
+ When text is typed into the input
13
+ Then a list of matching items is displayed
14
+ And an item can be selected from the list
15
+
16
+ Scenario: User can add a new value
17
+ Given the Combobox Input has a value
18
+ When new values are allowed
19
+ And the value has no match
20
+ Then a new value can be created
21
+
22
+ Scenario: User gets a warning on error
23
+ Given the Combobox Input has a value
24
+ When the value is invalid
25
+ Then a warning message appears
26
+
27
+ Scenario: User can select an item from the list
28
+ Given the ComboBox Input has text
29
+ When an item is selected
30
+ Then the item is marked as selected
31
+ And the input reflects the selected item
32
+
33
+ Scenario: Screen reader announces combobox
34
+ Given a screen reader is being used
35
+ When the Combobox input is focused
36
+ Then the screen reader announces it as a "combobox"
37
+
38
+ Scenario: Component handles input change
39
+ When the input value changes
40
+ Then the onChange callback fires
41
+
42
+ Scenario Outline: Custom open/close behavior
43
+ Given the ComboBox is rendered with custom behavior
44
+ When the `<action>` is performed
45
+ Then the `<handler>` is executed
46
+
47
+ Examples: Custom Behavior
48
+ | action | handler |
49
+ | item selected | onSelect |
50
+ | input focused | onFocus |
51
+ | input blurred | onBlur |
52
+
53
+ Scenario Outline: Autocomplete suggestions are displayed
54
+ Given the ComboBox is rendered with items
55
+ When <text> is typed into the input
56
+ Then matching <suggestion> items are displayed
57
+
58
+ Examples: Autocomplete Suggestions
59
+ | text | suggestion |
60
+ | ---------- | ---------------------- |
61
+ | app | apple, application |
62
+ | ban | banana, band |
63
+
64
+ Rule: API
65
+ Background:
66
+ Given an engineer is developing using the component
67
+
68
+ Scenario: ComboBox extends Menu
69
+ When Menu features are used in ComboBox
70
+ Then the ComboBox responds as a Menu would
71
+
72
+ Scenario: ComboBox accepts selected item
73
+ When a selected item is passed to the ComboBox
74
+ Then the input reflects the selected item
75
+
76
+ Scenario: ComboBox hasWarning
77
+ When the input's validation has an error
78
+ Then 'hasWarning' becomes true
79
+
80
+ Scenario Outline: ComboBox checks its types
81
+ When <propName> is passed a value
82
+ Then ComboBox expects it to be of type <propType>
83
+
84
+ Examples: Prop Types
85
+ | propName | propType |
86
+ | delimiters | array of strings |
87
+ | value | string |
88
+ | hasFocus | boolean |
89
+ | onChange | function |
90
+ | onPaste | function |
91
+ | autoFocus | boolean |
92
+ | isInvalid | boolean |
93
+ | hasWarning | boolean |
94
+ | required | boolean |
95
+ | inputProps | any |
96
+ | autocomplete | string |
@@ -0,0 +1,84 @@
1
+ Feature: TokenInput
2
+
3
+ A component that allows users to search, select, and manage tokens with autocomplete functionality.
4
+
5
+ Rule: User behavior
6
+
7
+ Background:
8
+ Given an end user is using the component
9
+
10
+ Scenario: User can add tokens
11
+ Given the ComboBox accepts new values
12
+ When text is typed
13
+ And a delimiter key is pressed
14
+ Then a new token is created from the input text
15
+
16
+ Scenario: User can not add tokens
17
+ Given the ComboBox does not accept new values
18
+ When text is typed
19
+ And a delimiter key is pressed
20
+ Then no new token is added
21
+
22
+ Scenario: User can remove tokens manually
23
+ Given the ComboBox has tokens
24
+ When token's remove button is clicked
25
+ Then the token is removed
26
+
27
+ Scenario: User can remove tokens with backspace
28
+ Given the ComboBox has tokens
29
+ When backspace is hit
30
+ Then the token is removed
31
+
32
+ Scenario: User can navigate tokens via keyboard
33
+ Given the ComboBox has tokens
34
+ And the Combobox Input has focus
35
+ When the tokens are navigated using arrow keys
36
+ Then the activeToken callback fires
37
+
38
+ Scenario: Component handles token addition
39
+ When a new token is added
40
+ Then the onAddToken callback fires
41
+
42
+ Scenario: Component handles token removal
43
+ Given the ComboBox is rendered with tokens
44
+ When a token is removed
45
+ Then the onRemoveToken callback fires
46
+
47
+ Scenario: Component handles token click
48
+ When a token is clicked
49
+ Then the onClickToken callback fires
50
+
51
+ Scenario: Component handles input change
52
+ When the input value changes
53
+ Then the onChange callback fires
54
+
55
+ Rule: API
56
+ Background:
57
+ Given an engineer is developing using the component
58
+
59
+ Scenario: TokenInput extends Combobox
60
+ When Combobox features are used in TokenInput
61
+ Then the TokenInput responds as a Combobox would
62
+
63
+ Scenario: ComboBox accepts tokens
64
+ When a token component is passed to the Combobox
65
+ And an item is selected
66
+ Then the token is rendered in the input
67
+
68
+ Scenario: ComboBox has a maximum token length
69
+ When 'tokenMaxLength' is set to <value>
70
+ Then the token string size is limited to <value>
71
+
72
+ Scenario Outline: ComboBox checks its types
73
+ When <propName> is passed a value
74
+ Then ComboBox expects it to be of type <propType>
75
+
76
+ Examples: Prop Types
77
+ | activeToken | string |
78
+ | tokens | array of { id: string, iconName?: EnumIconNames, value: string, valid?: boolean } |
79
+ | onChangeTokens | function |
80
+ | onAddToken | function |
81
+ | onRemoveToken | function |
82
+ | onClickToken | function |
83
+ | maxLength | number |
84
+ | tokenMaxLength | number |
@@ -0,0 +1,28 @@
1
+ import type { TypeMenuContentProps } from "./MenuContentTypes";
2
+ import { StyledMenuContent } from "./styles";
3
+ import { useMenuContext } from "../MenuContext";
4
+
5
+ const MenuContent = ({
6
+ id = "menu_content",
7
+ children,
8
+ ...rest
9
+ }: TypeMenuContentProps) => {
10
+ const { getToggleButtonProps } = useMenuContext();
11
+
12
+ return (
13
+ <StyledMenuContent
14
+ autoFocus
15
+ id={id}
16
+ tabIndex={0}
17
+ // apply onKeyDown logic for downshift keyboard accessibility support
18
+ onKeyDown={getToggleButtonProps?.().onKeyDown}
19
+ // apply active descendent updates to container for proper accessibility
20
+ aria-activedescendant={getToggleButtonProps?.()["aria-activedescendant"]}
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </StyledMenuContent>
25
+ );
26
+ };
27
+
28
+ export { MenuContent };
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { ReactNode, RefObject } from "react";
3
+
4
+ export interface TypeMenuContentProps
5
+ extends React.ButtonHTMLAttributes<HTMLUListElement> {
6
+ children: ReactNode;
7
+ }
@@ -0,0 +1,59 @@
1
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
2
+ import { MenuFooter, MenuHeader, MenuContent } from "../../index";
3
+ import Button from "@sproutsocial/seeds-react-button";
4
+
5
+ describe("MenuContent API", () => {
6
+ it("should render correctly with default props", () => {
7
+ render(
8
+ <MenuContent data-testid="menu-content">
9
+ <div>Test Child</div>
10
+ </MenuContent>
11
+ );
12
+ const menuContent = screen.getByTestId("menu-content");
13
+ expect(menuContent).toBeInTheDocument();
14
+ expect(menuContent).toHaveAttribute("tabIndex", "0");
15
+ });
16
+ });
17
+
18
+ describe("MenuContent Behavior", () => {
19
+ it("focus should move properly through menu", async () => {
20
+ const { user } = render(
21
+ <div>
22
+ <MenuHeader>
23
+ <Button data-testid="menu-header-button" onClick={() => {}}>
24
+ Button
25
+ </Button>
26
+ </MenuHeader>
27
+ <MenuContent data-testid="menu-content">
28
+ <div>Test Child</div>
29
+ </MenuContent>
30
+ <MenuFooter>
31
+ <Button data-testid="menu-footer-button" onClick={() => {}}>
32
+ Button 2
33
+ </Button>
34
+ </MenuFooter>
35
+ </div>
36
+ );
37
+
38
+ const menuHeaderButton = screen.getByTestId("menu-header-button");
39
+ const menuContent = screen.getByTestId("menu-content");
40
+ const menuFooterButton = screen.getByTestId("menu-footer-button");
41
+
42
+ //Tabs from container into header
43
+ await user.tab();
44
+ expect(menuHeaderButton).toHaveFocus();
45
+
46
+ //Tabs from header onto content
47
+ await user.tab();
48
+ expect(menuContent).toHaveFocus();
49
+
50
+ //Tabs back from content to header
51
+ await user.tab({ shift: true });
52
+ expect(menuHeaderButton).toHaveFocus();
53
+
54
+ //Tabs twice to go from header to footer
55
+ await user.tab();
56
+ await user.tab();
57
+ expect(menuFooterButton).toHaveFocus();
58
+ });
59
+ });
@@ -0,0 +1,18 @@
1
+ import { MenuContent, MenuItem } from "../../index";
2
+
3
+ export const MenuContentTypeTest = () => (
4
+ <>
5
+ {/* MenuContent allows children */}
6
+ <MenuContent>Test</MenuContent>
7
+ {/* MenuContent allows children with MenuItems */}
8
+ <MenuContent>
9
+ <MenuItem id="1">Test 1</MenuItem>
10
+ <MenuItem id="2">Test 2</MenuItem>
11
+ </MenuContent>
12
+ {/* MenuContent accepts ul props */}
13
+ <MenuContent aria-label="test label">Test</MenuContent>
14
+
15
+ {/* @ts-expect-error - MenuContent requires children */}
16
+ <MenuContent />
17
+ </>
18
+ );
@@ -0,0 +1,2 @@
1
+ export { MenuContent } from "./MenuContent";
2
+ export type { TypeMenuContentProps } from "./MenuContentTypes";