@patternfly/react-core 4.246.0 → 4.247.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 (27) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/esm/components/DualListSelector/DualListSelectorPane.d.ts +2 -0
  3. package/dist/esm/components/DualListSelector/DualListSelectorPane.d.ts.map +1 -1
  4. package/dist/esm/components/DualListSelector/DualListSelectorPane.js +4 -5
  5. package/dist/esm/components/DualListSelector/DualListSelectorPane.js.map +1 -1
  6. package/dist/esm/components/SearchInput/SearchInput.d.ts +2 -0
  7. package/dist/esm/components/SearchInput/SearchInput.d.ts.map +1 -1
  8. package/dist/esm/components/SearchInput/SearchInput.js +2 -2
  9. package/dist/esm/components/SearchInput/SearchInput.js.map +1 -1
  10. package/dist/js/components/DualListSelector/DualListSelectorPane.d.ts +2 -0
  11. package/dist/js/components/DualListSelector/DualListSelectorPane.d.ts.map +1 -1
  12. package/dist/js/components/DualListSelector/DualListSelectorPane.js +4 -5
  13. package/dist/js/components/DualListSelector/DualListSelectorPane.js.map +1 -1
  14. package/dist/js/components/SearchInput/SearchInput.d.ts +2 -0
  15. package/dist/js/components/SearchInput/SearchInput.d.ts.map +1 -1
  16. package/dist/js/components/SearchInput/SearchInput.js +2 -2
  17. package/dist/js/components/SearchInput/SearchInput.js.map +1 -1
  18. package/dist/umd/react-core.min.css +3370 -3370
  19. package/dist/umd/react-core.min.js +2 -2
  20. package/package.json +2 -2
  21. package/src/components/Dropdown/examples/Dropdown.md +79 -55
  22. package/src/components/DualListSelector/DualListSelectorPane.tsx +11 -7
  23. package/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap +70 -10
  24. package/src/components/SearchInput/SearchInput.tsx +15 -0
  25. package/src/demos/ComposableMenu/ComposableMenu.md +7 -2
  26. package/src/demos/ComposableMenu/examples/ComposableMultipleTypeaheadSelect.tsx +220 -0
  27. package/src/components/Dropdown/examples/DropdownSplitButtonIndeterminate.tsx +0 -78
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.246.0",
3
+ "version": "4.247.0",
4
4
  "description": "This library provides a set of common React components for use with the PatternFly reference implementation.",
5
5
  "main": "dist/js/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -70,5 +70,5 @@
70
70
  "react": "^16.8 || ^17 || ^18",
71
71
  "react-dom": "^16.8 || ^17 || ^18"
72
72
  },
73
- "gitHead": "3ae4fb2182c899e44a032ed468cb067ef496ad66"
73
+ "gitHead": "064d341a881a7ab291add565ed1929cf915ab049"
74
74
  }
@@ -18,133 +18,157 @@ import avatarImg from '../../Avatar/examples/avatarImg.svg';
18
18
 
19
19
  ## Examples
20
20
 
21
- ### Basic
21
+ ### Basic dropdowns
22
+
23
+ Basic dropdowns present users with a menu of items upon clicking a dropdown toggle. The `isOpen` property is used to determine whether the menu is opened or not.
24
+
25
+ All of the items in a dropdown list are created as `<DropdownItem>` components. The following example shows a few different states of dropdown items. As shown in this example, dropdown items may:
26
+ - Use the `tooltip` property to enable tooltip support.
27
+ - Use the `isDisabled` property to disable an item.
28
+ - Use the `isAriaDisabled` property to style an item as disabled, while still allowing the item to receive keyboard focus and display a tooltip.
29
+
30
+ This example also uses a `<DropdownSeparator>`, which adds a horizontal line that splits the menu into 2 sections.
22
31
 
23
32
  ```ts file='./DropdownBasic.tsx'
24
33
  ```
25
34
 
26
- ### With initial selection
35
+ ### Autofocus on dropdown expansion
36
+
37
+ A `<DropdownItem>` may use the `autoFocus` property so that it automatically receives focus when the menu is expanded. `autoFocus` cannot be applied to disabled dropdown items.
27
38
 
28
39
  ```ts file="./DropdownInitialSelection.tsx"
29
40
  ```
30
41
 
