@sproutsocial/seeds-react-menu 0.4.1 → 0.6.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 (105) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +32 -0
  3. package/dist/esm/index.js +1553 -423
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/index.d.mts +303 -122
  6. package/dist/index.d.ts +303 -122
  7. package/dist/index.js +1561 -405
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -1
  10. package/src/ActionMenu/ActionMenu.stories.tsx +426 -0
  11. package/src/ActionMenu/ActionMenu.tsx +30 -26
  12. package/src/ActionMenu/ActionMenuTypes.ts +13 -8
  13. package/src/ActionMenu/__tests__/ActionMenu.test.tsx +17 -30
  14. package/src/ActionMenu/__tests__/ActionMenu.typetest.tsx +32 -3
  15. package/src/Autocomplete/Autocomplete.stories.tsx +61 -2
  16. package/src/Autocomplete/Autocomplete.tsx +15 -74
  17. package/src/Autocomplete/AutocompleteInput.tsx +18 -27
  18. package/src/Autocomplete/AutocompleteTokenInput.tsx +50 -0
  19. package/src/Autocomplete/AutocompleteTypes.ts +27 -11
  20. package/src/Autocomplete/MultiAutocomplete.tsx +189 -0
  21. package/src/Autocomplete/SingleAutocomplete.tsx +74 -0
  22. package/src/Autocomplete/__tests__/Autocomplete.test.tsx +171 -3
  23. package/src/Autocomplete/__tests__/Autocomplete.typetest.tsx +116 -3
  24. package/src/MenuContent/MenuContent.stories.tsx +164 -0
  25. package/src/MenuContent/MenuContent.tsx +35 -8
  26. package/src/MenuContent/MenuContentTypes.ts +7 -5
  27. package/src/MenuContent/__tests__/MenuContent.typetest.tsx +18 -2
  28. package/src/MenuContext.tsx +191 -37
  29. package/src/MenuFooter/MenuFooter.stories.tsx +202 -0
  30. package/src/MenuFooter/__tests__/MenuFooter.test.tsx +16 -11
  31. package/src/MenuGroup/MenuGroup.feature +21 -21
  32. package/src/MenuGroup/MenuGroup.stories.tsx +248 -0
  33. package/src/MenuGroup/MenuGroup.tsx +9 -7
  34. package/src/MenuGroup/MenuGroupTypes.ts +8 -6
  35. package/src/MenuGroup/__tests__/MenuGroup.test.tsx +6 -6
  36. package/src/MenuGroup/__tests__/MenuGroup.typetest.tsx +23 -4
  37. package/src/MenuGroup/styles.tsx +4 -2
  38. package/src/MenuHeader/MenuHeader.stories.tsx +276 -0
  39. package/src/MenuHeader/MenuHeader.tsx +3 -1
  40. package/src/MenuHeader/__tests__/MenuHeader.test.tsx +23 -13
  41. package/src/MenuHeader/styles.tsx +18 -14
  42. package/src/MenuItem/MenuItem.stories.tsx +507 -0
  43. package/src/MenuItem/MenuItem.tsx +18 -9
  44. package/src/MenuItem/MenuItemTypes.ts +9 -4
  45. package/src/MenuItem/__tests__/MenuItem.test.tsx +6 -6
  46. package/src/MenuItem/styles.tsx +21 -5
  47. package/src/MenuItem/useOptionProps.tsx +10 -8
  48. package/src/MenuLabel.tsx +2 -2
  49. package/src/MenuRoot/MenuRoot.tsx +37 -23
  50. package/src/MenuRoot/__tests__/MenuRoot.test.tsx +59 -32
  51. package/src/MenuSearchInput/MenuSearchInput.tsx +63 -0
  52. package/src/MenuSearchInput/__tests__/MenuSearchInput.test.tsx +157 -0
  53. package/src/MenuSearchInput/__tests__/MenuSearchInput.typetest.tsx +31 -0
  54. package/src/MenuSearchInput/index.ts +1 -0
  55. package/src/MenuSearchTokenInput/MenuSearchTokenInput.tsx +98 -0
  56. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.test.tsx +247 -0
  57. package/src/MenuSearchTokenInput/__tests__/MenuSearchTokenInput.typetest.tsx +33 -0
  58. package/src/MenuSearchTokenInput/index.ts +1 -0
  59. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +72 -0
  60. package/src/{MenuToggleButton.tsx → MenuToggleButton/MenuToggleButton.tsx} +6 -5
  61. package/src/MenuToggleButton/index.ts +1 -0
  62. package/src/MenuTokenInput.tsx +104 -0
  63. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +214 -0
  64. package/src/MultiSelectMenu/MultiSelectMenu.tsx +44 -67
  65. package/src/MultiSelectMenu/MultiSelectMenuTypes.ts +9 -5
  66. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.test.tsx +46 -60
  67. package/src/MultiSelectMenu/__tests__/MultiSelectMenu.typetest.tsx +29 -2
  68. package/src/NestedMenu/NestedMenu.feature +109 -0
  69. package/src/NestedMenu/NestedMenu.stories.tsx +157 -0
  70. package/src/NestedMenu/NestedMenu.tsx +102 -0
  71. package/src/NestedMenu/NestedMenuContent.tsx +71 -0
  72. package/src/NestedMenu/NestedMenuContext.tsx +56 -0
  73. package/src/NestedMenu/NestedMenuHeader.tsx +25 -0
  74. package/src/NestedMenu/NestedMenuItem.tsx +37 -0
  75. package/src/NestedMenu/NestedMenuTypes.ts +47 -0
  76. package/src/NestedMenu/__tests__/NestedMenu.test.tsx +31 -0
  77. package/src/NestedMenu/__tests__/NestedMenu.typetest.tsx +134 -0
  78. package/src/NestedMenu/index.ts +5 -0
  79. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +227 -0
  80. package/src/SingleSelectMenu/SingleSelectMenu.tsx +29 -25
  81. package/src/SingleSelectMenu/SingleSelectMenuTypes.ts +9 -5
  82. package/src/SingleSelectMenu/__tests__/SingleSelectMenu.typetest.tsx +32 -2
  83. package/src/SubmenuItem/SubmenuItem.feature +82 -0
  84. package/src/SubmenuItem/SubmenuItem.stories.tsx +163 -0
  85. package/src/SubmenuItem/SubmenuItem.tsx +254 -0
  86. package/src/SubmenuItem/SubmenuItemTypes.ts +24 -0
  87. package/src/SubmenuItem/__tests__/SubmenuItem.test.tsx +3 -0
  88. package/src/SubmenuItem/__tests__/SubmenuItem.typetest.tsx +112 -0
  89. package/src/SubmenuItem/index.ts +2 -0
  90. package/src/__tests__/Menu.test.tsx +38 -0
  91. package/src/hooks/useDownshiftDefaults.tsx +107 -0
  92. package/src/hooks/useItemFiltering.tsx +66 -0
  93. package/src/hooks/useMenuChildren.tsx +279 -0
  94. package/src/index.ts +7 -1
  95. package/src/menuTestingUtils.tsx +21 -10
  96. package/src/reducers/nestedMenuStateReducer.tsx +23 -0
  97. package/src/reducers/useComboboxStateReducer.tsx +26 -8
  98. package/src/reducers/useSelectStateReducer.tsx +24 -6
  99. package/src/storybookUtilities/menu-storybook-components.tsx +5 -3
  100. package/src/types.ts +38 -5
  101. package/src/utils/downshiftDefaults.ts +9 -0
  102. package/src/utils/getMultiSelectOverrides.ts +21 -0
  103. package/src/utils/reduceReducers.ts +19 -0
  104. package/src/hooks/useItemsFromChildren.tsx +0 -131
  105. package/src/utils/itemToString.ts +0 -5
