@patternfly/react-core 5.4.0-prerelease.7 → 5.4.0-prerelease.9

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 (187) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +0 -2
  3. package/components/package.json +1 -1
  4. package/deprecated/package.json +1 -1
  5. package/dist/dynamic/components/AboutModal/package.json +1 -1
  6. package/dist/dynamic/components/Accordion/package.json +1 -1
  7. package/dist/dynamic/components/ActionList/package.json +1 -1
  8. package/dist/dynamic/components/Alert/package.json +1 -1
  9. package/dist/dynamic/components/Avatar/package.json +1 -1
  10. package/dist/dynamic/components/BackToTop/package.json +1 -1
  11. package/dist/dynamic/components/Backdrop/package.json +1 -1
  12. package/dist/dynamic/components/BackgroundImage/package.json +1 -1
  13. package/dist/dynamic/components/Badge/package.json +1 -1
  14. package/dist/dynamic/components/Banner/package.json +1 -1
  15. package/dist/dynamic/components/Brand/package.json +1 -1
  16. package/dist/dynamic/components/Breadcrumb/package.json +1 -1
  17. package/dist/dynamic/components/Button/package.json +1 -1
  18. package/dist/dynamic/components/CalendarMonth/package.json +1 -1
  19. package/dist/dynamic/components/Card/package.json +1 -1
  20. package/dist/dynamic/components/Checkbox/package.json +1 -1
  21. package/dist/dynamic/components/Chip/package.json +1 -1
  22. package/dist/dynamic/components/ClipboardCopy/package.json +1 -1
  23. package/dist/dynamic/components/CodeBlock/package.json +1 -1
  24. package/dist/dynamic/components/DataList/package.json +1 -1
  25. package/dist/dynamic/components/DatePicker/package.json +1 -1
  26. package/dist/dynamic/components/DescriptionList/package.json +1 -1
  27. package/dist/dynamic/components/Divider/package.json +1 -1
  28. package/dist/dynamic/components/DragDrop/package.json +1 -1
  29. package/dist/dynamic/components/Drawer/package.json +1 -1
  30. package/dist/dynamic/components/Dropdown/package.json +1 -1
  31. package/dist/dynamic/components/DualListSelector/package.json +1 -1
  32. package/dist/dynamic/components/EmptyState/package.json +1 -1
  33. package/dist/dynamic/components/ExpandableSection/package.json +1 -1
  34. package/dist/dynamic/components/FileUpload/package.json +1 -1
  35. package/dist/dynamic/components/Form/package.json +1 -1
  36. package/dist/dynamic/components/FormSelect/package.json +1 -1
  37. package/dist/dynamic/components/HelperText/package.json +1 -1
  38. package/dist/dynamic/components/Hint/package.json +1 -1
  39. package/dist/dynamic/components/Icon/package.json +1 -1
  40. package/dist/dynamic/components/InputGroup/package.json +1 -1
  41. package/dist/dynamic/components/JumpLinks/package.json +1 -1
  42. package/dist/dynamic/components/Label/package.json +1 -1
  43. package/dist/dynamic/components/List/package.json +1 -1
  44. package/dist/dynamic/components/LoginPage/package.json +1 -1
  45. package/dist/dynamic/components/Masthead/package.json +1 -1
  46. package/dist/dynamic/components/Menu/package.json +1 -1
  47. package/dist/dynamic/components/MenuToggle/package.json +1 -1
  48. package/dist/dynamic/components/Modal/package.json +1 -1
  49. package/dist/dynamic/components/MultipleFileUpload/package.json +1 -1
  50. package/dist/dynamic/components/Nav/package.json +1 -1
  51. package/dist/dynamic/components/NotificationBadge/package.json +1 -1
  52. package/dist/dynamic/components/NotificationDrawer/package.json +1 -1
  53. package/dist/dynamic/components/NumberInput/package.json +1 -1
  54. package/dist/dynamic/components/OverflowMenu/package.json +1 -1
  55. package/dist/dynamic/components/Page/package.json +1 -1
  56. package/dist/dynamic/components/Pagination/package.json +1 -1
  57. package/dist/dynamic/components/Panel/package.json +1 -1
  58. package/dist/dynamic/components/Popover/package.json +1 -1
  59. package/dist/dynamic/components/Progress/package.json +1 -1
  60. package/dist/dynamic/components/ProgressStepper/package.json +1 -1
  61. package/dist/dynamic/components/Radio/package.json +1 -1
  62. package/dist/dynamic/components/SearchInput/package.json +1 -1
  63. package/dist/dynamic/components/Select/package.json +1 -1
  64. package/dist/dynamic/components/Sidebar/package.json +1 -1
  65. package/dist/dynamic/components/SimpleList/package.json +1 -1
  66. package/dist/dynamic/components/Skeleton/package.json +1 -1
  67. package/dist/dynamic/components/SkipToContent/package.json +1 -1
  68. package/dist/dynamic/components/Slider/package.json +1 -1
  69. package/dist/dynamic/components/Spinner/package.json +1 -1
  70. package/dist/dynamic/components/Switch/package.json +1 -1
  71. package/dist/dynamic/components/Tabs/package.json +1 -1
  72. package/dist/dynamic/components/Text/package.json +1 -1
  73. package/dist/dynamic/components/TextArea/package.json +1 -1
  74. package/dist/dynamic/components/TextInput/package.json +1 -1
  75. package/dist/dynamic/components/TextInputGroup/package.json +1 -1
  76. package/dist/dynamic/components/Tile/package.json +1 -1
  77. package/dist/dynamic/components/TimePicker/package.json +1 -1
  78. package/dist/dynamic/components/Timestamp/package.json +1 -1
  79. package/dist/dynamic/components/Title/package.json +1 -1
  80. package/dist/dynamic/components/ToggleGroup/package.json +1 -1
  81. package/dist/dynamic/components/Toolbar/package.json +1 -1
  82. package/dist/dynamic/components/Tooltip/package.json +1 -1
  83. package/dist/dynamic/components/TreeView/package.json +1 -1
  84. package/dist/dynamic/components/Truncate/package.json +1 -1
  85. package/dist/dynamic/components/Wizard/hooks/package.json +1 -1
  86. package/dist/dynamic/components/Wizard/package.json +1 -1
  87. package/dist/dynamic/deprecated/components/ApplicationLauncher/package.json +1 -1
  88. package/dist/dynamic/deprecated/components/ContextSelector/package.json +1 -1
  89. package/dist/dynamic/deprecated/components/Dropdown/package.json +1 -1
  90. package/dist/dynamic/deprecated/components/OptionsMenu/package.json +1 -1
  91. package/dist/dynamic/deprecated/components/PageHeader/package.json +1 -1
  92. package/dist/dynamic/deprecated/components/Select/package.json +1 -1
  93. package/dist/dynamic/deprecated/components/Wizard/package.json +1 -1
  94. package/dist/dynamic/deprecated/components/package.json +1 -1
  95. package/dist/dynamic/helpers/FocusTrap/FocusTrap/package.json +1 -1
  96. package/dist/dynamic/helpers/GenerateId/GenerateId/package.json +1 -1
  97. package/dist/dynamic/helpers/KeyboardHandler/package.json +1 -1
  98. package/dist/dynamic/helpers/OUIA/ouia/package.json +1 -1
  99. package/dist/dynamic/helpers/Popper/Popper/package.json +1 -1
  100. package/dist/dynamic/helpers/constants/package.json +1 -1
  101. package/dist/dynamic/helpers/datetimeUtils/package.json +1 -1
  102. package/dist/dynamic/helpers/favorites/package.json +1 -1
  103. package/dist/dynamic/helpers/fileUtils/package.json +1 -1
  104. package/dist/dynamic/helpers/htmlConstants/package.json +1 -1
  105. package/dist/dynamic/helpers/package.json +1 -1
  106. package/dist/dynamic/helpers/resizeObserver/package.json +1 -1
  107. package/dist/dynamic/helpers/typeUtils/package.json +1 -1
  108. package/dist/dynamic/helpers/useInterval/package.json +1 -1
  109. package/dist/dynamic/helpers/useIsomorphicLayout/package.json +1 -1
  110. package/dist/dynamic/helpers/useUnmountEffect/package.json +1 -1
  111. package/dist/dynamic/helpers/util/package.json +1 -1
  112. package/dist/dynamic/layouts/Bullseye/package.json +1 -1
  113. package/dist/dynamic/layouts/Flex/package.json +1 -1
  114. package/dist/dynamic/layouts/Gallery/package.json +1 -1
  115. package/dist/dynamic/layouts/Grid/package.json +1 -1
  116. package/dist/dynamic/layouts/Level/package.json +1 -1
  117. package/dist/dynamic/layouts/Split/package.json +1 -1
  118. package/dist/dynamic/layouts/Stack/package.json +1 -1
  119. package/dist/dynamic/next/components/DualListSelector/package.json +1 -1
  120. package/dist/dynamic/next/components/Modal/package.json +1 -1
  121. package/dist/dynamic/next/components/package.json +1 -1
  122. package/dist/dynamic/styles/package.json +1 -1
  123. package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
  124. package/dist/esm/components/MenuToggle/MenuToggle.js +1 -1
  125. package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
  126. package/dist/esm/components/Pagination/Pagination.d.ts.map +1 -1
  127. package/dist/esm/components/Pagination/Pagination.js +2 -1
  128. package/dist/esm/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/esm/components/Pagination/PaginationOptionsMenu.d.ts +1 -0
  130. package/dist/esm/components/Pagination/PaginationOptionsMenu.d.ts.map +1 -1
  131. package/dist/esm/components/Pagination/PaginationOptionsMenu.js +2 -4
  132. package/dist/esm/components/Pagination/PaginationOptionsMenu.js.map +1 -1
  133. package/dist/esm/components/Select/Select.d.ts +2 -0
  134. package/dist/esm/components/Select/Select.d.ts.map +1 -1
  135. package/dist/esm/components/Select/Select.js +3 -3
  136. package/dist/esm/components/Select/Select.js.map +1 -1
  137. package/dist/esm/components/Tabs/Tabs.d.ts +4 -0
  138. package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
  139. package/dist/esm/components/Tabs/Tabs.js +17 -0
  140. package/dist/esm/components/Tabs/Tabs.js.map +1 -1
  141. package/dist/esm/components/Truncate/Truncate.d.ts.map +1 -1
  142. package/dist/esm/components/Truncate/Truncate.js +54 -12
  143. package/dist/esm/components/Truncate/Truncate.js.map +1 -1
  144. package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
  145. package/dist/js/components/MenuToggle/MenuToggle.js +1 -1
  146. package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
  147. package/dist/js/components/Pagination/Pagination.d.ts.map +1 -1
  148. package/dist/js/components/Pagination/Pagination.js +2 -1
  149. package/dist/js/components/Pagination/Pagination.js.map +1 -1
  150. package/dist/js/components/Pagination/PaginationOptionsMenu.d.ts +1 -0
  151. package/dist/js/components/Pagination/PaginationOptionsMenu.d.ts.map +1 -1
  152. package/dist/js/components/Pagination/PaginationOptionsMenu.js +2 -4
  153. package/dist/js/components/Pagination/PaginationOptionsMenu.js.map +1 -1
  154. package/dist/js/components/Select/Select.d.ts +2 -0
  155. package/dist/js/components/Select/Select.d.ts.map +1 -1
  156. package/dist/js/components/Select/Select.js +3 -3
  157. package/dist/js/components/Select/Select.js.map +1 -1
  158. package/dist/js/components/Tabs/Tabs.d.ts +4 -0
  159. package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
  160. package/dist/js/components/Tabs/Tabs.js +17 -0
  161. package/dist/js/components/Tabs/Tabs.js.map +1 -1
  162. package/dist/js/components/Truncate/Truncate.d.ts.map +1 -1
  163. package/dist/js/components/Truncate/Truncate.js +54 -12
  164. package/dist/js/components/Truncate/Truncate.js.map +1 -1
  165. package/dist/umd/react-core.min.js +2 -2
  166. package/helpers/package.json +1 -1
  167. package/layouts/package.json +1 -1
  168. package/next/package.json +1 -1
  169. package/package.json +6 -6
  170. package/src/components/CodeBlock/examples/CodeBlock.md +6 -0
  171. package/src/components/CodeBlock/examples/CodeBlockBasic.tsx +1 -1
  172. package/src/components/CodeBlock/examples/CodeBlockExpandable.tsx +3 -3
  173. package/src/components/MenuToggle/MenuToggle.tsx +1 -0
  174. package/src/components/Pagination/Pagination.tsx +2 -0
  175. package/src/components/Pagination/PaginationOptionsMenu.tsx +13 -14
  176. package/src/components/Pagination/__tests__/Generated/__snapshots__/PaginationOptionsMenu.test.tsx.snap +36 -38
  177. package/src/components/Pagination/__tests__/__snapshots__/Pagination.test.tsx.snap +714 -754
  178. package/src/components/Select/Select.tsx +5 -2
  179. package/src/components/Select/examples/SelectMultiTypeahead.tsx +111 -66
  180. package/src/components/Select/examples/SelectMultiTypeaheadCheckbox.tsx +103 -60
  181. package/src/components/Select/examples/SelectMultiTypeaheadCreatable.tsx +125 -79
  182. package/src/components/Select/examples/SelectTypeahead.tsx +113 -69
  183. package/src/components/Select/examples/SelectTypeaheadCreatable.tsx +115 -75
  184. package/src/components/Tabs/Tabs.tsx +21 -0
  185. package/src/components/ToggleGroup/examples/ToggleGroup.md +33 -8
  186. package/src/components/Truncate/Truncate.tsx +73 -7
  187. package/src/components/Truncate/__tests__/Truncate.test.tsx +6 -0