31
- ### With groups
42
+ ### With groups of items
43
+
44
+ Use the `<DropdownGroup>` component to group sets of related dropdown items together. Use the `isGrouped` property on the `Dropdown` component to indicate that a dropdown contains grouped items.
32
45
 
33
46
  ```ts file="./DropdownGroups.tsx"
34
47
  ```
35
48
 
36
- ### Disabled
49
+ ### With item descriptions
37
50
 
38
- ```ts file="./DropdownDisabled.tsx"
51
+ A `<DropdownItem>` may use the `description` property to provide users with more context. This description will appear in the menu below the dropdown item it corresponds to.
52
+
53
+ ```ts file="./DropdownDescriptions.tsx"
39
54
  ```
40
55
 
41
- ### Primary toggle
56
+ ### With custom content
42
57
 
43
- ```ts file="./DropdownPrimaryToggle.tsx"
58
+ You may replace standard dropdown items with custom content. If so, you must style the menu contents and manage focus within your custom implementation.
59
+
60
+ ```ts file="./DropdownBasicPanel.tsx"
44
61
  ```
45
62
 
46
- ### Secondary toggle
63
+ ### With router link items
47
64
 
48
- ```ts file="./DropdownSecondaryToggle.tsx"
65
+ `<DropdownItem>` may use the `component` property to include router links.
66
+
67
+ ```ts file="./DropdownRouterLink.tsx"
49
68
  ```
50
69
 
51
- ### Plain with text toggle
70
+ ### Toggle styling
52
71
 
53
- ```ts file="./DropdownPlainTextToggle.tsx"
72
+ You can adjust the styling of a `<DropdownToggle>` using the `toggleVariant` property. `toggleVariant` may be set to 'primary', 'secondary', or 'default'.
73
+
74
+ ```ts file="./DropdownPrimaryToggle.tsx"
54
75
  ```
55
76
 
56
- ### Position right
77
+ ### Plain text toggle
57
78
 
58
- ```ts file="./DropdownPositionRight.tsx"
79
+ A toggle may be styled as plain text using the `isText` and `isPlain`properties. The following example applies these properties to a `<Dropdown>` component.
80
+
81
+ ```ts file="./DropdownPlainTextToggle.tsx"
59
82
  ```
60
83
 
61
- ### Alignments on different breakpoints
84
+ ### Disabled toggles
62
85
 
63
- ```ts file="./DropdownAlignmentOnBreakpoints.tsx"
86
+ A `<DropdownToggle>` may use the `isDisabled` property to disable the expansion of the dropdown menu.
87
+
88
+ ```ts file="./DropdownDisabled.tsx"
64
89
  ```
65
90
 
66
- ### Direction up
91
+ ### Icon toggle
67
92
 
68
- ```ts file="./DropdownDirectionUp.tsx"
93
+ `<DropdownToggle>` may use a recognizable icon instead of a text label if text would take up too much room. Set the `toggleIndicator` property to `{null}` to remove the default dropdown toggle caret.
94
+
95
+ ```ts file="./DropdownIconOnly.tsx"
69
96
  ```
70
97
 
71
- ### With kebab
98
+ ### Kebab toggle
99
+
100
+ Use `<KebabToggle>` to replace the default text label and caret with a kebab icon. A kebab toggle can be used to indicate that a dropdown connects to a menu.
72
101
 
73
102
  ```ts file="./DropdownKebab.tsx"
74
103
  ```
75
104
 
76
- ### With badge
105
+ ### Toggle with image
77
106
 
78
- ```ts file="./DropdownBadge.tsx"
107
+ The `icon` property can be used to display an image in the `DropdownToggle` and can be used alongside a text label.
108
+
109
+ ```ts file="./DropdownImageAndText.tsx"
79
110
  ```
80
111
 
81
- ### Icon only
112
+ ### Toggle with badge
82
113
 