@@ -1,4 +1,10 @@
1
- import { ActionMenu, MenuContent, MenuHeader, MenuFooter } from "../../index";
1
+ import {
2
+ ActionMenu,
3
+ MenuContent,
4
+ MenuHeader,
5
+ MenuFooter,
6
+ MenuItem,
7
+ } from "../../index";
2
8
 
3
9
  export const ActionMenuTypeTest = () => (
4
10
  <>
@@ -14,6 +20,17 @@ export const ActionMenuTypeTest = () => (
14
20
  <MenuContent>Test</MenuContent>
15
21
  <MenuFooter>Test</MenuFooter>
16
22
  </ActionMenu>
23
+ {/* Valid ActionMenu allows menuItems to be passed using the menuItems prop */}
24
+ <ActionMenu
25
+ menuToggleElement={<>test</>}
26
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
27
+ />
28
+ {/* Valid ActionMenu allows menuItems prop to be rendered using a MenuItemComponent prop */}
29
+ <ActionMenu
30
+ menuToggleElement={<>test</>}
31
+ menuItems={[{ children: "Item Name", id: "item-1" }]}
32
+ MenuItemComponent={MenuItem}
33
+ />
17
34
  {/* Valid ActionMenu accepts Downshift props and functions for useSelect other than selectedItem */}
18
35
  <ActionMenu
19
36
  menuToggleElement={<>test</>}
@@ -34,8 +51,20 @@ export const ActionMenuTypeTest = () => (
34
51
  >
35
52
  Test
36
53
  </ActionMenu>
37
- {/* @ts-expect-error - when no children provided */}
38
- <ActionMenu menuToggleElement="test" />
54
+ {/* @ts-expect-error - when both children and MenuItemComponent provided */}
55
+ <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuItem}>
56
+ Children
57
+ </ActionMenu>
58
+ {/* @ts-expect-error - when both children and menuItems provided */}
59
+ <ActionMenu menuToggleElement={<>test</>} menuItems={[]}>
60
+ Children
61
+ </ActionMenu>
62
+ {/* @ts-expect-error - when no children or items provided */}
63
+ <ActionMenu menuToggleElement={<>test</>} />
64
+ {/* @ts-expect-error - when MenuItemComponent is of the wrong type */}
65
+ <ActionMenu menuToggleElement={<>test</>} MenuItemComponent={MenuContent}>
66
+ Children
67
+ </ActionMenu>
39
68
 
40
69
  <ActionMenu
41
70
  menuToggleElement={<>test</>}
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable react-hooks/rules-of-hooks */
2
2
  import React from "react";
3
3
  import type { Meta, StoryObj } from "@storybook/react";
4
+ import { action } from "@storybook/addon-actions";
4
5
 
5
6
  import { Box } from "@sproutsocial/seeds-react-box";
6
7
  import { Icon } from "@sproutsocial/seeds-react-icon";
@@ -10,6 +11,7 @@ import { TEST_BOOKS } from "../__fixtures__/menu-test-data";
10
11
 
11
12
  import { Autocomplete } from "./Autocomplete";
12
13
  import { AutocompleteInput } from "./AutocompleteInput";
14
+ import { AutocompleteTokenInput } from "./AutocompleteTokenInput";
13
15
 
14
16
  type CustomAutocompleteArgs = React.ComponentProps<typeof Autocomplete>;
15
17
 
@@ -23,7 +25,7 @@ const meta: Meta<CustomAutocompleteArgs> = {
23
25
  },
24
26
  decorators: [
25
27
  (Story) => (
26
- <Box display="flex" flexDirection="column" gap="16px" maxWidth={300}>
28
+ <Box display="flex" flexDirection="column" gap="16px" maxWidth={350}>
27
29
  <Story />
28
30
  </Box>
29
31
  ),
@@ -62,6 +64,30 @@ export const AutocompleteTextInputExample: Story = {
62
64
  },
63
65
  };
64
66
 
67
+ export const AutocompleteWithItemsAsProps: Story = {
68
+ name: "With Items as Props",
69
+ render: (args) => {
70
+ return (
71
+ <Autocomplete
72
+ itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
73
+ menuToggleElement={
74
+ <>
75
+ <MenuLabel htmlFor="my-combobox" mb={300}>
76
+ Choose a book
77
+ </MenuLabel>
78
+ <AutocompleteInput />
79
+ </>
80
+ }
81
+ menuItems={TEST_BOOKS.map(({ id, title }) => ({
82
+ children: title,
83
+ id,
84
+ }))}
85
+ {...(args as object)}
86
+ />
87
+ );
88
+ },
89
+ };
90
+
65
91
  export const AutocompleteTextWithButtonExample: Story = {
66
92
  name: "Text Input w/ Button",
67
93
  render: (args) => {
@@ -95,12 +121,45 @@ export const AutocompleteTextWithButtonExample: Story = {
95
121
  },
96
122
  };
97
123
 
124
+ export const AutocompleteTokenInputExample: Story = {
125
+ name: "Token Input w/ Button",
126
+ render: (args) => {
127
+ return (
128
+ <Autocomplete
129
+ itemToString={(item) => (item ? TEST_BOOKS[item.index].title : "")}
130
+ menuToggleElement={
131
+ <>
132
+ <MenuLabel htmlFor="my-combobox" mb={300}>
133
+ Choose a book
134
+ </MenuLabel>
135
+ <AutocompleteTokenInput />
136
+ <MenuToggleButton mt={200} width="100%" p="0">
137
+ <Icon name="chevron-down" aria-label="Open Menu" />
138
+ </MenuToggleButton>
139
+ </>
140
+ }
141
+ onStateChange={(changes) => action("onStateChange")(changes)}
142
+ multiSelect
143
+ {...(args as object)}
144
+ >
145
+ <MenuContent>
146
+ {TEST_BOOKS.map(({ id, title }) => (
147
+ <MenuItem key={id} id={id}>
148
+ {title}
149
+ </MenuItem>
150
+ ))}
151
+ </MenuContent>
152
+ </Autocomplete>
153
+ );
154
+ },
155
+ };
156
+
98
157
  // export const AutocompleteControlled: Story = {
99
158
  // name: "Controlled State",
100
159
  // render: ({ inputType, ...args }) => {
101
160
  // const [isOpen, setIsOpen] = React.useState<boolean>(false);
102
161
  // const [selectedItem, setSelectedItem] =
103
- // React.useState<TypeDefaultItemProps | null>(null);
162
+ // React.useState<TypeInternalItemProps | null>(null);
104
163
  // return (
105
164
  // <Autocomplete
106
165
  // isOpen={isOpen}
@@ -1,83 +1,24 @@
1
1
  import React from "react";
2
- import { useCombobox, type UseComboboxState } from "downshift";
3
- import { useItemsFromChildren } from "../hooks/useItemsFromChildren";
4
- import {
5
- defaultUseComboboxProps,
6
- useComboboxStateReducer,
7
- } from "../reducers/useComboboxStateReducer";
8
- import { itemToString as defaultItemToString } from "../utils/itemToString";
9
- import { MenuRoot } from "../MenuRoot";
2
+
3
+ import type { TypeMenuItemProps } from "../MenuItem";
4
+ import type { TypeMenuRootProps } from "../MenuRoot";
10
5
  import type { TypeAutocompleteProps } from "./AutocompleteTypes";
11
- import type { TypeDefaultItemProps } from "../types";
6
+ import { SingleAutocomplete } from "./SingleAutocomplete";
7
+ import { MultiAutocomplete } from "./MultiAutocomplete";
12
8
 
13
9
  /**
14
- * @link https://seeds.sproutsocial.com/components/multiselect-menu/
10
+ * @link https://seeds.sproutsocial.com/components/autocomplete/
15
11
  *
16
- * @description A Autocomplete allows multiple {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems} to be selected from a list or groups of lists.
12
+ * @description A Autocomplete allows selecting a {@link https://github.com/sproutsocial/seeds/tree/dev/seeds-react/seeds-react-menu/src/MenuItem | MenuItems} to be selected from a list or groups of lists to populate an input.
17
13
  *
18
- * @see {@link https://seeds.sproutsocial.com/components/menu-group/ | MenuGroup}
19
14
  */
15
+ export const Autocomplete = <I extends TypeMenuItemProps = TypeMenuItemProps>({
16
+ multiSelect,
17
+ ...rest
18
+ }: TypeAutocompleteProps<I>) => {
19
+ if (multiSelect) {
20
+ return <MultiAutocomplete {...rest} />;
21
+ }
20
22
 
21
- export const Autocomplete = ({
22
- children,
23
- stateReducer: externalStateReducer,
24
- itemToString = defaultItemToString,
25
- getIsItemVisible = (item, inputValue) =>
26
- itemToString(item).toLowerCase().startsWith(inputValue.toLowerCase()),
27
- ...useComboboxProps
28
- }: TypeAutocompleteProps) => {
29
- /** Get a list of items from the children */
30
- const { allMenuItems } = useItemsFromChildren({ children });
31
- const [items, setItems] = React.useState(allMenuItems);
32
-
33
- /**
34
- * Destructure Downshift props and call the core useCombobox with items and state handler
35
- */
36
- const { isOpen, ...useComboboxReturnProps } = useCombobox({
37
- ...defaultUseComboboxProps,
38
- items,
39
- itemToString,
40
- onInputValueChange({ inputValue }) {
41
- setItems(
42
- inputValue
43
- ? allMenuItems.reduce(
44
- (items, item) => [
45
- ...items,
46
- { ...item, hidden: !getIsItemVisible(item, inputValue) },
47
- ],
48
- [] as TypeDefaultItemProps[]
49
- )
50
- : allMenuItems
51
- );
52
- },
53
- stateReducer: (state, actionAndChanges) => {
54
- let newState: Partial<UseComboboxState<TypeDefaultItemProps>>;
55
- newState = useComboboxStateReducer(state, actionAndChanges);
56
- return externalStateReducer
57
- ? externalStateReducer(state, {
58
- ...actionAndChanges,
59
- changes: newState,
60
- })
61
- : newState;
62
- },
63
- ...useComboboxProps,
64
- });
65
-
66
- return (
67
- <MenuRoot
68
- {...{
69
- isListbox: true,
70
- items,
71
- itemToString,
72
- isOpen,
73
- ...useComboboxProps,
74
- ...useComboboxReturnProps,
75
- }}
76
- popoutProps={{
77
- focusOnContent: false,
78
- }}
79
- >
80
- {children}
81
- </MenuRoot>
82
- );
23
+ return <SingleAutocomplete {...rest} />;
83
24
  };
@@ -1,21 +1,10 @@
1
1
  import { useContext } from "react";
2
- import type {
3
- UseComboboxGetInputPropsOptions,
4
- GetInputPropsReturnValue,
5
- } from "downshift";
6
2
  import { Input, type TypeInputProps } from "@sproutsocial/seeds-react-input";
7
- import { MenuContext } from "../MenuContext";
3
+ import type { TypeDownshiftMergedInputProps } from "../types";
4
+ import { useMenuToggleContext } from "../MenuContext";
8
5
 
9
- type TypeInputEventListeners =
10
- | "onKeyDown"
11
- | "onChange"
12
- | "onInput"
13
- | "onBlur"
14
- | "onClick"
15
- | "refKey";
16
6
  export interface TypeAutocompleteInputProps
17
- extends Pick<UseComboboxGetInputPropsOptions, TypeInputEventListeners>,
18
- Omit<Partial<TypeInputProps>, TypeInputEventListeners> {}
7
+ extends TypeDownshiftMergedInputProps<TypeInputProps> {}
19
8
 
20
9
  export const AutocompleteInput = ({
21
10
  onKeyDown,
@@ -26,23 +15,25 @@ export const AutocompleteInput = ({
26
15
  inputProps,
27
16
  ...rest
28
17
  }: TypeAutocompleteInputProps) => {
29
- const { getInputProps, inputValue } = useContext(MenuContext);
18
+ const { getInputComponentProps, inputValue, ref, ariaProps } =
19
+ useMenuToggleContext();
20
+
30
21
  return (
31
22
  <Input
32
23
  id="autocomplete_input"
33
24
  name="autocomplete_input"
34
- inputProps={{
35
- ...getInputProps?.({
36
- // refKey: "innerRef",
37
- onKeyDown,
38
- onChange,
39
- onInput,
40
- onBlur,
41
- onClick,
42
- value: inputValue,
43
- }),
44
- ...inputProps,
45
- }}
25
+ {...getInputComponentProps({
26
+ onKeyDown,
27
+ onChange,
28
+ onInput,
29
+ onBlur,
30
+ onClick,
31
+ value: inputValue,
32
+ ref,
33
+ refKey: "innerRef",
34
+ inputProps,
35
+ ...ariaProps,
36
+ })}
46
37
  autoComplete={false}
47
38
  value={inputValue}
48
39
  {...rest}
@@ -0,0 +1,50 @@
1
+ import React, { useContext } from "react";
2
+ import type { TypeDownshiftMergedInputProps } from "../types";
3
+ import {
4
+ MenuTokenInput,
5
+ type TypeMenuTokenInputProps,
6
+ } from "../MenuTokenInput";
7
+ import { useMenuToggleContext } from "../MenuContext";
8
+
9
+ export interface TypeAutocompleteTokenInputProps
10
+ extends TypeDownshiftMergedInputProps<TypeMenuTokenInputProps> {}
11
+
12
+ export const AutocompleteTokenInput = ({
13
+ onKeyDown,
14
+ onChange,
15
+ onInput,
16
+ onBlur,
17
+ onClick,
18
+ inputProps,
19
+ getTokenProps,
20
+ ...rest
21
+ }: TypeAutocompleteTokenInputProps) => {
22
+ const {
23
+ getInputComponentProps,
24
+ getDropdownProps,
25
+ inputValue,
26
+ isOpen,
27
+ ref,
28
+ ariaProps,
29
+ } = useMenuToggleContext();
30
+
31
+ return (
32
+ <MenuTokenInput
33
+ id="autocomplete_input"
34
+ name="autocomplete_input"
35
+ {...getInputComponentProps({
36
+ ...(getDropdownProps?.({ ref, preventKeyAction: isOpen }) || { ref }),
37
+ onKeyDown,
38
+ onChange,
39
+ onInput,
40
+ onBlur,
41
+ onClick,
42
+ value: inputValue,
43
+ refKey: "innerRef",
44
+ inputProps,
45
+ ...ariaProps,
46
+ })}
47
+ {...rest}
48
+ />
49
+ );
50
+ };
@@ -1,15 +1,31 @@
1
- import type { TypeMenuRootProps, TypeMenuRootOwnProps } from "../MenuRoot";
1
+ import type { TypeMenuRootOwnProps } from "../MenuRoot";
2
2
  import type {
3
3
  TypeDownshiftComboboxProps,
4
- TypeMenuContext,
4
+ TypeDownshiftMultiSelectProps,
5
5
  } from "../MenuContext";
6
+ import type { TypeGetIsItemVisibleFn } from "../hooks/useItemFiltering";
7
+ import type { TypeMenuRootProps } from "../MenuRoot";
8
+ import type { TypeMenuItemProps } from "../MenuItem";
9
+ import type { TypeChildrenOrItems } from "../types";
6
10
 
7
- export interface TypeAutocompleteProps
8
- extends TypeMenuRootOwnProps,
9
- TypeDownshiftComboboxProps {
10
- children: TypeMenuRootProps["children"];
11
- getIsItemVisible?: (
12
- item: TypeMenuContext["items"][number],
13
- inputValue: string
14
- ) => boolean;
15
- }
11
+ export type TypeSingleAutocompleteProps<
12
+ I extends TypeMenuItemProps = TypeMenuItemProps
13
+ > = TypeMenuRootOwnProps &
14
+ TypeChildrenOrItems<I> &
15
+ TypeDownshiftComboboxProps & {
16
+ getIsItemVisible?: TypeGetIsItemVisibleFn;
17
+ };
18
+
19
+ export type TypeMultiAutocompleteProps<
20
+ I extends TypeMenuItemProps = TypeMenuItemProps
21
+ > = TypeSingleAutocompleteProps<I> &
22
+ TypeDownshiftMultiSelectProps & {
23
+ /* Determine whether or not the combobox should display selected items in the dropdown */
24
+ showSelectedItemsInDropdown?: boolean;
25
+ };
26
+
27
+ export type TypeAutocompleteProps<
28
+ I extends TypeMenuItemProps = TypeMenuItemProps
29
+ > =
30
+ | (TypeSingleAutocompleteProps<I> & { multiSelect?: false })
31
+ | (TypeMultiAutocompleteProps<I> & { multiSelect: true });
@@ -0,0 +1,189 @@
1
+ import React from "react";
2
+ import { useCombobox, useMultipleSelection } from "downshift";
3
+ import { useMenuChildren } from "../hooks/useMenuChildren";
4
+ import {
5
+ itemToString as defaultItemToString,
6
+ itemToKey as defaultItemToKey,
7
+ } from "../utils/downshiftDefaults";
8
+ import { reduceReducers } from "../utils/reduceReducers";
9
+ import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
10
+ import {
11
+ useItemFiltering,
12
+ getDefaultGetIsItemVisible,
13
+ defaultGetShouldFilter,
14
+ type TypeGetShouldFilterFn,
15
+ } from "../hooks/useItemFiltering";
16
+ import { getSelectedItemIds } from "../MenuContext";
17
+ import { MenuRoot } from "../MenuRoot";
18
+
19
+ import type { TypeMenuItemProps } from "../MenuItem";
20
+ import type { TypeMenuRootProps } from "../MenuRoot";
21
+ import type { TypeMultiAutocompleteProps } from "./AutocompleteTypes";
22
+
23
+ export const MultiAutocomplete = <
24
+ I extends TypeMenuItemProps = TypeMenuItemProps
25
+ >({
26
+ children: childrenProp,
27
+ stateReducer: externalStateReducer,
28
+ menuItems,
29
+ MenuItemComponent,
30
+ itemToString = defaultItemToString,
31
+ itemToKey = defaultItemToKey,
32
+ getIsItemVisible = getDefaultGetIsItemVisible(itemToString),
33
+ showSelectedItemsInDropdown = false,
34
+ initialSelectedItems = [],
35
+ onStateChange,
36
+ onSelectedItemsChange,
37
+ getA11yMultiStatusMessage,
38
+ onMultiSelectStateChange,
39
+ multiSelectStateReducer,
40
+ ...useComboboxProps
41
+ }: TypeMultiAutocompleteProps<I>) => {
42
+ /** Get a list of menuItems and children */
43
+ const { allMenuItems, children } = useMenuChildren({
44
+ children: childrenProp,
45
+ menuItems,
46
+ MenuItemComponent,
47
+ });
48
+ const [selectedItems, setSelectedItems] =
49
+ React.useState(initialSelectedItems);
50
+ const [selectedItemIds, setSelectedItemIds] = React.useState<
51
+ Record<string, boolean>
52
+ >(getSelectedItemIds(initialSelectedItems));
53
+
54
+ // function that checks that item should be visible based on the selected items
55
+ const getIsItemVisibleMulti = React.useCallback<typeof getIsItemVisible>(
56
+ (item, inputValue) => {
57
+ return (
58
+ (showSelectedItemsInDropdown || !selectedItemIds[item.id]) &&
59
+ getIsItemVisible(item, inputValue)
60
+ );
61
+ },
62
+ [selectedItemIds, showSelectedItemsInDropdown, getIsItemVisible]
63
+ );
64
+
65
+ // function that checks that item should be filtered
66
+ const getShouldFilterMulti = React.useCallback<TypeGetShouldFilterFn>(
67
+ (args) => {
68
+ return (
69
+ (!showSelectedItemsInDropdown &&
70
+ Object.keys(selectedItemIds).length > 0) ||
71
+ defaultGetShouldFilter(args)
72
+ );
73
+ },
74
+ [selectedItemIds, showSelectedItemsInDropdown, getIsItemVisible]
75
+ );
76
+
77
+ const { setHiddenItemsMap, defaultDownshiftProps, ...restDefaults } =
78
+ useDownshiftDefaults({
79
+ isCombobox: true,
80
+ isMulti: true,
81
+ // handles the prop name mapping for multi select
82
+ multiSelectProps: {
83
+ getA11yMultiStatusMessage,
84
+ onMultiSelectStateChange,
85
+ multiSelectStateReducer,
86
+ ...useComboboxProps,
87
+ },
88
+ });
89
+
90
+ // used to handle filtering of items
91
+ const { updateFilteredItems } = useItemFiltering({
92
+ allMenuItems,
93
+ itemToString,
94
+ getIsItemVisible: getIsItemVisibleMulti,
95
+ getShouldFilter: getShouldFilterMulti,
96
+ setHiddenItemsMap,
97
+ });
98
+
99
+ // update the filtered items when the selectedItemIds changes
100
+ React.useEffect(() => {
101
+ // the inputValue should be empty whenever the selected items change
102
+ // if the inputValue is needed here then it will need to be stored on a new state
103
+ updateFilteredItems({ inputValue: "" });
104
+ }, [selectedItemIds]);
105
+
106
+ const useMultipleSelectionReturnProps = useMultipleSelection({
107
+ ...defaultDownshiftProps.multi,
108
+ itemToKey,
109
+ selectedItems,
110
+ onSelectedItemsChange: ({ selectedItems, ...changes }) => {
111
+ setSelectedItems(selectedItems);
112
+ setSelectedItemIds(getSelectedItemIds(selectedItems));
113
+ onSelectedItemsChange &&
114
+ onSelectedItemsChange({ selectedItems, ...changes });
115
+ },
116
+ });
117
+
118
+ /**
119
+ * Destructure Downshift props and call the core useCombobox with items and state handler
120
+ */
121
+ const { isOpen, ...useComboboxReturnProps } = useCombobox<
122
+ (typeof allMenuItems)[number]
123
+ >({
124
+ ...defaultDownshiftProps.combobox,
125
+ items: allMenuItems,
126
+ itemToString,
127
+ itemToKey,
128
+ stateReducer: reduceReducers(
129
+ defaultDownshiftProps.combobox.stateReducer,
130
+ externalStateReducer
131
+ ),
132
+ onStateChange(changes) {
133
+ const { type, selectedItem: newSelectedItem, inputValue } = changes;
134
+ switch (type) {
135
+ // Add the selected item to the selected items list
136
+ case useCombobox.stateChangeTypes.InputKeyDownEnter:
137
+ case useCombobox.stateChangeTypes.ItemClick:
138
+ if (newSelectedItem) {
139
+ const newSelectedItems = [...selectedItems, newSelectedItem];
140
+ setSelectedItems(newSelectedItems);
141
+ setSelectedItemIds({
142
+ ...selectedItemIds,
143
+ [newSelectedItem.id]: true,
144
+ });
145
+ onSelectedItemsChange &&
146
+ onSelectedItemsChange({
147
+ selectedItems: newSelectedItems,
148
+ type: useMultipleSelection.stateChangeTypes
149
+ .FunctionSetSelectedItems,
150
+ });
151
+ }
152
+ break;
153
+ default:
154
+ break;
155
+ }
156
+ // Update the filtered items based on the input value
157
+ if (inputValue !== undefined) updateFilteredItems({ inputValue });
158
+ onStateChange && onStateChange(changes);
159
+ },
160
+ ...useComboboxProps,
161
+ });
162
+
163
+ return (
164
+ <MenuRoot
165
+ {...{
166
+ isListbox: true,
167
+ items: allMenuItems,
168
+ itemToString,
169
+ isOpen,
170
+ initialSelectedItems,
171
+ onStateChange,
172
+ onSelectedItemsChange,
173
+ getA11yMultiStatusMessage,
174
+ onMultiSelectStateChange,
175
+ multiSelectStateReducer,
176
+ setHiddenItemsMap,
177
+ ...useComboboxProps,
178
+ ...useComboboxReturnProps,
179
+ ...useMultipleSelectionReturnProps,
180
+ ...restDefaults,
181
+ }}
182
+ popoutProps={{
183
+ focusOnContent: false,
184
+ }}
185
+ >
186
+ {children}
187
+ </MenuRoot>
188
+ );
189
+ };
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { useCombobox } from "downshift";
3
+ import { useMenuChildren } from "../hooks/useMenuChildren";
4
+ import { itemToString as defaultItemToString } from "../utils/downshiftDefaults";
5
+ import { reduceReducers } from "../utils/reduceReducers";
6
+ import { useDownshiftDefaults } from "../hooks/useDownshiftDefaults";
7
+ import { useItemFiltering } from "../hooks/useItemFiltering";
8
+ import { MenuRoot } from "../MenuRoot";
9
+
10
+ import type { TypeMenuItemProps } from "../MenuItem";
11
+ import type { TypeMenuRootProps } from "../MenuRoot";
12
+ import type { TypeAutocompleteProps } from "./AutocompleteTypes";
13
+
14
+ export const SingleAutocomplete = <
15
+ I extends TypeMenuItemProps = TypeMenuItemProps
16
+ >({
17
+ children: childrenProp,
18
+ stateReducer: externalStateReducer,
19
+ menuItems,
20
+ MenuItemComponent,
21
+ itemToString = defaultItemToString,
22
+ getIsItemVisible,
23
+ ...useComboboxProps
24
+ }: TypeAutocompleteProps<I>) => {
25
+ /** Get a list of menuItems and children */
26
+ const { allMenuItems, children } = useMenuChildren({
27
+ children: childrenProp,
28
+ menuItems,
29
+ MenuItemComponent,
30
+ });
31
+ const { setHiddenItemsMap, defaultDownshiftProps, ...restDefaults } =
32
+ useDownshiftDefaults({ isCombobox: true });
33
+ const { updateFilteredItems } = useItemFiltering({
34
+ allMenuItems,
35
+ itemToString,
36
+ getIsItemVisible,
37
+ setHiddenItemsMap,
38
+ });
39
+
40
+ /**
41
+ * Destructure Downshift props and call the core useCombobox with items and state handler
42
+ */
43
+ const { isOpen, ...useComboboxReturnProps } = useCombobox({
44
+ ...defaultDownshiftProps.combobox,
45
+ items: allMenuItems,
46
+ itemToString,
47
+ onInputValueChange: updateFilteredItems,
48
+ stateReducer: reduceReducers(
49
+ defaultDownshiftProps.combobox.stateReducer,
50
+ externalStateReducer
51
+ ),
52
+ ...useComboboxProps,
53
+ });
54
+
55
+ return (
56
+ <MenuRoot
57
+ {...{
58
+ isListbox: true,
59
+ items: allMenuItems,
60
+ itemToString,
61
+ isOpen,
62
+ setHiddenItemsMap,
63
+ ...useComboboxProps,
64
+ ...useComboboxReturnProps,
65
+ ...restDefaults,
66
+ }}
67
+ popoutProps={{
68
+ focusOnContent: false,
69
+ }}
70
+ >
71
+ {children}
72
+ </MenuRoot>
73
+ );
74
+ };