@@ -30,10 +30,12 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
30
30
  const [selected, setSelected] = React.useState<string[]>([]);
31
31
  const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(initialSelectOptions);
32
32
  const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
33
- const [activeItem, setActiveItem] = React.useState<string | null>(null);
33
+ const [activeItemId, setActiveItemId] = React.useState<string | null>(null);
34
34
  const [onCreation, setOnCreation] = React.useState<boolean>(false); // Boolean to refresh filter state after new option is created
35
35
  const textInputRef = React.useRef<HTMLInputElement>();
36
36
 
37
+ const CREATE_NEW = 'create';
38
+
37
39
  React.useEffect(() => {
38
40
  let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
39
41
 
@@ -43,9 +45,9 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
43
45
  String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())
44
46
  );
45
47
 
46
- // When no options are found after filtering, display creation option
47
- if (!newSelectOptions.length) {
48
- newSelectOptions = [{ isDisabled: false, children: `Create new option "${inputValue}"`, value: 'create' }];
48
+ // If no option matches the filter exactly, display creation option
49
+ if (!initialSelectOptions.some((option) => option.value === inputValue)) {
50
+ newSelectOptions = [...newSelectOptions, { children: `Create new option "${inputValue}"`, value: CREATE_NEW }];
49
51
  }
50
52
 
51
53
  // Open the menu when the input value changes and the new value is not empty
@@ -55,56 +57,125 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
55
57
  }