83
- ```ts file="./DropdownIconOnly.tsx"
114
+ You may use a `<BadgeToggle>` to display the number of dropdown menu items.
115
+
116
+ ```ts file="./DropdownBadge.tsx"
84
117
  ```
85
118
 
86
119
  ### Split button
87
120
 
88
- ```ts file="./DropdownSplitButton.tsx"
89
- ```
121
+ To combine an action button or other control with a dropdown menu, use a split button. A `<DropdownToggle>` can be rendered as a split button by using the `splitButtonItems` property. Elements to be displayed before the toggle button must be included in `splitButtonItems`.
90
122
 
91
- ### Split button (with text)
123
+ The following example shows a split button with a `<DropdownToggleCheckbox>`, which is primarily used for bulk select operations on large data sets.
92
124
 
93
- ```ts file="./DropdownSplitButtonText.tsx"
125
+ ```ts file="./DropdownSplitButton.tsx"
94
126
  ```
95
127
 
96
- ### Split button (indeterminate state)
97
-
98
- ```ts file="./DropdownSplitButtonIndeterminate.tsx"
99
- ```
128
+ ### Split button text label
100
129
 
101
- ### Split button (disabled)
130
+ Text labels may optionally be used alongside actions within split buttons.
102
131
 
103
- ```ts file="./DropdownSplitButtonDisabled.tsx"
132
+ ```ts file="./DropdownSplitButtonText.tsx"
104
133
  ```
105
134
 
106
- ### Split button action
135
+ ### Split button toggle variants
107
136
 
108
- ```ts file="./DropdownSplitButtonAction.tsx"
109
- ```
137
+ Use the `toggleVariant` and `splitButtonVariant` properties to adjust the styling of split button toggles.
110
138
 
111
- ### Split button primary action
139
+ The following example uses `toggleVariant="primary"` to apply primary visual styling and `splitButtonVariant="action"` to include an action.
112
140
 
113
141
  ```ts file="./DropdownSplitButtonActionPrimary.tsx"
114
142
  ```
115
143
 
116
- ### Split button secondary action
144
+ ### Dropdown alignment
117
145
 
118
- ```ts file="./DropdownSplitButtonActionSecondary.tsx"
119
- ```
146
+ The alignment of a dropdown menu may be changed to better fit the size of a user's browser window. A `<Dropdown>` may use the `alignments` property to indicate how the menu will align at different screen sizes.
120
147
 
121
- ### Basic panel
122
-
123
- ```ts file="./DropdownBasicPanel.tsx"
148
+ ```ts file="./DropdownAlignmentOnBreakpoints.tsx"
124
149
  ```
125
150
 
126
- ### Router link
151
+ ### Right-positioned menu
127
152
 
128
- ```ts file="./DropdownRouterLink.tsx"
153
+ The following example demonstrates a `<Dropdown>` with a `position` equal to 'right'. By default, a dropdown menu is left-positioned, but a right-positioned menu may work better depending on where your dropdown is placed on a page.
154
+
155
+ ```ts file="./DropdownPositionRight.tsx"
129
156
  ```
130
157
 
131
- ### Dropdown with image and text
158
+ ### Upwards directional menu
132
159
 
133
- ```ts file="./DropdownImageAndText.tsx"
160
+ The following example demonstrates a `<Dropdown>` with a `direction` equal to 'up'. This will alter the default menu behavior so that the dropdown menu is displayed above the dropdown toggle.
161
+
162
+ ```ts file="./DropdownDirectionUp.tsx"
134
163
  ```
135
164
 
136
- ### Appending document body vs parent
165
+ ### Using document parent element
137
166
 
138
- Avoid passing in `document.body` when passing a value to the `menuAppendTo` prop on the Dropdown component, as it can cause accessibility issues. These issues can include, but are not limited to, being unable to enter the contents of the Dropdown options via assistive technologies (like keyboards or screen readers).
167
+ Avoid passing in `document.body` to a `<Dropdown>` `menuAppendTo` property. Doing so can cause accessibility issues, such as being unable to navigate `<Dropdown>` options using keyboards or screen readers.
139
168
 
140
- Instead append to `"parent"` to achieve the same result without sacrificing accessibility.
169
+ Instead, append to ‘parent’ to avoid sacrificing accessibility.
141
170
 
142
- In this example, while, after making a selection, both variants retain focus on their respective Dropdown component, the options for the `document.body` variant cannot be navigated to via Voice Over.
171
+ The following example demonstrates both methods of appending. After making a selection, both dropdowns retain focus on their respective `<Dropdown>`, but the options for the `document.body` variant cannot be navigated via Voice Over.
143
172
 
144
173
  ```ts file="./DropdownAppendBodyVsParent.tsx"
145
174
  ```
