UNPKG

56 kBJavaScriptView Raw
1import { __rest } from "tslib";
2import * as React from 'react';
3import styles from '@patternfly/react-styles/css/components/Select/select';
4import badgeStyles from '@patternfly/react-styles/css/components/Badge/badge';
5import formStyles from '@patternfly/react-styles/css/components/FormControl/form-control';
6import buttonStyles from '@patternfly/react-styles/css/components/Button/button';
7import { css } from '@patternfly/react-styles';
8import TimesCircleIcon from '@patternfly/react-icons/dist/esm/icons/times-circle-icon';
9import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon';
10import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon';
11import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
12import { SelectMenu } from './SelectMenu';
13import { SelectOption } from './SelectOption';
14import { SelectGroup } from './SelectGroup';
15import { SelectToggle } from './SelectToggle';
16import { SelectContext, SelectVariant, SelectPosition, SelectDirection, SelectFooterTabbableItems } from './selectConstants';
17import { ChipGroup } from '../ChipGroup';
18import { Chip } from '../Chip';
19import { Spinner } from '../Spinner';
20import { keyHandler, getNextIndex, getOUIAProps, getDefaultOUIAId, GenerateId } from '../../helpers';
21import { KeyTypes } from '../../helpers/constants';
22import { Divider } from '../Divider';
23import { Popper } from '../../helpers/Popper/Popper';
24import { createRenderableFavorites, extendItemsWithFavorite } from '../../helpers/favorites';
25import { ValidatedOptions } from '../../helpers/constants';
26import { findTabbableElements } from '../../helpers/util';
27// seed for the aria-labelledby ID
28let currentId = 0;
29export class Select extends React.Component {
30 constructor() {
31 super(...arguments);
32 this.parentRef = React.createRef();
33 this.menuComponentRef = React.createRef();
34 this.filterRef = React.createRef();
35 this.clearRef = React.createRef();
36 this.inputRef = React.createRef();
37 this.refCollection = [[]];
38 this.optionContainerRefCollection = [];
39 this.footerRef = React.createRef();
40 this.state = {
41 focusFirstOption: false,
42 typeaheadInputValue: null,
43 typeaheadFilteredChildren: React.Children.toArray(this.props.children),
44 favoritesGroup: [],
45 typeaheadCurrIndex: -1,
46 typeaheadStoredIndex: -1,
47 creatableValue: '',
48 tabbedIntoFavoritesMenu: false,
49 ouiaStateId: getDefaultOUIAId(Select.displayName, this.props.variant),
50 viewMoreNextIndex: -1
51 };
52 this.getTypeaheadActiveChild = (typeaheadCurrIndex) => this.refCollection[typeaheadCurrIndex] ? this.refCollection[typeaheadCurrIndex][0] : null;
53 this.componentDidUpdate = (prevProps, prevState) => {
54 if (this.props.hasInlineFilter) {
55 this.refCollection[0][0] = this.filterRef.current;
56 }
57 // Move focus to top of the menu if state.focusFirstOption was updated to true and the menu does not have custom content
58 if (!prevState.focusFirstOption && this.state.focusFirstOption && !this.props.customContent) {
59 const firstRef = this.refCollection.find(ref => ref !== null);
60 if (firstRef && firstRef[0]) {
61 firstRef[0].focus();
62 }
63 }
64 else if (
65 // if viewMoreNextIndex is not -1, view more was clicked, set focus on first newly loaded item
66 this.state.viewMoreNextIndex !== -1 &&
67 this.refCollection.length > this.state.viewMoreNextIndex &&
68 this.props.loadingVariant !== 'spinner' &&
69 this.refCollection[this.state.viewMoreNextIndex][0] &&
70 this.props.variant !== 'typeahead' && // do not hard focus newly added items for typeahead variants
71 this.props.variant !== 'typeaheadmulti') {
72 this.refCollection[this.state.viewMoreNextIndex][0].focus();
73 }
74 const hasUpdatedChildren = prevProps.children.length !== this.props.children.length ||
75 prevProps.children.some((prevChild, index) => {
76 const prevChildProps = prevChild.props;
77 const currChild = this.props.children[index];
78 const { props: currChildProps } = currChild;
79 if (prevChildProps && currChildProps) {
80 return (prevChildProps.value !== currChildProps.value ||
81 prevChildProps.label !== currChildProps.label ||
82 prevChildProps.isDisabled !== currChildProps.isDisabled ||
83 prevChildProps.isPlaceholder !== currChildProps.isPlaceholder);
84 }
85 else {
86 return prevChild !== currChild;
87 }
88 });
89 if (hasUpdatedChildren) {
90 this.updateTypeAheadFilteredChildren(prevState.typeaheadInputValue || '', null);
91 }
92 // for menus with favorites,
93 // if the number of favorites or typeahead filtered children has changed, the generated
94 // list of favorites needs to be updated
95 if (this.props.onFavorite &&
96 (this.props.favorites.length !== prevProps.favorites.length ||
97 this.state.typeaheadFilteredChildren !== prevState.typeaheadFilteredChildren)) {
98 const tempRenderableChildren = this.props.variant === 'typeahead' || this.props.variant === 'typeaheadmulti'
99 ? this.state.typeaheadFilteredChildren
100 : this.props.children;
101 const renderableFavorites = createRenderableFavorites(tempRenderableChildren, this.props.isGrouped, this.props.favorites);
102 const favoritesGroup = renderableFavorites.length
103 ? [
104 React.createElement(SelectGroup, { key: "favorites", label: this.props.favoritesLabel }, renderableFavorites),
105 React.createElement(Divider, { key: "favorites-group-divider" })
106 ]
107 : [];
108 this.setState({ favoritesGroup });
109 }
110 };
111 this.onEnter = () => {
112 this.setState({ focusFirstOption: true });
113 };
114 this.onToggle = (isExpanded, e) => {
115 const { isInputValuePersisted, onSelect, onToggle, hasInlineFilter } = this.props;
116 if (!isExpanded && isInputValuePersisted && onSelect) {
117 onSelect(undefined, this.inputRef.current ? this.inputRef.current.value : '');
118 }
119 if (isExpanded && hasInlineFilter) {
120 this.setState({
121 focusFirstOption: true
122 });
123 }
124 onToggle(isExpanded, e);
125 };
126 this.onClose = () => {
127 const { isInputFilterPersisted } = this.props;
128 this.setState(Object.assign(Object.assign({ focusFirstOption: false, typeaheadInputValue: null }, (!isInputFilterPersisted && {
129 typeaheadFilteredChildren: React.Children.toArray(this.props.children)
130 })), { typeaheadCurrIndex: -1, tabbedIntoFavoritesMenu: false, viewMoreNextIndex: -1 }));
131 };
132 this.onChange = (e) => {
133 if (e.target.value.toString() !== '' && !this.props.isOpen) {
134 this.onToggle(true, e);
135 }
136 if (this.props.onTypeaheadInputChanged) {
137 this.props.onTypeaheadInputChanged(e.target.value.toString());
138 }
139 this.setState({
140 typeaheadCurrIndex: -1,
141 typeaheadInputValue: e.target.value,
142 creatableValue: e.target.value
143 });
144 this.updateTypeAheadFilteredChildren(e.target.value.toString(), e);
145 this.refCollection = [[]];
146 };
147 this.updateTypeAheadFilteredChildren = (typeaheadInputValue, e) => {
148 let typeaheadFilteredChildren;
149 const { onFilter, isCreatable, onCreateOption, createText, noResultsFoundText, children, isGrouped, isCreateSelectOptionObject, loadingVariant } = this.props;
150 if (onFilter) {
151 /* The updateTypeAheadFilteredChildren callback is not only called on input changes but also when the children change.
152 * In this case the e is null but we can get the typeaheadInputValue from the state.
153 */
154 typeaheadFilteredChildren = onFilter(e, e ? e.target.value : typeaheadInputValue) || children;
155 }
156 else {
157 let input;
158 try {
159 input = new RegExp(typeaheadInputValue.toString(), 'i');
160 }
161 catch (err) {
162 input = new RegExp(typeaheadInputValue.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
163 }
164 const childrenArray = React.Children.toArray(children);
165 if (isGrouped) {
166 const childFilter = (child) => child.props.value &&
167 child.props.value.toString &&
168 this.getDisplay(child.props.value.toString(), 'text').search(input) === 0;
169 typeaheadFilteredChildren =
170 typeaheadInputValue.toString() !== ''
171 ? React.Children.map(children, group => {
172 if (React.isValidElement(group) && group.type === SelectGroup) {
173 const filteredGroupChildren = React.Children.toArray(group.props.children).filter(childFilter);
174 if (filteredGroupChildren.length > 0) {
175 return React.cloneElement(group, {
176 titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
177 children: filteredGroupChildren
178 });
179 }
180 }
181 else {
182 return React.Children.toArray(group).filter(childFilter);
183 }
184 })
185 : childrenArray;
186 }
187 else {
188 typeaheadFilteredChildren =
189 typeaheadInputValue.toString() !== ''
190 ? childrenArray.filter(child => {
191 const valueToCheck = child.props.value;
192 // Dividers don't have value and should not be filtered
193 if (!valueToCheck) {
194 return true;
195 }
196 const isSelectOptionObject = typeof valueToCheck !== 'string' &&
197 valueToCheck.toString &&
198 valueToCheck.compareTo;
199 // View more option should be returned as not a match
200 if (loadingVariant !== 'spinner' && (loadingVariant === null || loadingVariant === void 0 ? void 0 : loadingVariant.text) === valueToCheck) {
201 return true;
202 }
203 // spinner should be returned as not a match
204 if (loadingVariant === 'spinner' && valueToCheck === 'loading') {
205 return true;
206 }
207 if (isSelectOptionObject) {
208 return valueToCheck.compareTo(typeaheadInputValue);
209 }
210 else {
211 return this.getDisplay(child.props.value.toString(), 'text').search(input) === 0;
212 }
213 })
214 : childrenArray;
215 }
216 }
217 if (!typeaheadFilteredChildren) {
218 typeaheadFilteredChildren = [];
219 }
220 if (typeaheadFilteredChildren.length === 0) {
221 !isCreatable &&
222 typeaheadFilteredChildren.push(React.createElement(SelectOption, { isDisabled: true, key: "no-results", value: noResultsFoundText, isNoResultsOption: true }));
223 }
224 if (isCreatable && typeaheadInputValue !== '') {
225 const newValue = typeaheadInputValue;
226 if (!typeaheadFilteredChildren.find((i) => i.props.value && i.props.value.toString().toLowerCase() === newValue.toString().toLowerCase())) {
227 const newOptionValue = isCreateSelectOptionObject
228 ? {
229 toString: () => newValue,
230 compareTo: value => this.toString()
231 .toLowerCase()
232 .includes(value.toString().toLowerCase())
233 }
234 : newValue;
235 typeaheadFilteredChildren.push(React.createElement(SelectOption, { key: `create ${newValue}`, value: newOptionValue, onClick: () => onCreateOption && onCreateOption(newValue) },
236 createText,
237 " \"",
238 newValue,
239 "\""));
240 }
241 }
242 this.setState({
243 typeaheadFilteredChildren
244 });
245 };
246 this.onClick = (e) => {
247 if (!this.props.isOpen) {
248 this.onToggle(true, e);
249 }
250 };
251 this.clearSelection = (_e) => {
252 this.setState({
253 typeaheadInputValue: null,
254 typeaheadFilteredChildren: React.Children.toArray(this.props.children),
255 typeaheadCurrIndex: -1
256 });
257 };
258 this.sendRef = (optionRef, favoriteRef, optionContainerRef, index) => {
259 this.refCollection[index] = [optionRef, favoriteRef];
260 this.optionContainerRefCollection[index] = optionContainerRef;
261 };
262 this.handleMenuKeys = (index, innerIndex, position) => {
263 keyHandler(index, innerIndex, position, this.refCollection, this.refCollection);
264 if (this.props.variant === SelectVariant.typeahead || this.props.variant === SelectVariant.typeaheadMulti) {
265 if (position !== 'tab') {
266 this.handleTypeaheadKeys(position);
267 }
268 }
269 };
270 this.moveFocus = (nextIndex, updateCurrentIndex = true) => {
271 const { isCreatable, createText } = this.props;
272 const hasDescriptionElm = Boolean(this.refCollection[nextIndex][0] && this.refCollection[nextIndex][0].classList.contains('pf-m-description'));
273 const isLoad = Boolean(this.refCollection[nextIndex][0] && this.refCollection[nextIndex][0].classList.contains('pf-m-load'));
274 const optionTextElm = hasDescriptionElm
275 ? this.refCollection[nextIndex][0].firstElementChild
276 : this.refCollection[nextIndex][0];
277 let typeaheadInputValue = '';
278 if (isCreatable && optionTextElm.innerText.includes(createText)) {
279 typeaheadInputValue = this.state.creatableValue;
280 }
281 else if (optionTextElm && !isLoad) {
282 // !isLoad prevents the view more button text from appearing the typeahead input
283 typeaheadInputValue = optionTextElm.innerText;
284 }
285 this.setState(prevState => ({
286 typeaheadCurrIndex: updateCurrentIndex ? nextIndex : prevState.typeaheadCurrIndex,
287 typeaheadStoredIndex: nextIndex,
288 typeaheadInputValue
289 }));
290 };
291 this.switchFocusToFavoriteMenu = () => {
292 const { typeaheadCurrIndex, typeaheadStoredIndex } = this.state;
293 let indexForFocus = 0;
294 if (typeaheadCurrIndex !== -1) {
295 indexForFocus = typeaheadCurrIndex;
296 }
297 else if (typeaheadStoredIndex !== -1) {
298 indexForFocus = typeaheadStoredIndex;
299 }
300 if (this.refCollection[indexForFocus] !== null && this.refCollection[indexForFocus][0] !== null) {
301 this.refCollection[indexForFocus][0].focus();
302 }
303 else {
304 this.clearRef.current.focus();
305 }
306 this.setState({
307 tabbedIntoFavoritesMenu: true,
308 typeaheadCurrIndex: -1
309 });
310 };
311 this.moveFocusToLastMenuItem = () => {
312 const refCollectionLen = this.refCollection.length;
313 if (refCollectionLen > 0 &&
314 this.refCollection[refCollectionLen - 1] !== null &&
315 this.refCollection[refCollectionLen - 1][0] !== null) {
316 this.refCollection[refCollectionLen - 1][0].focus();
317 }
318 };
319 this.handleTypeaheadKeys = (position, shiftKey = false) => {
320 const { isOpen, onFavorite } = this.props;
321 const { typeaheadCurrIndex, tabbedIntoFavoritesMenu } = this.state;
322 const typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
323 if (isOpen) {
324 if (position === 'enter') {
325 if (typeaheadCurrIndex !== -1 && // do not allow selection without moving to an initial option
326 (typeaheadActiveChild || (this.refCollection[0] && this.refCollection[0][0]))) {
327 if (typeaheadActiveChild) {
328 if (!typeaheadActiveChild.classList.contains('pf-m-load')) {
329 const hasDescriptionElm = typeaheadActiveChild.childElementCount > 1;
330 const typeaheadActiveChildText = hasDescriptionElm
331 ? typeaheadActiveChild.firstChild.innerText
332 : typeaheadActiveChild.innerText;
333 this.setState({
334 typeaheadInputValue: typeaheadActiveChildText
335 });
336 }
337 }
338 else if (this.refCollection[0] && this.refCollection[0][0]) {
339 this.setState({
340 typeaheadInputValue: this.refCollection[0][0].innerText
341 });
342 }
343 if (typeaheadActiveChild) {
344 typeaheadActiveChild.click();
345 }
346 else {
347 this.refCollection[0][0].click();
348 }
349 }
350 }
351 else if (position === 'tab') {
352 if (onFavorite) {
353 // if the input has focus, tab to the first item or the last item that was previously focused.
354 if (this.inputRef.current === document.activeElement) {
355 // If shift is also clicked and there is a footer, tab to the last item in tabbable footer
356 if (this.props.footer && shiftKey) {
357 const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
358 if (tabbableItems.length > 0) {
359 if (tabbableItems[tabbableItems.length - 1]) {
360 tabbableItems[tabbableItems.length - 1].focus();
361 }
362 }
363 }
364 else {
365 this.switchFocusToFavoriteMenu();
366 }
367 }
368 else {
369 // focus is on menu or footer
370 if (this.props.footer) {
371 let tabbedIntoMenu = false;
372 const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
373 if (tabbableItems.length > 0) {
374 // if current element is not in footer, tab to first tabbable element in footer,
375 // if shift was clicked, tab to input since focus is on menu
376 const currentElementIndex = tabbableItems.findIndex((item) => item === document.activeElement);
377 if (currentElementIndex === -1) {
378 if (shiftKey) {
379 // currently in menu, shift back to input
380 this.inputRef.current.focus();
381 }
382 else {
383 // currently in menu, tab to first tabbable item in footer
384 tabbableItems[0].focus();
385 }
386 }
387 else {
388 // already in footer
389 if (shiftKey) {
390 // shift to previous item
391 if (currentElementIndex === 0) {
392 // on first footer item, shift back to menu
393 this.switchFocusToFavoriteMenu();
394 tabbedIntoMenu = true;
395 }
396 else {
397 // shift to previous footer item
398 tabbableItems[currentElementIndex - 1].focus();
399 }
400 }
401 else {
402 // tab to next tabbable item in footer or to input.
403 if (tabbableItems[currentElementIndex + 1]) {
404 tabbableItems[currentElementIndex + 1].focus();
405 }
406 else {
407 this.inputRef.current.focus();
408 }
409 }
410 }
411 }
412 else {
413 // no tabbable items in footer, tab to input
414 this.inputRef.current.focus();
415 tabbedIntoMenu = false;
416 }
417 this.setState({ tabbedIntoFavoritesMenu: tabbedIntoMenu });
418 }
419 else {
420 this.inputRef.current.focus();
421 this.setState({ tabbedIntoFavoritesMenu: false });
422 }
423 }
424 }
425 else {
426 // Close if there is no footer
427 if (!this.props.footer) {
428 this.onToggle(false, null);
429 this.onClose();
430 }
431 else {
432 // has footer
433 const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
434 const currentElementIndex = tabbableItems.findIndex((item) => item === document.activeElement);
435 if (this.inputRef.current === document.activeElement) {
436 if (shiftKey) {
437 // close toggle if shift key and tab on input
438 this.onToggle(false, null);
439 this.onClose();
440 }
441 else {
442 // tab to first tabbable item in footer
443 if (tabbableItems[0]) {
444 tabbableItems[0].focus();
445 }
446 else {
447 this.onToggle(false, null);
448 this.onClose();
449 }
450 }
451 }
452 else {
453 // focus is in footer
454 if (shiftKey) {
455 if (currentElementIndex === 0) {
456 // shift tab back to input
457 this.inputRef.current.focus();
458 }
459 else {
460 // shift to previous footer item
461 tabbableItems[currentElementIndex - 1].focus();
462 }
463 }
464 else {
465 // tab to next footer item or close tab if last item
466 if (tabbableItems[currentElementIndex + 1]) {
467 tabbableItems[currentElementIndex + 1].focus();
468 }
469 else {
470 // no next item, close toggle
471 this.onToggle(false, null);
472 this.inputRef.current.focus();
473 this.onClose();
474 }
475 }
476 }
477 }
478 }
479 }
480 else if (!tabbedIntoFavoritesMenu) {
481 if (this.refCollection[0][0] === null) {
482 return;
483 }
484 let nextIndex;
485 if (typeaheadCurrIndex === -1 && position === 'down') {
486 nextIndex = 0;
487 }
488 else if (typeaheadCurrIndex === -1 && position === 'up') {
489 nextIndex = this.refCollection.length - 1;
490 }
491 else if (position !== 'left' && position !== 'right') {
492 nextIndex = getNextIndex(typeaheadCurrIndex, position, this.refCollection);
493 }
494 else {
495 nextIndex = typeaheadCurrIndex;
496 }
497 if (this.refCollection[nextIndex] === null) {
498 return;
499 }
500 this.moveFocus(nextIndex);
501 }
502 else {
503 const nextIndex = this.refCollection.findIndex(ref => ref !== undefined && (ref[0] === document.activeElement || ref[1] === document.activeElement));
504 this.moveFocus(nextIndex);
505 }
506 }
507 };
508 this.onClickTypeaheadToggleButton = () => {
509 if (this.inputRef && this.inputRef.current) {
510 this.inputRef.current.focus();
511 }
512 };
513 this.getDisplay = (value, type = 'node') => {
514 if (!value) {
515 return;
516 }
517 const item = this.props.isGrouped
518 ? React.Children.toArray(this.props.children)
519 .reduce((acc, curr) => [...acc, ...React.Children.toArray(curr.props.children)], [])
520 .find(child => child.props.value.toString() === value.toString())
521 : React.Children.toArray(this.props.children).find(child => child.props.value &&
522 child.props.value.toString() === value.toString());
523 if (item) {
524 if (item && item.props.children) {
525 if (type === 'node') {
526 return item.props.children;
527 }
528 return this.findText(item);
529 }
530 return item.props.value.toString();
531 }
532 return value.toString();
533 };
534 this.findText = (item) => {
535 if (typeof item === 'string') {
536 return item;
537 }
538 else if (!React.isValidElement(item)) {
539 return '';
540 }
541 else {
542 const multi = [];
543 React.Children.toArray(item.props.children).forEach(child => multi.push(this.findText(child)));
544 return multi.join('');
545 }
546 };
547 this.generateSelectedBadge = () => {
548 const { customBadgeText, selections } = this.props;
549 if (customBadgeText !== null) {
550 return customBadgeText;
551 }
552 if (Array.isArray(selections) && selections.length > 0) {
553 return selections.length;
554 }
555 return null;
556 };
557 this.setVieMoreNextIndex = () => {
558 this.setState({ viewMoreNextIndex: this.refCollection.length - 1 });
559 };
560 this.isLastOptionBeforeFooter = (index) => this.props.footer && index === this.refCollection.length - 1 ? true : false;
561 }
562 extendTypeaheadChildren(typeaheadCurrIndex, favoritesGroup) {
563 const { isGrouped, onFavorite } = this.props;
564 const typeaheadChildren = favoritesGroup
565 ? favoritesGroup.concat(this.state.typeaheadFilteredChildren)
566 : this.state.typeaheadFilteredChildren;
567 const activeElement = this.optionContainerRefCollection[typeaheadCurrIndex];
568 let typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
569 if (typeaheadActiveChild && typeaheadActiveChild.classList.contains('pf-m-description')) {
570 typeaheadActiveChild = typeaheadActiveChild.firstElementChild;
571 }
572 this.refCollection = [[]];
573 this.optionContainerRefCollection = [];
574 if (isGrouped) {
575 return React.Children.map(typeaheadChildren, (group) => {
576 if (group.type === Divider) {
577 return group;
578 }
579 else if (group.type === SelectGroup && onFavorite) {
580 return React.cloneElement(group, {
581 titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
582 children: React.Children.map(group.props.children, (child) => child.type === Divider
583 ? child
584 : React.cloneElement(child, {
585 isFocused: activeElement &&
586 (activeElement.id === child.props.id ||
587 (this.props.isCreatable &&
588 typeaheadActiveChild.innerText ===
589 `{createText} "${group.props.value}"`))
590 }))
591 });
592 }
593 else if (group.type === SelectGroup) {
594 return React.cloneElement(group, {
595 titleId: group.props.label && group.props.label.replace(/\W/g, '-'),
596 children: React.Children.map(group.props.children, (child) => child.type === Divider
597 ? child
598 : React.cloneElement(child, {
599 isFocused: typeaheadActiveChild &&
600 (typeaheadActiveChild.innerText === child.props.value.toString() ||
601 (this.props.isCreatable &&
602 typeaheadActiveChild.innerText ===
603 `{createText} "${child.props.value}"`))
604 }))
605 });
606 }
607 else {
608 // group has been filtered down to SelectOption
609 return React.cloneElement(group, {
610 isFocused: typeaheadActiveChild &&
611 (typeaheadActiveChild.innerText === group.props.value.toString() ||
612 (this.props.isCreatable && typeaheadActiveChild.innerText === `{createText} "${group.props.value}"`))
613 });
614 }
615 });
616 }
617 return typeaheadChildren.map((child, index) => {
618 const childElement = child;
619 return childElement.type.displayName === 'Divider'
620 ? child
621 : React.cloneElement(child, {
622 isFocused: typeaheadActiveChild
623 ? typeaheadActiveChild.innerText === child.props.value.toString() ||
624 (this.props.isCreatable &&
625 typeaheadActiveChild.innerText === `{createText} "${child.props.value}"`)
626 : index === typeaheadCurrIndex // fallback for view more + typeahead use cases, when the new expanded list is loaded and refCollection hasn't be updated yet
627 });
628 });
629 }
630 render() {
631 const _a = this.props, { children, chipGroupProps, chipGroupComponent, className, customContent, variant, direction, onSelect, onClear, onBlur, toggleId, isOpen, isGrouped, isPlain, isDisabled, hasPlaceholderStyle, validated, selections: selectionsProp, typeAheadAriaLabel, clearSelectionsAriaLabel, toggleAriaLabel, removeSelectionAriaLabel, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedby, 'aria-invalid': ariaInvalid, placeholderText, width, maxHeight, toggleIcon, ouiaId, ouiaSafe, hasInlineFilter, isCheckboxSelectionBadgeHidden, inlineFilterPlaceholderText,
632 /* eslint-disable @typescript-eslint/no-unused-vars */
633 onFilter,
634 /* eslint-disable @typescript-eslint/no-unused-vars */
635 onTypeaheadInputChanged, onCreateOption, isCreatable, onToggle, createText, noResultsFoundText, customBadgeText, inputIdPrefix, inputAutoComplete,
636 /* eslint-disable @typescript-eslint/no-unused-vars */
637 isInputValuePersisted, isInputFilterPersisted,
638 /* eslint-enable @typescript-eslint/no-unused-vars */
639 menuAppendTo, favorites, onFavorite,
640 /* eslint-disable @typescript-eslint/no-unused-vars */
641 favoritesLabel, footer, loadingVariant, isCreateSelectOptionObject, shouldResetOnSelect } = _a, props = __rest(_a, ["children", "chipGroupProps", "chipGroupComponent", "className", "customContent", "variant", "direction", "onSelect", "onClear", "onBlur", "toggleId", "isOpen", "isGrouped", "isPlain", "isDisabled", "hasPlaceholderStyle", "validated", "selections", "typeAheadAriaLabel", "clearSelectionsAriaLabel", "toggleAriaLabel", "removeSelectionAriaLabel", 'aria-label', 'aria-labelledby', 'aria-describedby', 'aria-invalid', "placeholderText", "width", "maxHeight", "toggleIcon", "ouiaId", "ouiaSafe", "hasInlineFilter", "isCheckboxSelectionBadgeHidden", "inlineFilterPlaceholderText", "onFilter", "onTypeaheadInputChanged", "onCreateOption", "isCreatable", "onToggle", "createText", "noResultsFoundText", "customBadgeText", "inputIdPrefix", "inputAutoComplete", "isInputValuePersisted", "isInputFilterPersisted", "menuAppendTo", "favorites", "onFavorite", "favoritesLabel", "footer", "loadingVariant", "isCreateSelectOptionObject", "shouldResetOnSelect"]);
642 const { focusFirstOption: openedOnEnter, typeaheadCurrIndex, typeaheadInputValue, typeaheadFilteredChildren, favoritesGroup } = this.state;
643 const selectToggleId = toggleId || `pf-select-toggle-id-${currentId++}`;
644 const selections = Array.isArray(selectionsProp) ? selectionsProp : [selectionsProp];
645 // Find out if the selected option is a placeholder
646 const selectedOption = React.Children.toArray(children).find((option) => option.props.value === selections[0]);
647 const isSelectedPlaceholder = selectedOption && selectedOption.props.isPlaceholder;
648 const hasAnySelections = Boolean(selections[0] && selections[0] !== '');
649 const typeaheadActiveChild = this.getTypeaheadActiveChild(typeaheadCurrIndex);
650 let childPlaceholderText = null;
651 // If onFavorites is set, add isFavorite prop to children and add a Favorites group to the SelectMenu
652 let renderableItems = [];
653 if (onFavorite) {
654 // if variant is type-ahead call the extendTypeaheadChildren before adding favorites
655 let tempExtendedChildren = children;
656 if (variant === 'typeahead' || variant === 'typeaheadmulti') {
657 tempExtendedChildren = this.extendTypeaheadChildren(typeaheadCurrIndex, favoritesGroup);
658 }
659 else if (onFavorite) {
660 tempExtendedChildren = favoritesGroup.concat(children);
661 }
662 // mark items that are favorited with isFavorite
663 renderableItems = extendItemsWithFavorite(tempExtendedChildren, isGrouped, favorites);
664 }
665 else {
666 renderableItems = children;
667 }
668 if (!customContent) {
669 if (!hasAnySelections && !placeholderText) {
670 const childPlaceholder = React.Children.toArray(children).filter((child) => child.props.isPlaceholder === true);
671 childPlaceholderText =
672 (childPlaceholder[0] && this.getDisplay(childPlaceholder[0].props.value, 'node')) ||
673 (children[0] && this.getDisplay(children[0].props.value, 'node'));
674 }
675 }
676 if (isOpen) {
677 if (renderableItems.find(item => { var _a; return ((_a = item) === null || _a === void 0 ? void 0 : _a.key) === 'loading'; }) === undefined) {
678 if (loadingVariant === 'spinner') {
679 renderableItems.push(React.createElement(SelectOption, { isLoading: true, key: "loading", value: "loading" },
680 React.createElement(Spinner, { size: "lg" })));
681 }
682 else if (loadingVariant === null || loadingVariant === void 0 ? void 0 : loadingVariant.text) {
683 renderableItems.push(React.createElement(SelectOption, { isLoad: true, key: "loading", value: loadingVariant.text, setViewMoreNextIndex: this.setVieMoreNextIndex, onClick: loadingVariant === null || loadingVariant === void 0 ? void 0 : loadingVariant.onClick }));
684 }
685 }
686 }
687 const hasOnClear = onClear !== Select.defaultProps.onClear;
688 const clearBtn = (React.createElement("button", { className: css(buttonStyles.button, buttonStyles.modifiers.plain, styles.selectToggleClear), onClick: e => {
689 this.clearSelection(e);
690 onClear(e);
691 e.stopPropagation();
692 }, "aria-label": clearSelectionsAriaLabel, type: "button", disabled: isDisabled, ref: this.clearRef, onKeyDown: event => {
693 if (event.key === KeyTypes.Enter) {
694 this.clearRef.current.click();
695 }
696 } },
697 React.createElement(TimesCircleIcon, { "aria-hidden": true })));
698 let selectedChips = null;
699 if (variant === SelectVariant.typeaheadMulti) {
700 selectedChips = chipGroupComponent ? (chipGroupComponent) : (React.createElement(ChipGroup, Object.assign({}, chipGroupProps), selections &&
701 selections.map(item => (React.createElement(Chip, { key: item, onClick: (e) => onSelect(e, item), closeBtnAriaLabel: removeSelectionAriaLabel }, this.getDisplay(item, 'node'))))));
702 }
703 if (hasInlineFilter) {
704 const filterBox = (React.createElement(React.Fragment, null,
705 React.createElement("div", { key: "inline-filter", className: css(styles.selectMenuSearch) },
706 React.createElement("input", { key: "inline-filter-input", type: "search", className: css(formStyles.formControl, formStyles.modifiers.search), onChange: this.onChange, placeholder: inlineFilterPlaceholderText, onKeyDown: event => {
707 if (event.key === KeyTypes.ArrowUp) {
708 this.handleMenuKeys(0, 0, 'up');
709 event.preventDefault();
710 }
711 else if (event.key === KeyTypes.ArrowDown) {
712 this.handleMenuKeys(0, 0, 'down');
713 event.preventDefault();
714 }
715 else if (event.key === KeyTypes.ArrowLeft) {
716 this.handleMenuKeys(0, 0, 'left');
717 event.preventDefault();
718 }
719 else if (event.key === KeyTypes.ArrowRight) {
720 this.handleMenuKeys(0, 0, 'right');
721 event.preventDefault();
722 }
723 else if (event.key === KeyTypes.Tab && variant !== SelectVariant.checkbox && this.props.footer) {
724 // tab to footer or close menu if shift key
725 if (event.shiftKey) {
726 this.onToggle(false, event);
727 }
728 else {
729 const tabbableItems = findTabbableElements(this.footerRef, SelectFooterTabbableItems);
730 if (tabbableItems.length > 0) {
731 tabbableItems[0].focus();
732 event.stopPropagation();
733 event.preventDefault();
734 }
735 else {
736 this.onToggle(false, event);
737 }
738 }
739 }
740 else if (event.key === KeyTypes.Tab && variant === SelectVariant.checkbox) {
741 // More modal-like experience for checkboxes
742 // Let SelectOption handle this
743 if (event.shiftKey) {
744 this.handleMenuKeys(0, 0, 'up');
745 }
746 else {
747 this.handleMenuKeys(0, 0, 'down');
748 }
749 event.stopPropagation();
750 event.preventDefault();
751 }
752 }, ref: this.filterRef, autoComplete: inputAutoComplete })),
753 React.createElement(Divider, { key: "inline-filter-divider" })));
754 renderableItems = [filterBox, ...typeaheadFilteredChildren].map((option, index) => React.cloneElement(option, { key: index }));
755 }
756 let variantProps;
757 let variantChildren;
758 if (customContent) {
759 variantProps = {
760 selected: selections,
761 openedOnEnter,
762 isCustomContent: true
763 };
764 variantChildren = customContent;
765 }
766 else {
767 switch (variant) {
768 case 'single':
769 variantProps = {
770 selected: selections[0],
771 hasInlineFilter,
772 openedOnEnter
773 };
774 variantChildren = renderableItems;
775 break;
776 case 'checkbox':
777 variantProps = {
778 checked: selections,
779 isGrouped,
780 hasInlineFilter,
781 openedOnEnter
782 };
783 variantChildren = renderableItems;
784 break;
785 case 'typeahead':
786 variantProps = {
787 selected: selections[0],
788 openedOnEnter
789 };
790 variantChildren = onFavorite ? renderableItems : this.extendTypeaheadChildren(typeaheadCurrIndex);
791 if (variantChildren.length === 0) {
792 variantChildren.push(React.createElement(SelectOption, { isDisabled: true, key: 0, value: noResultsFoundText, isNoResultsOption: true }));
793 }
794 break;
795 case 'typeaheadmulti':
796 variantProps = {
797 selected: selections,
798 openedOnEnter
799 };
800 variantChildren = onFavorite ? renderableItems : this.extendTypeaheadChildren(typeaheadCurrIndex);
801 if (variantChildren.length === 0) {
802 variantChildren.push(React.createElement(SelectOption, { isDisabled: true, key: 0, value: noResultsFoundText, isNoResultsOption: true }));
803 }
804 break;
805 }
806 }
807 const innerMenu = (React.createElement(SelectMenu, Object.assign({}, props, { isGrouped: isGrouped, selected: selections }, variantProps, { openedOnEnter: openedOnEnter, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, sendRef: this.sendRef, keyHandler: this.handleMenuKeys, maxHeight: maxHeight, ref: this.menuComponentRef, footer: footer, footerRef: this.footerRef, isLastOptionBeforeFooter: this.isLastOptionBeforeFooter }), variantChildren));
808 const menuContainer = footer ? React.createElement("div", { className: css(styles.selectMenu) },
809 " ",
810 innerMenu,
811 " ") : innerMenu;
812 const popperContainer = (React.createElement("div", Object.assign({ className: css(styles.select, isOpen && styles.modifiers.expanded, validated === ValidatedOptions.success && styles.modifiers.success, validated === ValidatedOptions.warning && styles.modifiers.warning, validated === ValidatedOptions.error && styles.modifiers.invalid, direction === SelectDirection.up && styles.modifiers.top, className) }, (width && { style: { width } }), (validated !== ValidatedOptions.default && { 'aria-describedby': ariaDescribedby }), (validated !== ValidatedOptions.default && { 'aria-invalid': ariaInvalid })), isOpen && menuContainer));
813 const mainContainer = (React.createElement("div", Object.assign({ className: css(styles.select, isOpen && styles.modifiers.expanded, validated === ValidatedOptions.success && styles.modifiers.success, validated === ValidatedOptions.warning && styles.modifiers.warning, validated === ValidatedOptions.error && styles.modifiers.invalid, direction === SelectDirection.up && styles.modifiers.top, className), ref: this.parentRef }, getOUIAProps(Select.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe), (width && { style: { width } }), (validated !== ValidatedOptions.default && { 'aria-describedby': ariaDescribedby }), (validated !== ValidatedOptions.default && { 'aria-invalid': ariaInvalid })),
814 React.createElement(SelectToggle, Object.assign({ id: selectToggleId, parentRef: this.parentRef, menuRef: this.menuComponentRef }, (footer && { footerRef: this.footerRef }), { isOpen: isOpen, isPlain: isPlain, hasPlaceholderStyle: hasPlaceholderStyle && (!selections.length || selections[0] === null || isSelectedPlaceholder), onToggle: this.onToggle, onEnter: this.onEnter, onClose: this.onClose, onBlur: onBlur, variant: variant, "aria-labelledby": `${ariaLabelledBy || ''} ${selectToggleId}`, "aria-label": toggleAriaLabel, handleTypeaheadKeys: this.handleTypeaheadKeys, moveFocusToLastMenuItem: this.moveFocusToLastMenuItem, isDisabled: isDisabled, hasClearButton: hasOnClear, hasFooter: footer !== undefined, onClickTypeaheadToggleButton: this.onClickTypeaheadToggleButton }),
815 customContent && (React.createElement("div", { className: css(styles.selectToggleWrapper) },
816 toggleIcon && React.createElement("span", { className: css(styles.selectToggleIcon) }, toggleIcon),
817 React.createElement("span", { className: css(styles.selectToggleText) }, placeholderText))),
818 variant === SelectVariant.single && !customContent && (React.createElement(React.Fragment, null,
819 React.createElement("div", { className: css(styles.selectToggleWrapper) },
820 toggleIcon && React.createElement("span", { className: css(styles.selectToggleIcon) }, toggleIcon),
821 React.createElement("span", { className: css(styles.selectToggleText) }, this.getDisplay(selections[0], 'node') || placeholderText || childPlaceholderText)),
822 hasOnClear && hasAnySelections && clearBtn)),
823 variant === SelectVariant.checkbox && !customContent && (React.createElement(React.Fragment, null,
824 React.createElement("div", { className: css(styles.selectToggleWrapper) },
825 toggleIcon && React.createElement("span", { className: css(styles.selectToggleIcon) }, toggleIcon),
826 React.createElement("span", { className: css(styles.selectToggleText) }, placeholderText),
827 !isCheckboxSelectionBadgeHidden && hasAnySelections && (React.createElement("div", { className: css(styles.selectToggleBadge) },
828 React.createElement("span", { className: css(badgeStyles.badge, badgeStyles.modifiers.read) }, this.generateSelectedBadge())))),
829 hasOnClear && hasAnySelections && clearBtn)),
830 variant === SelectVariant.typeahead && !customContent && (React.createElement(React.Fragment, null,
831 React.createElement("div", { className: css(styles.selectToggleWrapper) },
832 toggleIcon && React.createElement("span", { className: css(styles.selectToggleIcon) }, toggleIcon),
833 React.createElement("input", { className: css(formStyles.formControl, styles.selectToggleTypeahead), "aria-activedescendant": typeaheadActiveChild && typeaheadActiveChild.id, id: `${selectToggleId}-select-typeahead`, "aria-label": typeAheadAriaLabel, placeholder: placeholderText, value: typeaheadInputValue !== null
834 ? typeaheadInputValue
835 : this.getDisplay(selections[0], 'text') || '', type: "text", onClick: this.onClick, onChange: this.onChange, autoComplete: inputAutoComplete, disabled: isDisabled, ref: this.inputRef })),
836 hasOnClear && (selections[0] || typeaheadInputValue) && clearBtn)),
837 variant === SelectVariant.typeaheadMulti && !customContent && (React.createElement(React.Fragment, null,
838 React.createElement("div", { className: css(styles.selectToggleWrapper) },
839 toggleIcon && React.createElement("span", { className: css(styles.selectToggleIcon) }, toggleIcon),
840 selections && Array.isArray(selections) && selections.length > 0 && selectedChips,
841 React.createElement("input", { className: css(formStyles.formControl, styles.selectToggleTypeahead), "aria-activedescendant": typeaheadActiveChild && typeaheadActiveChild.id, id: `${selectToggleId}-select-multi-typeahead-typeahead`, "aria-label": typeAheadAriaLabel, "aria-invalid": validated === ValidatedOptions.error, placeholder: placeholderText, value: typeaheadInputValue !== null ? typeaheadInputValue : '', type: "text", onChange: this.onChange, onClick: this.onClick, autoComplete: inputAutoComplete, disabled: isDisabled, ref: this.inputRef })),
842 hasOnClear && ((selections && selections.length > 0) || typeaheadInputValue) && clearBtn)),
843 validated === ValidatedOptions.success && (React.createElement("span", { className: css(styles.selectToggleStatusIcon) },
844 React.createElement(CheckCircleIcon, { "aria-hidden": "true" }))),
845 validated === ValidatedOptions.error && (React.createElement("span", { className: css(styles.selectToggleStatusIcon) },
846 React.createElement(ExclamationCircleIcon, { "aria-hidden": "true" }))),
847 validated === ValidatedOptions.warning && (React.createElement("span", { className: css(styles.selectToggleStatusIcon) },
848 React.createElement(ExclamationTriangleIcon, { "aria-hidden": "true" })))),
849 isOpen && menuAppendTo === 'inline' && menuContainer));
850 const getParentElement = () => {
851 if (this.parentRef && this.parentRef.current) {
852 return this.parentRef.current.parentElement;
853 }
854 return null;
855 };
856 return (React.createElement(GenerateId, null, randomId => (React.createElement(SelectContext.Provider, { value: {
857 onSelect,
858 onFavorite,
859 onClose: this.onClose,
860 variant,
861 inputIdPrefix: inputIdPrefix || randomId,
862 shouldResetOnSelect
863 } }, menuAppendTo === 'inline' ? (mainContainer) : (React.createElement(Popper, { trigger: mainContainer, popper: popperContainer, direction: direction, appendTo: menuAppendTo === 'parent' ? getParentElement() : menuAppendTo, isVisible: isOpen }))))));
864 }
865}
866Select.displayName = 'Select';
867Select.defaultProps = {
868 children: [],
869 className: '',
870 position: SelectPosition.left,
871 direction: SelectDirection.down,
872 toggleId: null,
873 isOpen: false,
874 isGrouped: false,
875 isPlain: false,
876 isDisabled: false,
877 hasPlaceholderStyle: false,
878 isCreatable: false,
879 validated: 'default',
880 'aria-label': '',
881 'aria-labelledby': '',
882 'aria-describedby': '',
883 'aria-invalid': false,
884 typeAheadAriaLabel: '',
885 clearSelectionsAriaLabel: 'Clear all',
886 toggleAriaLabel: 'Options menu',
887 removeSelectionAriaLabel: 'Remove',
888 selections: [],
889 createText: 'Create',
890 placeholderText: '',
891 noResultsFoundText: 'No results found',
892 variant: SelectVariant.single,
893 width: '',
894 onClear: () => undefined,
895 onCreateOption: () => undefined,
896 toggleIcon: null,
897 onFilter: null,
898 onTypeaheadInputChanged: null,
899 customContent: null,
900 hasInlineFilter: false,
901 inlineFilterPlaceholderText: null,
902 customBadgeText: null,
903 inputIdPrefix: '',
904 inputAutoComplete: 'off',
905 menuAppendTo: 'inline',
906 favorites: [],
907 favoritesLabel: 'Favorites',
908 ouiaSafe: true,
909 chipGroupComponent: null,
910 isInputValuePersisted: false,
911 isInputFilterPersisted: false,
912 isCreateSelectOptionObject: false,
913 shouldResetOnSelect: true
914};
915//# sourceMappingURL=Select.js.map
\No newline at end of file