56
58
 
57
59
  setSelectOptions(newSelectOptions);
58
- setFocusedItemIndex(null);
59
- setActiveItem(null);
60
60
  }, [inputValue, onCreation]);
61
61
 
62
+ const createItemId = (value: any) => `select-multi-create-typeahead-${value.replace(' ', '-')}`;
63
+
64
+ const setActiveAndFocusedItem = (itemIndex: number) => {
65
+ setFocusedItemIndex(itemIndex);
66
+ const focusedItem = selectOptions[itemIndex];
67
+ setActiveItemId(createItemId(focusedItem.value));
68
+ };
69
+
70
+ const resetActiveAndFocusedItem = () => {
71
+ setFocusedItemIndex(null);
72
+ setActiveItemId(null);
73
+ };
74
+
75
+ const closeMenu = () => {
76
+ setIsOpen(false);
77
+ resetActiveAndFocusedItem();
78
+ };
79
+
80
+ const onInputClick = () => {
81
+ if (!isOpen) {
82
+ setIsOpen(true);
83
+ } else if (!inputValue) {
84
+ closeMenu();
85
+ }
86
+ };
87
+
88
+ const onSelect = (value: string) => {
89
+ if (value) {
90
+ if (value === CREATE_NEW) {
91
+ if (!initialSelectOptions.some((item) => item.value === inputValue)) {
92
+ initialSelectOptions = [...initialSelectOptions, { value: inputValue, children: inputValue }];
93
+ }
94
+ setSelected(
95
+ selected.includes(inputValue)
96
+ ? selected.filter((selection) => selection !== inputValue)
97
+ : [...selected, inputValue]
98
+ );
99
+ setOnCreation(!onCreation);
100
+ resetActiveAndFocusedItem();
101
+ } else {
102
+ // eslint-disable-next-line no-console
103
+ console.log('selected', value);
104
+ setSelected(
105
+ selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
106
+ );
107
+ }
108
+ }
109
+
110
+ textInputRef.current?.focus();
111
+ };
112
+
113
+ const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
114
+ setInputValue(value);
115
+ resetActiveAndFocusedItem();
116
+ };
117
+
62
118
  const handleMenuArrowKeys = (key: string) => {
63
- let indexToFocus;
119
+ let indexToFocus = 0;
120
+
121
+ if (!isOpen) {
122
+ setIsOpen(true);
123
+ }
64
124
 
65
- if (isOpen) {
66
- if (key === 'ArrowUp') {
67
- // When no index is set or at the first index, focus to the last, otherwise decrement focus index
68
- if (focusedItemIndex === null || focusedItemIndex === 0) {
125
+ if (selectOptions.every((option) => option.isDisabled)) {
126
+ return;
127
+ }
128
+
129
+ if (key === 'ArrowUp') {
130
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
131
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
132
+ indexToFocus = selectOptions.length - 1;
133
+ } else {
134
+ indexToFocus = focusedItemIndex - 1;
135
+ }
136
+
137
+ // Skip disabled options
138
+ while (selectOptions[indexToFocus].isDisabled) {
139
+ indexToFocus--;
140
+ if (indexToFocus === -1) {
69
141
  indexToFocus = selectOptions.length - 1;
70
- } else {
71
- indexToFocus = focusedItemIndex - 1;
72
142
  }
73
143
  }
144
+ }
74
145
 
75
- if (key === 'ArrowDown') {
76
- // When no index is set or at the last index, focus to the first, otherwise increment focus index
77
- if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
146
+ if (key === 'ArrowDown') {
147
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
148
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
149
+ indexToFocus = 0;
150
+ } else {
151
+ indexToFocus = focusedItemIndex + 1;
152
+ }
153
+
154
+ // Skip disabled options
155
+ while (selectOptions[indexToFocus].isDisabled) {
156
+ indexToFocus++;
157
+ if (indexToFocus === selectOptions.length) {
78
158
  indexToFocus = 0;
79
- } else {
80
- indexToFocus = focusedItemIndex + 1;
81
159
  }
82
160
  }
83
-
84
- setFocusedItemIndex(indexToFocus);
85
- const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
86
- setActiveItem(`select-multi-create-typeahead-${focusedItem.value.replace(' ', '-')}`);
87
161
  }
162
+
163
+ setActiveAndFocusedItem(indexToFocus);
88
164
  };
89
165
 
90
166
  const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
91
- const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled);
92
- const [firstMenuItem] = enabledMenuItems;
93
- const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
167
+ const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null;
94
168
 
95
169
  switch (event.key) {
96
- // Select the first available option
97
170
  case 'Enter':
98
- if (!isOpen) {
99
- setIsOpen((prevIsOpen) => !prevIsOpen);
100
- } else if (isOpen && focusedItem.value !== 'no results') {
171
+ if (isOpen && focusedItem && !focusedItem.isAriaDisabled) {
101
172
  onSelect(focusedItem.value as string);
102
173
  }
103
- break;
104
- case 'Tab':
105
- case 'Escape':
106
- setIsOpen(false);
107
- setActiveItem(null);
174
+
175
+ if (!isOpen) {
176
+ setIsOpen(true);
177
+ }
178
+
108
179
  break;
109
180
  case 'ArrowUp':
110
181
  case 'ArrowDown':
@@ -116,35 +187,17 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
116
187
 
117
188
  const onToggleClick = () => {
118
189
  setIsOpen(!isOpen);
190
+ textInputRef?.current?.focus();
119
191
  };
120
192
 
121
- const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
122
- setInputValue(value);
193
+ const onClearButtonClick = () => {
194
+ setSelected([]);
195
+ setInputValue('');
196
+ resetActiveAndFocusedItem();
197
+ textInputRef?.current?.focus();
123
198
  };
124
199
 
125
- const onSelect = (value: string) => {
126
- if (value) {
127
- if (value === 'create') {
128
- if (!initialSelectOptions.some((item) => item.value === inputValue)) {
129
- initialSelectOptions = [...initialSelectOptions, { value: inputValue, children: inputValue }];
130
- }
131
- setSelected(
132
- selected.includes(inputValue)
133
- ? selected.filter((selection) => selection !== inputValue)
134
- : [...selected, inputValue]
135
- );
136
- setOnCreation(!onCreation);
137
- } else {
138
- // eslint-disable-next-line no-console
139
- console.log('selected', value);
140
- setSelected(
141
- selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value]
142
- );
143
- }
144
- }
145
-
146
- textInputRef.current?.focus();
147
- };
200
+ const getChildren = (value: string) => initialSelectOptions.find((option) => option.value === value)?.children;
148
201
 
149
202
  const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
150
203
  <MenuToggle
@@ -158,14 +211,14 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
158
211
  <TextInputGroup isPlain>
159
212
  <TextInputGroupMain
160
213
  value={inputValue}
161
- onClick={onToggleClick}
214
+ onClick={onInputClick}
162
215
  onChange={onTextInputChange}
163
216
  onKeyDown={onInputKeyDown}
164
217
  id="multi-create-typeahead-select-input"
165
218
  autoComplete="off"
166
219
  innerRef={textInputRef}
167
220
  placeholder="Select a state"
168
- {...(activeItem && { 'aria-activedescendant': activeItem })}
221
+ {...(activeItemId && { 'aria-activedescendant': activeItemId })}
169
222
  role="combobox"
170
223
  isExpanded={isOpen}
171
224
  aria-controls="select-multi-create-typeahead-listbox"
@@ -179,25 +232,15 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
179
232
  onSelect(selection);
180
233
  }}