146
-
147
- ### Dropdown with descriptions
148
-
149
- ```ts file="./DropdownDescriptions.tsx"
150
- ```
@@ -1,12 +1,12 @@
1
1
  import * as React from 'react';
2
2
  import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector';
3
3
  import { css } from '@patternfly/react-styles';
4
- import formStyles from '@patternfly/react-styles/css/components/FormControl/form-control';
5
4
  import { DualListSelectorTree, DualListSelectorTreeItemData } from './DualListSelectorTree';
6
5
  import { getUniqueId } from '../../helpers';
7
6
  import { DualListSelectorListWrapper } from './DualListSelectorListWrapper';
8
7
  import { DualListSelectorContext, DualListSelectorPaneContext } from './DualListSelectorContext';
9
8
  import { DualListSelectorList } from './DualListSelectorList';
9
+ import { SearchInput } from '../SearchInput';
10
10
 
11
11
  /** Acts as the container for a list of options that are either available or chosen,
12
12
  * depending on the pane type (available or chosen). A search input and other actions,
@@ -57,6 +57,8 @@ export interface DualListSelectorPaneProps {
57
57
  onSearch?: (event: React.ChangeEvent<HTMLInputElement>) => void;
58
58
  /** @hide A callback for when the search input value for changes. To be used when isSearchable is true. */
59
59
  onSearchInputChanged?: (value: string, event: React.FormEvent<HTMLInputElement>) => void;
60
+ /** @hide Callback for search input clear button */
61
+ onSearchInputClear?: (event: React.SyntheticEvent<HTMLButtonElement>) => void;
60
62
  /** @hide Filter function for custom filtering based on search string. To be used when isSearchable is true. */
61
63
  filterOption?: (option: React.ReactNode, input: string) => boolean;
62
64
  /** @hide Accessible label for the search input. To be used when isSearchable is true. */
@@ -81,6 +83,7 @@ export const DualListSelectorPane: React.FunctionComponent<DualListSelectorPaneP
81
83
  searchInputAriaLabel = '',
82
84
  onFilterUpdate,
83
85
  onSearchInputChanged,
86
+ onSearchInputClear,
84
87
  filterOption,
85
88
  id = getUniqueId('dual-list-selector-pane'),
86
89
  isDisabled = false,
@@ -90,8 +93,7 @@ export const DualListSelectorPane: React.FunctionComponent<DualListSelectorPaneP
90
93
  const { isTree } = React.useContext(DualListSelectorContext);
91
94
 
92
95
  // only called when search input is dynamically built