181
234
  >
182
- {selection}
235
+ {getChildren(selection)}
183
236
  </Chip>
184
237
  ))}
185
238
  </ChipGroup>
186
239
  </TextInputGroupMain>
187
- <TextInputGroupUtilities>
188
- {selected.length > 0 && (
189
- <Button
190
- variant="plain"
191
- onClick={() => {
192
- setInputValue('');
193
- setSelected([]);
194
- textInputRef?.current?.focus();
195
- }}
196
- aria-label="Clear input value"
197
- >
198
- <TimesIcon aria-hidden />
199
- </Button>
200
- )}
240
+ <TextInputGroupUtilities {...(selected.length === 0 ? { style: { display: 'none' } } : {})}>
241
+ <Button variant="plain" onClick={onClearButtonClick} aria-label="Clear input value">
242
+ <TimesIcon aria-hidden />
243
+ </Button>
201
244
  </TextInputGroupUtilities>
202
245
  </TextInputGroup>
203
246
  </MenuToggle>
@@ -208,9 +251,12 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
208
251
  id="multi-create-typeahead-select"
209
252
  isOpen={isOpen}
210
253
  selected={selected}
211
- onSelect={(ev, selection) => onSelect(selection as string)}
212
- onOpenChange={() => setIsOpen(false)}
254
+ onSelect={(_event, selection) => onSelect(selection as string)}
255
+ onOpenChange={(isOpen) => {
256
+ !isOpen && closeMenu();
257
+ }}
213
258
  toggle={toggle}
259
+ shouldFocusFirstItemOnOpen={false}
214
260
  >
215
261
  <SelectList isAriaMultiselectable id="select-multi-create-typeahead-listbox">
216
262
  {selectOptions.map((option, index) => (
@@ -218,7 +264,7 @@ export const SelectMultiTypeaheadCreatable: React.FunctionComponent = () => {
218
264
  key={option.value || option.children}
219
265
  isFocused={focusedItemIndex === index}
220
266
  className={option.className}
221
- id={`select-multi-create-typeahead-${option.value.replace(' ', '-')}`}
267
+ id={createItemId(option.value)}
222
268
  {...option}
223
269
  ref={null}
224
270
  />
@@ -22,16 +22,18 @@ const initialSelectOptions: SelectOptionProps[] = [
22
22
  { value: 'North Carolina', children: 'North Carolina' }
23
23
  ];
24
24
 
25
- export const SelectBasic: React.FunctionComponent = () => {
25
+ export const SelectTypeahead: React.FunctionComponent = () => {
26
26
  const [isOpen, setIsOpen] = React.useState(false);
27
27
  const [selected, setSelected] = React.useState<string>('');
28
28
  const [inputValue, setInputValue] = React.useState<string>('');
29
29
  const [filterValue, setFilterValue] = React.useState<string>('');
30
30
  const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(initialSelectOptions);
31
31
  const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
32
- const [activeItem, setActiveItem] = React.useState<string | null>(null);
32
+ const [activeItemId, setActiveItemId] = React.useState<string | null>(null);
33
33
  const textInputRef = React.useRef<HTMLInputElement>();
34
34
 
35
+ const NO_RESULTS = 'no results';
36
+
35
37
  React.useEffect(() => {
36
38
  let newSelectOptions: SelectOptionProps[] = initialSelectOptions;
37
39
 
@@ -44,7 +46,7 @@ export const SelectBasic: React.FunctionComponent = () => {
44
46
  // When no options are found after filtering, display 'No results found'
45
47
  if (!newSelectOptions.length) {
46
48
  newSelectOptions = [
47
- { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' }
49
+ { isAriaDisabled: true, children: `No results found for "${filterValue}"`, value: NO_RESULTS }
48
50
  ];
49
51
  }
50
52
 
@@ -55,84 +57,124 @@ export const SelectBasic: React.FunctionComponent = () => {
55
57
  }
56
58
 
57
59
  setSelectOptions(newSelectOptions);
58
- setActiveItem(null);
59
- setFocusedItemIndex(null);
60
60
  }, [filterValue]);
61
61
 
62
- const onToggleClick = () => {
63
- setIsOpen(!isOpen);
62
+ const createItemId = (value: any) => `select-typeahead-${value.replace(' ', '-')}`;
63
+
64
+ const setActiveAndFocusedItem = (itemIndex: number) => {
65
+ setFocusedItemIndex(itemIndex);
66
+ const focusedItem = selectOptions[itemIndex];
67
+ setActiveItemId(createItemId(focusedItem.value));
64
68
  };
65
69
 
66
- const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
70
+ const resetActiveAndFocusedItem = () => {
71
+ setFocusedItemIndex(null);
72
+ setActiveItemId(null);
73
+ };
74
+
75
+ const closeMenu = () => {
76
+ setIsOpen(false);
77
+ resetActiveAndFocusedItem();
78
+ };
79
+
80
+ const onInputClick = () => {
81
+ if (!isOpen) {
82
+ setIsOpen(true);
83
+ } else if (!inputValue) {
84
+ closeMenu();
85
+ }
86
+ };
87
+
88
+ const selectOption = (value: string | number, content: string | number) => {
67
89
  // eslint-disable-next-line no-console
68
- console.log('selected', value);
90
+ console.log('selected', content);
91
+
92
+ setInputValue(String(content));
93
+ setFilterValue('');
94
+ setSelected(String(value));
69
95
 
70
- if (value && value !== 'no results') {
71
- setInputValue(value as string);
72
- setFilterValue('');
73
- setSelected(value as string);
96
+ closeMenu();
97
+ };
98
+
99
+ const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
100
+ if (value && value !== NO_RESULTS) {
101
+ const optionText = selectOptions.find((option) => option.value === value)?.children;
102
+ selectOption(value, optionText as string);
74
103
  }
75
- setIsOpen(false);
76
- setFocusedItemIndex(null);
77
- setActiveItem(null);
78
104
  };
79
105
 
80
106
  const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
81
107
  setInputValue(value);
82
108
  setFilterValue(value);
109
+
110
+ resetActiveAndFocusedItem();
111
+
112
+ if (value !== selected) {
113
+ setSelected('');
114
+ }
83
115
  };
84
116
 
85
117
  const handleMenuArrowKeys = (key: string) => {
86
- let indexToFocus;
118
+ let indexToFocus = 0;
119
+
120
+ if (!isOpen) {
121
+ setIsOpen(true);
122
+ }
123
+
124
+ if (selectOptions.every((option) => option.isDisabled)) {
125
+ return;
126
+ }
87
127
 
88
- if (isOpen) {
89
- if (key === 'ArrowUp') {
90
- // When no index is set or at the first index, focus to the last, otherwise decrement focus index
91
- if (focusedItemIndex === null || focusedItemIndex === 0) {
128
+ if (key === 'ArrowUp') {
129
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
130
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
131
+ indexToFocus = selectOptions.length - 1;
132
+ } else {
133
+ indexToFocus = focusedItemIndex - 1;
134
+ }
135
+
136
+ // Skip disabled options
137
+ while (selectOptions[indexToFocus].isDisabled) {
138
+ indexToFocus--;
139
+ if (indexToFocus === -1) {
92
140
  indexToFocus = selectOptions.length - 1;
93
- } else {
94
- indexToFocus = focusedItemIndex - 1;
95
141
  }
96
142
  }
143
+ }
144
+
145
+ if (key === 'ArrowDown') {
146
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
147
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
148
+ indexToFocus = 0;
149
+ } else {
150
+ indexToFocus = focusedItemIndex + 1;
151
+ }
97
152
 
98
- if (key === 'ArrowDown') {
99
- // When no index is set or at the last index, focus to the first, otherwise increment focus index
100
- if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
153
+ // Skip disabled options
154
+ while (selectOptions[indexToFocus].isDisabled) {
155
+ indexToFocus++;
156
+ if (indexToFocus === selectOptions.length) {
101
157
  indexToFocus = 0;
102
- } else {
103
- indexToFocus = focusedItemIndex + 1;
104
158
  }
105
159
  }
106
-
107
- setFocusedItemIndex(indexToFocus);
108
- const focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus];
109
- setActiveItem(`select-typeahead-${focusedItem.value.replace(' ', '-')}`);
110
160
  }
161
+
162
+ setActiveAndFocusedItem(indexToFocus);
111
163
  };
112
164
 
113
165
  const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
114
- const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
115
- const [firstMenuItem] = enabledMenuItems;
116
- const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
166
+ const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null;
117
167
 
118
168
  switch (event.key) {
119
- // Select the first available option
120
169
  case 'Enter':
121
- if (isOpen && focusedItem.value !== 'no results') {
122
- setInputValue(String(focusedItem.children));
123
- setFilterValue('');
124
- setSelected(String(focusedItem.children));
170
+ if (isOpen && focusedItem && focusedItem.value !== NO_RESULTS && !focusedItem.isAriaDisabled) {
171
+ selectOption(focusedItem.value, focusedItem.children as string);
125
172
  }
126
173
 
127
- setIsOpen((prevIsOpen) => !prevIsOpen);
128
- setFocusedItemIndex(null);
129
- setActiveItem(null);
174
+ if (!isOpen) {
175
+ setIsOpen(true);
176
+ }
130
177
 
131
- break;
132
- case 'Tab':
133
- case 'Escape':
134
- setIsOpen(false);
135
- setActiveItem(null);
136
178
  break;
137
179
  case 'ArrowUp':
138
180
  case 'ArrowDown':
@@ -142,6 +184,19 @@ export const SelectBasic: React.FunctionComponent = () => {
142
184
  }
143
185
  };
144
186
 
187
+ const onToggleClick = () => {
188
+ setIsOpen(!isOpen);
189
+ textInputRef?.current?.focus();
190
+ };
191
+
192
+ const onClearButtonClick = () => {
193
+ setSelected('');
194
+ setInputValue('');
195
+ setFilterValue('');
196
+ resetActiveAndFocusedItem();
197
+ textInputRef?.current?.focus();
198
+ };
199
+
145
200
  const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
146
201
  <MenuToggle
147
202
  ref={toggleRef}
@@ -154,34 +209,23 @@ export const SelectBasic: React.FunctionComponent = () => {
154
209
  <TextInputGroup isPlain>
155
210
  <TextInputGroupMain
156
211
  value={inputValue}
157
- onClick={onToggleClick}
212
+ onClick={onInputClick}
158
213
  onChange={onTextInputChange}
159
214
  onKeyDown={onInputKeyDown}
160
215
  id="typeahead-select-input"
161
216
  autoComplete="off"
162
217
  innerRef={textInputRef}
163
218
  placeholder="Select a state"
164
- {...(activeItem && { 'aria-activedescendant': activeItem })}
219
+ {...(activeItemId && { 'aria-activedescendant': activeItemId })}
165
220
  role="combobox"
166
221
  isExpanded={isOpen}
167
222
  aria-controls="select-typeahead-listbox"
168
223
  />
169
224
 
170
- <TextInputGroupUtilities>
171
- {!!inputValue && (
172
- <Button
173
- variant="plain"
174
- onClick={() => {
175
- setSelected('');
176
- setInputValue('');
177
- setFilterValue('');
178
- textInputRef?.current?.focus();
179
- }}
180
- aria-label="Clear input value"
181
- >
182
- <TimesIcon aria-hidden />
183
- </Button>
184
- )}
225
+ <TextInputGroupUtilities {...(!inputValue ? { style: { display: 'none' } } : {})}>
226
+ <Button variant="plain" onClick={onClearButtonClick} aria-label="Clear input value">
227
+ <TimesIcon aria-hidden />
228
+ </Button>
185
229
  </TextInputGroupUtilities>
186
230
  </TextInputGroup>
187
231
  </MenuToggle>
@@ -193,10 +237,11 @@ export const SelectBasic: React.FunctionComponent = () => {
193
237
  isOpen={isOpen}
194
238
  selected={selected}
195
239
  onSelect={onSelect}
196
- onOpenChange={() => {
197
- setIsOpen(false);
240
+ onOpenChange={(isOpen) => {
241
+ !isOpen && closeMenu();
198
242
  }}
199
243
  toggle={toggle}
244
+ shouldFocusFirstItemOnOpen={false}
200
245
  >
201
246
  <SelectList id="select-typeahead-listbox">
202
247
  {selectOptions.map((option, index) => (
@@ -204,8 +249,7 @@ export const SelectBasic: React.FunctionComponent = () => {
204
249
  key={option.value || option.children}
205
250
  isFocused={focusedItemIndex === index}
206
251
  className={option.className}
207
- onClick={() => setSelected(option.value)}
208
- id={`select-typeahead-${option.value.replace(' ', '-')}`}
252
+ id={createItemId(option.value)}
209
253
  {...option}
210
254
  ref={null}
211
255
  />