93
- const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
94
- const newValue = e.target.value;
96
+ const onChange = (newValue: string, e: React.FormEvent<HTMLInputElement>) => {
95
97
  let filtered: React.ReactNode[];
96
98
  if (isTree) {
97
99
  filtered = options
@@ -160,12 +162,14 @@ export const DualListSelectorPane: React.FunctionComponent<DualListSelectorPaneP
160
162
  {searchInput ? (
161
163
  searchInput
162
164
  ) : (
163
- <input
164
- className={css(formStyles.formControl, formStyles.modifiers.search)}
165
- type="search"
165
+ <SearchInput
166
166
  onChange={isDisabled ? undefined : onChange}
167
+ onClear={
168
+ onSearchInputClear ? onSearchInputClear : e => onChange('', e as React.FormEvent<HTMLInputElement>)
169
+ }
170
+ isDisabled={isDisabled}
167
171
  aria-label={searchInputAriaLabel}
168
- disabled={isDisabled}
172
+ type="search"
169
173
  />
170
174
  )}
171
175
  </div>
@@ -1135,11 +1135,41 @@ exports[`DualListSelector with search inputs 1`] = `
1135
1135
  <div
1136
1136
  class="pf-c-dual-list-selector__tools-filter"
1137
1137
  >
1138
- <input
1139
- aria-label="Available search input"
1140
- class="pf-c-form-control pf-m-search"
1141
- type="search"
1142
- />
1138
+ <div
1139
+ class="pf-c-text-input-group"
1140
+ >
1141
+ <div
1142
+ class="pf-c-text-input-group__main pf-m-icon"
1143
+ >
1144
+ <span
1145
+ class="pf-c-text-input-group__text"
1146
+ >
1147
+ <span
1148
+ class="pf-c-text-input-group__icon"
1149
+ >
1150
+ <svg
1151
+ aria-hidden="true"
1152
+ fill="currentColor"
1153
+ height="1em"
1154
+ role="img"
1155
+ style="vertical-align: -0.125em;"
1156
+ viewBox="0 0 512 512"
1157
+ width="1em"
1158
+ >
1159
+ <path
1160
+ d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
1161
+ />
1162
+ </svg>
1163
+ </span>
1164
+ <input
1165
+ aria-label="Available search input"
1166
+ class="pf-c-text-input-group__text-input"
1167
+ type="search"
1168
+ value=""
1169
+ />
1170
+ </span>
1171
+ </div>
1172
+ </div>
1143
1173
  </div>
1144
1174
  </div>
1145
1175
  <div
@@ -1360,11 +1390,41 @@ exports[`DualListSelector with search inputs 1`] = `
1360
1390
  <div
1361
1391
  class="pf-c-dual-list-selector__tools-filter"
1362
1392
  >
1363
- <input
1364
- aria-label="Chosen search input"
1365
- class="pf-c-form-control pf-m-search"
1366
- type="search"
1367
- />
1393
+ <div
1394
+ class="pf-c-text-input-group"
1395
+ >
1396
+ <div
1397
+ class="pf-c-text-input-group__main pf-m-icon"
1398
+ >
1399
+ <span
1400
+ class="pf-c-text-input-group__text"
1401
+ >
1402
+ <span
1403
+ class="pf-c-text-input-group__icon"
1404
+ >
1405
+ <svg
1406
+ aria-hidden="true"
1407
+ fill="currentColor"
1408
+ height="1em"
1409
+ role="img"
1410
+ style="vertical-align: -0.125em;"
1411
+ viewBox="0 0 512 512"
1412
+ width="1em"
1413
+ >
1414
+ <path
1415
+ d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"
1416
+ />
1417
+ </svg>
1418
+ </span>
1419
+ <input
1420
+ aria-label="Chosen search input"
1421
+ class="pf-c-text-input-group__text-input"
1422
+ type="search"
1423
+ value=""
1424
+ />
1425
+ </span>
1426
+ </div>
1427
+ </div>
1368
1428
  </div>
1369
1429
  </div>
1370
1430
  <div
@@ -70,6 +70,19 @@ export interface SearchInputProps extends Omit<React.HTMLProps<HTMLDivElement>,
70
70
  hasWordsAttrLabel?: React.ReactNode;
71
71
  /** A suggestion for autocompleting. */
72
72
  hint?: string;
73
+ /** Type of the input */
74
+ type?:
75
+ | 'text'
76
+ | 'date'
77
+ | 'datetime-local'
78
+ | 'email'
79
+ | 'month'
80
+ | 'number'
81
+ | 'password'
82
+ | 'search'
83
+ | 'tel'
84
+ | 'time'
85
+ | 'url';
73
86
  /** @hide A reference object to attach to the input box. */
74
87
  innerRef?: React.RefObject<any>;
75
88
  /** A flag for controlling the open state of a custom advanced search implementation. */
@@ -147,6 +160,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
147
160
  isDisabled = false,
148
161
  appendTo,
149
162
  removeFindDomNode = false,
163
+ type = 'text',
150
164
  ...props
151
165
  }: SearchInputProps) => {
152
166
  const [isSearchMenuOpen, setIsSearchMenuOpen] = React.useState(false);
@@ -263,6 +277,7 @@ const SearchInputBase: React.FunctionComponent<SearchInputProps> = ({
263
277
  aria-label={ariaLabel}
264
278
  onKeyDown={onEnter}
265
279
  onChange={onChangeHandler}
280
+ type={type}
266
281
  />
267
282
  {renderUtilities && (
268
283
  <TextInputGroupUtilities>
@@ -19,8 +19,8 @@ import ThIcon from '@patternfly/react-icons/dist/esm/icons/th-icon';
19
19
  import pfIcon from './examples/pf-logo-small.svg';
20
20
  import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';
21
21
  import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
22
- import avatarImg from './examples/avatarImg.svg';
23
22
  import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
23
+ import avatarImg from './examples/avatarImg.svg';
24
24
  import { css } from '@patternfly/react-styles';
25
25
  import styles from '@patternfly/react-styles/css/components/Menu/menu';
26
26
 
@@ -50,7 +50,12 @@ Composable menus currently require consumer keyboard handling and use of our und
50
50
 
51
51
  ### Composable typeahead select
52
52
 
53
- ```ts file="./examples/ComposableTypeaheadSelect.tsx"
53
+ ```ts isBeta file="./examples/ComposableTypeaheadSelect.tsx"
54
+ ```
55
+
56
+ ### Composable multiple typeahead select
57
+
58
+ ```ts isBeta file="./examples/ComposableMultipleTypeaheadSelect.tsx"
54
59
  ```
55
60
 
56
61
  ### Composable drilldown menu
@@ -0,0 +1,220 @@
1
+ import React from 'react';
2
+ import {
3
+ Menu,
4
+ MenuContent,
5
+ MenuList,
6
+ MenuItem,
7
+ MenuItemProps,
8
+ MenuToggle,
9
+ MenuToggleElement,
10
+ Popper,
11
+ TextInputGroup,
12
+ TextInputGroupMain,
13
+ TextInputGroupUtilities,
14
+ ChipGroup,
15
+ Chip,
16
+ Button
17
+ } from '@patternfly/react-core';
18
+ import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
19
+
20
+ const intitalMenuItems = [
21
+ { itemId: 'Option 1', children: 'Option 1' },
22
+ { itemId: 'Option 2', children: 'Option 2' },
23
+ { itemId: 'Option 3', children: 'Option 3' }
24
+ ];
25
+
26
+ export const ComposableMultipleTypeaheadSelect: React.FunctionComponent = () => {
27
+ const [isMenuOpen, setIsMenuOpen] = React.useState(false);
28
+ const [inputValue, setInputValue] = React.useState<string>('');
29
+ const [menuItems, setMenuItems] = React.useState<MenuItemProps[]>(intitalMenuItems);
30
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
31
+ const [selected, setSelected] = React.useState<string[]>([]);
32
+ const containerRef = React.useRef<HTMLDivElement>();
33
+ const menuToggleRef = React.useRef<MenuToggleElement>({} as MenuToggleElement);
34
+ const textInputRef = React.useRef<HTMLInputElement>();
35
+ const menuRef = React.useRef<HTMLDivElement>();
36
+
37
+ React.useEffect(() => {
38
+ let newMenuItems: MenuItemProps[] = intitalMenuItems;
39
+
40
+ // Filter menu items based on the text input value when one exists
41
+ if (inputValue) {
42
+ newMenuItems = intitalMenuItems.filter(menuItem =>
43
+ String(menuItem.children)
44
+ .toLowerCase()
45
+ .includes(inputValue.toLowerCase())
46
+ );
47
+
48
+ // When no options are found after filtering, display 'No results found'
49
+ if (!newMenuItems.length) {
50
+ newMenuItems = [{ isDisabled: true, children: 'No results found' }];
51
+ }
52
+ }
53
+
54
+ setMenuItems(newMenuItems);
55
+ }, [inputValue]);
56
+
57
+ const handleMenuArrowKeys = (key: string) => {
58
+ let indexToFocus;
59
+
60
+ if (isMenuOpen) {
61
+ if (key === 'ArrowUp') {
62
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
63
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
64
+ indexToFocus = menuItems.length - 1;
65
+ } else {
66
+ indexToFocus = focusedItemIndex - 1;
67
+ }
68
+ }
69
+
70
+ if (key === 'ArrowDown') {
71
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
72
+ if (focusedItemIndex === null || focusedItemIndex === menuItems.length - 1) {
73
+ indexToFocus = 0;
74
+ } else {
75
+ indexToFocus = focusedItemIndex + 1;
76
+ }
77
+ }
78
+
79
+ setFocusedItemIndex(indexToFocus);
80
+ }
81
+ };
82
+
83
+ const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
84
+ const enabledMenuItems = menuItems.filter(menuItem => !menuItem.isDisabled);
85
+ const [firstMenuItem] = enabledMenuItems;
86
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
87
+
88
+ switch (event.key) {
89
+ // Select the first available option
90
+ case 'Enter':
91
+ if (!isMenuOpen) {
92
+ setIsMenuOpen(prevIsOpen => !prevIsOpen);
93
+ } else {
94
+ onMenuSelect(focusedItem.itemId as string);
95
+ }
96
+ break;
97
+ case 'Tab':
98
+ case 'Escape':
99
+ setIsMenuOpen(false);
100
+ break;
101
+ case 'ArrowUp':
102
+ case 'ArrowDown':
103
+ handleMenuArrowKeys(event.key);
104
+ break;
105
+ default:
106
+ !isMenuOpen && setIsMenuOpen(true);
107
+ }
108
+ };
109
+
110
+ // Close the menu when a click occurs outside of the menu toggle content
111
+ const onDocumentClick = (event: MouseEvent) => {
112
+ if (
113
+ isMenuOpen &&
114
+ !menuToggleRef.current?.contains(event?.target as HTMLElement) &&
115
+ !menuRef?.current?.contains(event.target as Node)
116
+ ) {
117
+ setIsMenuOpen(false);
118
+ }
119
+ };
120
+
121
+ const toggleMenuOpen = () => {
122
+ setIsMenuOpen(!isMenuOpen);
123
+ textInputRef.current?.focus();
124
+ };
125
+
126
+ const onTextInputChange = (value: string) => {
127
+ setInputValue(value);
128
+ };
129
+
130
+ const onMenuSelect = (itemId: string) => {
131
+ if (itemId) {
132
+ setSelected(
133
+ selected.includes(itemId) ? selected.filter(selection => selection !== itemId) : [...selected, itemId]
134
+ );
135
+ }
136
+ };
137
+
138
+ const toggle = (
139
+ <MenuToggle
140
+ variant="typeahead"
141
+ onClick={toggleMenuOpen}
142
+ innerRef={menuToggleRef}
143
+ isExpanded={isMenuOpen}
144
+ isFullWidth
145
+ >
146
+ <TextInputGroup isPlain>
147
+ <TextInputGroupMain
148
+ value={inputValue}
149
+ onClick={toggleMenuOpen}
150
+ onChange={onTextInputChange}
151
+ onKeyDown={onInputKeyDown}
152
+ id="typeahead-select-input"
153
+ autoComplete="off"
154
+ innerRef={textInputRef}
155
+ >
156
+ <ChipGroup>
157
+ {selected.map((selection, index) => (
158
+ <Chip
159
+ key={index}
160
+ onClick={ev => {
161
+ ev.stopPropagation();
162
+ onMenuSelect(selection);
163
+ }}
164
+ >
165
+ {selection}
166
+ </Chip>
167
+ ))}
168
+ </ChipGroup>
169
+ </TextInputGroupMain>
170
+ <TextInputGroupUtilities>
171
+ {selected.length > 0 && (
172
+ <Button
173
+ variant="plain"
174
+ onClick={() => {
175
+ setInputValue('');
176
+ setSelected([]);
177
+ }}
178
+ aria-label="Clear input value"
179
+ >
180
+ <TimesIcon aria-hidden />
181
+ </Button>
182
+ )}
183
+ </TextInputGroupUtilities>
184
+ </TextInputGroup>
185
+ </MenuToggle>
186
+ );
187
+ const menu = (
188
+ <Menu
189
+ ref={menuRef as React.Ref<HTMLDivElement>}
190
+ id="multiple-typeahead-select-menu"
191
+ onSelect={(_ev, itemId) => onMenuSelect(itemId?.toString() as string)}
192
+ selected={selected}
193
+ >
194
+ <MenuContent>
195
+ <MenuList>
196
+ {menuItems.map((itemProps, index) => (
197
+ <MenuItem
198
+ key={itemProps.itemId || itemProps.children}
199
+ isFocused={focusedItemIndex === index}
200
+ className={itemProps.className}
201
+ {...itemProps}
202
+ ref={null}
203
+ />
204
+ ))}
205
+ </MenuList>
206
+ </MenuContent>
207
+ </Menu>
208
+ );
209
+ return (
210
+ <div ref={containerRef as React.Ref<HTMLDivElement>}>
211
+ <Popper
212
+ trigger={toggle}
213
+ popper={menu}
214
+ appendTo={containerRef.current}
215
+ isVisible={isMenuOpen}
216
+ onDocumentClick={onDocumentClick}
217
+ />
218
+ </div>
219
+ );
220
+ };