UNPKG

27.6 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2
3/* eslint-disable no-constant-condition */
4import * as React from 'react';
5import { setRef, useEventCallback, useControlled, unstable_useId as useId } from '@material-ui/core/utils'; // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
6// Give up on IE 11 support for this feature
7
8function stripDiacritics(string) {
9 return typeof string.normalize !== 'undefined' ? string.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : string;
10}
11
12export function createFilterOptions(config = {}) {
13 const {
14 ignoreAccents = true,
15 ignoreCase = true,
16 limit,
17 matchFrom = 'any',
18 stringify,
19 trim = false
20 } = config;
21 return (options, {
22 inputValue,
23 getOptionLabel
24 }) => {
25 let input = trim ? inputValue.trim() : inputValue;
26
27 if (ignoreCase) {
28 input = input.toLowerCase();
29 }
30
31 if (ignoreAccents) {
32 input = stripDiacritics(input);
33 }
34
35 const filteredOptions = options.filter(option => {
36 let candidate = (stringify || getOptionLabel)(option);
37
38 if (ignoreCase) {
39 candidate = candidate.toLowerCase();
40 }
41
42 if (ignoreAccents) {
43 candidate = stripDiacritics(candidate);
44 }
45
46 return matchFrom === 'start' ? candidate.indexOf(input) === 0 : candidate.indexOf(input) > -1;
47 });
48 return typeof limit === 'number' ? filteredOptions.slice(0, limit) : filteredOptions;
49 };
50} // To replace with .findIndex() once we stop IE 11 support.
51
52function findIndex(array, comp) {
53 for (let i = 0; i < array.length; i += 1) {
54 if (comp(array[i])) {
55 return i;
56 }
57 }
58
59 return -1;
60}
61
62const defaultFilterOptions = createFilterOptions(); // Number of options to jump in list box when pageup and pagedown keys are used.
63
64const pageSize = 5;
65export default function useAutocomplete(props) {
66 const {
67 autoComplete = false,
68 autoHighlight = false,
69 autoSelect = false,
70 blurOnSelect = false,
71 clearOnBlur = !props.freeSolo,
72 clearOnEscape = false,
73 componentName = 'useAutocomplete',
74 debug = false,
75 defaultValue = props.multiple ? [] : null,
76 disableClearable = false,
77 disableCloseOnSelect = false,
78 disabledItemsFocusable = false,
79 disableListWrap = false,
80 filterOptions = defaultFilterOptions,
81 filterSelectedOptions = false,
82 freeSolo = false,
83 getOptionDisabled,
84 getOptionLabel: getOptionLabelProp = option => option,
85 getOptionSelected = (option, value) => option === value,
86 groupBy,
87 handleHomeEndKeys = !props.freeSolo,
88 id: idProp,
89 includeInputInList = false,
90 inputValue: inputValueProp,
91 multiple = false,
92 onChange,
93 onClose,
94 onHighlightChange,
95 onInputChange,
96 onOpen,
97 open: openProp,
98 openOnFocus = false,
99 options,
100 selectOnFocus = !props.freeSolo,
101 value: valueProp
102 } = props;
103 const id = useId(idProp);
104 let getOptionLabel = getOptionLabelProp;
105
106 if (process.env.NODE_ENV !== 'production') {
107 getOptionLabel = option => {
108 const optionLabel = getOptionLabelProp(option);
109
110 if (typeof optionLabel !== 'string') {
111 const erroneousReturn = optionLabel === undefined ? 'undefined' : `${typeof optionLabel} (${optionLabel})`;
112 console.error(`Material-UI: The \`getOptionLabel\` method of ${componentName} returned ${erroneousReturn} instead of a string for ${JSON.stringify(option)}.`);
113 }
114
115 return optionLabel;
116 };
117 }
118
119 const ignoreFocus = React.useRef(false);
120 const firstFocus = React.useRef(true);
121 const inputRef = React.useRef(null);
122 const listboxRef = React.useRef(null);
123 const [anchorEl, setAnchorEl] = React.useState(null);
124 const [focusedTag, setFocusedTag] = React.useState(-1);
125 const defaultHighlighted = autoHighlight ? 0 : -1;
126 const highlightedIndexRef = React.useRef(defaultHighlighted);
127 const [value, setValue] = useControlled({
128 controlled: valueProp,
129 default: defaultValue,
130 name: componentName
131 });
132 const [inputValue, setInputValue] = useControlled({
133 controlled: inputValueProp,
134 default: '',
135 name: componentName,
136 state: 'inputValue'
137 });
138 const [focused, setFocused] = React.useState(false);
139 const resetInputValue = useEventCallback((event, newValue) => {
140 let newInputValue;
141
142 if (multiple) {
143 newInputValue = '';
144 } else if (newValue == null) {
145 newInputValue = '';
146 } else {
147 const optionLabel = getOptionLabel(newValue);
148 newInputValue = typeof optionLabel === 'string' ? optionLabel : '';
149 }
150
151 if (inputValue === newInputValue) {
152 return;
153 }
154
155 setInputValue(newInputValue);
156
157 if (onInputChange) {
158 onInputChange(event, newInputValue, 'reset');
159 }
160 });
161 React.useEffect(() => {
162 resetInputValue(null, value);
163 }, [value, resetInputValue]);
164 const [open, setOpenState] = useControlled({
165 controlled: openProp,
166 default: false,
167 name: componentName,
168 state: 'open'
169 });
170 const inputValueIsSelectedValue = !multiple && value != null && inputValue === getOptionLabel(value);
171 const popupOpen = open;
172 const filteredOptions = popupOpen ? filterOptions(options.filter(option => {
173 if (filterSelectedOptions && (multiple ? value : [value]).some(value2 => value2 !== null && getOptionSelected(option, value2))) {
174 return false;
175 }
176
177 return true;
178 }), // we use the empty string to manipulate `filterOptions` to not filter any options
179 // i.e. the filter predicate always returns true
180 {
181 inputValue: inputValueIsSelectedValue ? '' : inputValue,
182 getOptionLabel
183 }) : [];
184
185 if (process.env.NODE_ENV !== 'production') {
186 if (value !== null && !freeSolo && options.length > 0) {
187 const missingValue = (multiple ? value : [value]).filter(value2 => !options.some(option => getOptionSelected(option, value2)));
188
189 if (missingValue.length > 0) {
190 console.warn([`Material-UI: The value provided to ${componentName} is invalid.`, `None of the options match with \`${missingValue.length > 1 ? JSON.stringify(missingValue) : JSON.stringify(missingValue[0])}\`.`, 'You can use the `getOptionSelected` prop to customize the equality test.'].join('\n'));
191 }
192 }
193 }
194
195 const focusTag = useEventCallback(tagToFocus => {
196 if (tagToFocus === -1) {
197 inputRef.current.focus();
198 } else {
199 anchorEl.querySelector(`[data-tag-index="${tagToFocus}"]`).focus();
200 }
201 }); // Ensure the focusedTag is never inconsistent
202
203 React.useEffect(() => {
204 if (multiple && focusedTag > value.length - 1) {
205 setFocusedTag(-1);
206 focusTag(-1);
207 }
208 }, [value, multiple, focusedTag, focusTag]);
209
210 function validOptionIndex(index, direction) {
211 if (!listboxRef.current || index === -1) {
212 return -1;
213 }
214
215 let nextFocus = index;
216
217 while (true) {
218 // Out of range
219 if (direction === 'next' && nextFocus === filteredOptions.length || direction === 'previous' && nextFocus === -1) {
220 return -1;
221 }
222
223 const option = listboxRef.current.querySelector(`[data-option-index="${nextFocus}"]`); // Same logic as MenuList.js
224
225 const nextFocusDisabled = disabledItemsFocusable ? false : option && (option.disabled || option.getAttribute('aria-disabled') === 'true');
226
227 if (option && !option.hasAttribute('tabindex') || nextFocusDisabled) {
228 // Move to the next element.
229 nextFocus += direction === 'next' ? 1 : -1;
230 } else {
231 return nextFocus;
232 }
233 }
234 }
235
236 const setHighlightedIndex = useEventCallback(({
237 event,
238 index,
239 reason = 'auto'
240 }) => {
241 highlightedIndexRef.current = index; // does the index exist?
242
243 if (index === -1) {
244 inputRef.current.removeAttribute('aria-activedescendant');
245 } else {
246 inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
247 }
248
249 if (onHighlightChange) {
250 onHighlightChange(event, index === -1 ? null : filteredOptions[index], reason);
251 }
252
253 if (!listboxRef.current) {
254 return;
255 }
256
257 const prev = listboxRef.current.querySelector('[data-focus]');
258
259 if (prev) {
260 prev.removeAttribute('data-focus');
261 }
262
263 const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]'); // "No results"
264
265 if (!listboxNode) {
266 return;
267 }
268
269 if (index === -1) {
270 listboxNode.scrollTop = 0;
271 return;
272 }
273
274 const option = listboxRef.current.querySelector(`[data-option-index="${index}"]`);
275
276 if (!option) {
277 return;
278 }
279
280 option.setAttribute('data-focus', 'true'); // Scroll active descendant into view.
281 // Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js
282 //
283 // Consider this API instead once it has a better browser support:
284 // .scrollIntoView({ scrollMode: 'if-needed', block: 'nearest' });
285
286 if (listboxNode.scrollHeight > listboxNode.clientHeight && reason !== 'mouse') {
287 const element = option;
288 const scrollBottom = listboxNode.clientHeight + listboxNode.scrollTop;
289 const elementBottom = element.offsetTop + element.offsetHeight;
290
291 if (elementBottom > scrollBottom) {
292 listboxNode.scrollTop = elementBottom - listboxNode.clientHeight;
293 } else if (element.offsetTop - element.offsetHeight * (groupBy ? 1.3 : 0) < listboxNode.scrollTop) {
294 listboxNode.scrollTop = element.offsetTop - element.offsetHeight * (groupBy ? 1.3 : 0);
295 }
296 }
297 });
298 const changeHighlightedIndex = useEventCallback(({
299 event,
300 diff,
301 direction = 'next',
302 reason = 'auto'
303 }) => {
304 if (!popupOpen) {
305 return;
306 }
307
308 const getNextIndex = () => {
309 const maxIndex = filteredOptions.length - 1;
310
311 if (diff === 'reset') {
312 return defaultHighlighted;
313 }
314
315 if (diff === 'start') {
316 return 0;
317 }
318
319 if (diff === 'end') {
320 return maxIndex;
321 }
322
323 const newIndex = highlightedIndexRef.current + diff;
324
325 if (newIndex < 0) {
326 if (newIndex === -1 && includeInputInList) {
327 return -1;
328 }
329
330 if (disableListWrap && highlightedIndexRef.current !== -1 || Math.abs(diff) > 1) {
331 return 0;
332 }
333
334 return maxIndex;
335 }
336
337 if (newIndex > maxIndex) {
338 if (newIndex === maxIndex + 1 && includeInputInList) {
339 return -1;
340 }
341
342 if (disableListWrap || Math.abs(diff) > 1) {
343 return maxIndex;
344 }
345
346 return 0;
347 }
348
349 return newIndex;
350 };
351
352 const nextIndex = validOptionIndex(getNextIndex(), direction);
353 setHighlightedIndex({
354 index: nextIndex,
355 reason,
356 event
357 }); // Sync the content of the input with the highlighted option.
358
359 if (autoComplete && diff !== 'reset') {
360 if (nextIndex === -1) {
361 inputRef.current.value = inputValue;
362 } else {
363 const option = getOptionLabel(filteredOptions[nextIndex]);
364 inputRef.current.value = option; // The portion of the selected suggestion that has not been typed by the user,
365 // a completion string, appears inline after the input cursor in the textbox.
366
367 const index = option.toLowerCase().indexOf(inputValue.toLowerCase());
368
369 if (index === 0 && inputValue.length > 0) {
370 inputRef.current.setSelectionRange(inputValue.length, option.length);
371 }
372 }
373 }
374 });
375 const syncHighlightedIndex = React.useCallback(() => {
376 if (!popupOpen) {
377 return;
378 }
379
380 const valueItem = multiple ? value[0] : value; // The popup is empty, reset
381
382 if (filteredOptions.length === 0 || valueItem == null) {
383 changeHighlightedIndex({
384 diff: 'reset'
385 });
386 return;
387 }
388
389 if (!listboxRef.current) {
390 return;
391 } // Synchronize the value with the highlighted index
392
393
394 if (!filterSelectedOptions && valueItem != null) {
395 const currentOption = filteredOptions[highlightedIndexRef.current]; // Keep the current highlighted index if possible
396
397 if (multiple && currentOption && findIndex(value, val => getOptionSelected(currentOption, val)) !== -1) {
398 return;
399 }
400
401 const itemIndex = findIndex(filteredOptions, optionItem => getOptionSelected(optionItem, valueItem));
402
403 if (itemIndex === -1) {
404 changeHighlightedIndex({
405 diff: 'reset'
406 });
407 } else {
408 setHighlightedIndex({
409 index: itemIndex
410 });
411 }
412
413 return;
414 } // Prevent the highlighted index to leak outside the boundaries.
415
416
417 if (highlightedIndexRef.current >= filteredOptions.length - 1) {
418 setHighlightedIndex({
419 index: filteredOptions.length - 1
420 });
421 return;
422 } // Restore the focus to the previous index.
423
424
425 setHighlightedIndex({
426 index: highlightedIndexRef.current
427 }); // Ignore filteredOptions (and options, getOptionSelected, getOptionLabel) not to break the scroll position
428 // eslint-disable-next-line react-hooks/exhaustive-deps
429 }, [// Only sync the highlighted index when the option switch between empty and not
430 // eslint-disable-next-line react-hooks/exhaustive-deps
431 filteredOptions.length === 0, // Don't sync the highlighted index with the value when multiple
432 // eslint-disable-next-line react-hooks/exhaustive-deps
433 multiple ? false : value, filterSelectedOptions, changeHighlightedIndex, setHighlightedIndex, popupOpen, inputValue, multiple]);
434 const handleListboxRef = useEventCallback(node => {
435 setRef(listboxRef, node);
436
437 if (!node) {
438 return;
439 }
440
441 syncHighlightedIndex();
442 });
443 React.useEffect(() => {
444 syncHighlightedIndex();
445 }, [syncHighlightedIndex]);
446
447 const handleOpen = event => {
448 if (open) {
449 return;
450 }
451
452 setOpenState(true);
453
454 if (onOpen) {
455 onOpen(event);
456 }
457 };
458
459 const handleClose = (event, reason) => {
460 if (!open) {
461 return;
462 }
463
464 setOpenState(false);
465
466 if (onClose) {
467 onClose(event, reason);
468 }
469 };
470
471 const handleValue = (event, newValue, reason, details) => {
472 if (value === newValue) {
473 return;
474 }
475
476 if (onChange) {
477 onChange(event, newValue, reason, details);
478 }
479
480 setValue(newValue);
481 };
482
483 const isTouch = React.useRef(false);
484
485 const selectNewValue = (event, option, reasonProp = 'select-option', origin = 'options') => {
486 let reason = reasonProp;
487 let newValue = option;
488
489 if (multiple) {
490 newValue = Array.isArray(value) ? value.slice() : [];
491
492 if (process.env.NODE_ENV !== 'production') {
493 const matches = newValue.filter(val => getOptionSelected(option, val));
494
495 if (matches.length > 1) {
496 console.error([`Material-UI: The \`getOptionSelected\` method of ${componentName} do not handle the arguments correctly.`, `The component expects a single value to match a given option but found ${matches.length} matches.`].join('\n'));
497 }
498 }
499
500 const itemIndex = findIndex(newValue, valueItem => getOptionSelected(option, valueItem));
501
502 if (itemIndex === -1) {
503 newValue.push(option);
504 } else if (origin !== 'freeSolo') {
505 newValue.splice(itemIndex, 1);
506 reason = 'remove-option';
507 }
508 }
509
510 resetInputValue(event, newValue);
511 handleValue(event, newValue, reason, {
512 option
513 });
514
515 if (!disableCloseOnSelect) {
516 handleClose(event, reason);
517 }
518
519 if (blurOnSelect === true || blurOnSelect === 'touch' && isTouch.current || blurOnSelect === 'mouse' && !isTouch.current) {
520 inputRef.current.blur();
521 }
522 };
523
524 function validTagIndex(index, direction) {
525 if (index === -1) {
526 return -1;
527 }
528
529 let nextFocus = index;
530
531 while (true) {
532 // Out of range
533 if (direction === 'next' && nextFocus === value.length || direction === 'previous' && nextFocus === -1) {
534 return -1;
535 }
536
537 const option = anchorEl.querySelector(`[data-tag-index="${nextFocus}"]`); // Same logic as MenuList.js
538
539 if (option && (!option.hasAttribute('tabindex') || option.disabled || option.getAttribute('aria-disabled') === 'true')) {
540 nextFocus += direction === 'next' ? 1 : -1;
541 } else {
542 return nextFocus;
543 }
544 }
545 }
546
547 const handleFocusTag = (event, direction) => {
548 if (!multiple) {
549 return;
550 }
551
552 handleClose(event, 'toggleInput');
553 let nextTag = focusedTag;
554
555 if (focusedTag === -1) {
556 if (inputValue === '' && direction === 'previous') {
557 nextTag = value.length - 1;
558 }
559 } else {
560 nextTag += direction === 'next' ? 1 : -1;
561
562 if (nextTag < 0) {
563 nextTag = 0;
564 }
565
566 if (nextTag === value.length) {
567 nextTag = -1;
568 }
569 }
570
571 nextTag = validTagIndex(nextTag, direction);
572 setFocusedTag(nextTag);
573 focusTag(nextTag);
574 };
575
576 const handleClear = event => {
577 ignoreFocus.current = true;
578 setInputValue('');
579
580 if (onInputChange) {
581 onInputChange(event, '', 'clear');
582 }
583
584 handleValue(event, multiple ? [] : null, 'clear');
585 };
586
587 const handleKeyDown = other => event => {
588 if (focusedTag !== -1 && ['ArrowLeft', 'ArrowRight'].indexOf(event.key) === -1) {
589 setFocusedTag(-1);
590 focusTag(-1);
591 }
592
593 switch (event.key) {
594 case 'Home':
595 if (popupOpen && handleHomeEndKeys) {
596 // Prevent scroll of the page
597 event.preventDefault();
598 changeHighlightedIndex({
599 diff: 'start',
600 direction: 'next',
601 reason: 'keyboard',
602 event
603 });
604 }
605
606 break;
607
608 case 'End':
609 if (popupOpen && handleHomeEndKeys) {
610 // Prevent scroll of the page
611 event.preventDefault();
612 changeHighlightedIndex({
613 diff: 'end',
614 direction: 'previous',
615 reason: 'keyboard',
616 event
617 });
618 }
619
620 break;
621
622 case 'PageUp':
623 // Prevent scroll of the page
624 event.preventDefault();
625 changeHighlightedIndex({
626 diff: -pageSize,
627 direction: 'previous',
628 reason: 'keyboard',
629 event
630 });
631 handleOpen(event);
632 break;
633
634 case 'PageDown':
635 // Prevent scroll of the page
636 event.preventDefault();
637 changeHighlightedIndex({
638 diff: pageSize,
639 direction: 'next',
640 reason: 'keyboard',
641 event
642 });
643 handleOpen(event);
644 break;
645
646 case 'ArrowDown':
647 // Prevent cursor move
648 event.preventDefault();
649 changeHighlightedIndex({
650 diff: 1,
651 direction: 'next',
652 reason: 'keyboard',
653 event
654 });
655 handleOpen(event);
656 break;
657
658 case 'ArrowUp':
659 // Prevent cursor move
660 event.preventDefault();
661 changeHighlightedIndex({
662 diff: -1,
663 direction: 'previous',
664 reason: 'keyboard',
665 event
666 });
667 handleOpen(event);
668 break;
669
670 case 'ArrowLeft':
671 handleFocusTag(event, 'previous');
672 break;
673
674 case 'ArrowRight':
675 handleFocusTag(event, 'next');
676 break;
677
678 case 'Enter':
679 // Wait until IME is settled.
680 if (event.which === 229) {
681 break;
682 }
683
684 if (highlightedIndexRef.current !== -1 && popupOpen) {
685 const option = filteredOptions[highlightedIndexRef.current];
686 const disabled = getOptionDisabled ? getOptionDisabled(option) : false; // We don't want to validate the form.
687
688 event.preventDefault();
689
690 if (disabled) {
691 return;
692 }
693
694 selectNewValue(event, option, 'select-option'); // Move the selection to the end.
695
696 if (autoComplete) {
697 inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
698 }
699 } else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
700 if (multiple) {
701 // Allow people to add new values before they submit the form.
702 event.preventDefault();
703 }
704
705 selectNewValue(event, inputValue, 'create-option', 'freeSolo');
706 }
707
708 break;
709
710 case 'Escape':
711 if (popupOpen) {
712 // Avoid Opera to exit fullscreen mode.
713 event.preventDefault(); // Avoid the Modal to handle the event.
714
715 event.stopPropagation();
716 handleClose(event, 'escape');
717 } else if (clearOnEscape && (inputValue !== '' || multiple && value.length > 0)) {
718 // Avoid Opera to exit fullscreen mode.
719 event.preventDefault(); // Avoid the Modal to handle the event.
720
721 event.stopPropagation();
722 handleClear(event);
723 }
724
725 break;
726
727 case 'Backspace':
728 if (multiple && inputValue === '' && value.length > 0) {
729 const index = focusedTag === -1 ? value.length - 1 : focusedTag;
730 const newValue = value.slice();
731 newValue.splice(index, 1);
732 handleValue(event, newValue, 'remove-option', {
733 option: value[index]
734 });
735 }
736
737 break;
738
739 default:
740 }
741
742 if (other.onKeyDown) {
743 other.onKeyDown(event);
744 }
745 };
746
747 const handleFocus = event => {
748 setFocused(true);
749
750 if (openOnFocus && !ignoreFocus.current) {
751 handleOpen(event);
752 }
753 };
754
755 const handleBlur = event => {
756 // Ignore the event when using the scrollbar with IE 11
757 if (listboxRef.current !== null && document.activeElement === listboxRef.current.parentElement) {
758 inputRef.current.focus();
759 return;
760 }
761
762 setFocused(false);
763 firstFocus.current = true;
764 ignoreFocus.current = false;
765
766 if (debug && inputValue !== '') {
767 return;
768 }
769
770 if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
771 selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
772 } else if (autoSelect && freeSolo && inputValue !== '') {
773 selectNewValue(event, inputValue, 'blur', 'freeSolo');
774 } else if (clearOnBlur) {
775 resetInputValue(event, value);
776 }
777
778 handleClose(event, 'blur');
779 };
780
781 const handleInputChange = event => {
782 const newValue = event.target.value;
783
784 if (inputValue !== newValue) {
785 setInputValue(newValue);
786
787 if (onInputChange) {
788 onInputChange(event, newValue, 'input');
789 }
790 }
791
792 if (newValue === '') {
793 if (!disableClearable && !multiple) {
794 handleValue(event, null, 'clear');
795 }
796 } else {
797 handleOpen(event);
798 }
799 };
800
801 const handleOptionMouseOver = event => {
802 setHighlightedIndex({
803 event,
804 index: Number(event.currentTarget.getAttribute('data-option-index')),
805 reason: 'mouse'
806 });
807 };
808
809 const handleOptionTouchStart = () => {
810 isTouch.current = true;
811 };
812
813 const handleOptionClick = event => {
814 const index = Number(event.currentTarget.getAttribute('data-option-index'));
815 selectNewValue(event, filteredOptions[index], 'select-option');
816 isTouch.current = false;
817 };
818
819 const handleTagDelete = index => event => {
820 const newValue = value.slice();
821 newValue.splice(index, 1);
822 handleValue(event, newValue, 'remove-option', {
823 option: value[index]
824 });
825 };
826
827 const handlePopupIndicator = event => {
828 if (open) {
829 handleClose(event, 'toggleInput');
830 } else {
831 handleOpen(event);
832 }
833 }; // Prevent input blur when interacting with the combobox
834
835
836 const handleMouseDown = event => {
837 if (event.target.getAttribute('id') !== id) {
838 event.preventDefault();
839 }
840 }; // Focus the input when interacting with the combobox
841
842
843 const handleClick = () => {
844 inputRef.current.focus();
845
846 if (selectOnFocus && firstFocus.current && inputRef.current.selectionEnd - inputRef.current.selectionStart === 0) {
847 inputRef.current.select();
848 }
849
850 firstFocus.current = false;
851 };
852
853 const handleInputMouseDown = event => {
854 if (inputValue === '' || !open) {
855 handlePopupIndicator(event);
856 }
857 };
858
859 let dirty = freeSolo && inputValue.length > 0;
860 dirty = dirty || (multiple ? value.length > 0 : value !== null);
861 let groupedOptions = filteredOptions;
862
863 if (groupBy) {
864 // used to keep track of key and indexes in the result array
865 const indexBy = new Map();
866 let warn = false;
867 groupedOptions = filteredOptions.reduce((acc, option, index) => {
868 const group = groupBy(option);
869
870 if (acc.length > 0 && acc[acc.length - 1].group === group) {
871 acc[acc.length - 1].options.push(option);
872 } else {
873 if (process.env.NODE_ENV !== 'production') {
874 if (indexBy.get(group) && !warn) {
875 console.warn(`Material-UI: The options provided combined with the \`groupBy\` method of ${componentName} returns duplicated headers.`, 'You can solve the issue by sorting the options with the output of `groupBy`.');
876 warn = true;
877 }
878
879 indexBy.set(group, true);
880 }
881
882 acc.push({
883 key: index,
884 index,
885 group,
886 options: [option]
887 });
888 }
889
890 return acc;
891 }, []);
892 }
893
894 return {
895 getRootProps: (other = {}) => _extends({
896 'aria-owns': popupOpen ? `${id}-popup` : null,
897 role: 'combobox',
898 'aria-expanded': popupOpen
899 }, other, {
900 onKeyDown: handleKeyDown(other),
901 onMouseDown: handleMouseDown,
902 onClick: handleClick
903 }),
904 getInputLabelProps: () => ({
905 id: `${id}-label`,
906 htmlFor: id
907 }),
908 getInputProps: () => ({
909 id,
910 value: inputValue,
911 onBlur: handleBlur,
912 onFocus: handleFocus,
913 onChange: handleInputChange,
914 onMouseDown: handleInputMouseDown,
915 // if open then this is handled imperativeley so don't let react override
916 // only have an opinion about this when closed
917 'aria-activedescendant': popupOpen ? '' : null,
918 'aria-autocomplete': autoComplete ? 'both' : 'list',
919 'aria-controls': popupOpen ? `${id}-popup` : null,
920 // Disable browser's suggestion that might overlap with the popup.
921 // Handle autocomplete but not autofill.
922 autoComplete: 'off',
923 ref: inputRef,
924 autoCapitalize: 'none',
925 spellCheck: 'false'
926 }),
927 getClearProps: () => ({
928 tabIndex: -1,
929 onClick: handleClear
930 }),
931 getPopupIndicatorProps: () => ({
932 tabIndex: -1,
933 onClick: handlePopupIndicator
934 }),
935 getTagProps: ({
936 index
937 }) => ({
938 key: index,
939 'data-tag-index': index,
940 tabIndex: -1,
941 onDelete: handleTagDelete(index)
942 }),
943 getListboxProps: () => ({
944 role: 'listbox',
945 id: `${id}-popup`,
946 'aria-labelledby': `${id}-label`,
947 ref: handleListboxRef,
948 onMouseDown: event => {
949 // Prevent blur
950 event.preventDefault();
951 }
952 }),
953 getOptionProps: ({
954 index,
955 option
956 }) => {
957 const selected = (multiple ? value : [value]).some(value2 => value2 != null && getOptionSelected(option, value2));
958 const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
959 return {
960 key: index,
961 tabIndex: -1,
962 role: 'option',
963 id: `${id}-option-${index}`,
964 onMouseOver: handleOptionMouseOver,
965 onClick: handleOptionClick,
966 onTouchStart: handleOptionTouchStart,
967 'data-option-index': index,
968 'aria-disabled': disabled,
969 'aria-selected': selected
970 };
971 },
972 id,
973 inputValue,
974 value,
975 dirty,
976 popupOpen,
977 focused: focused || focusedTag !== -1,
978 anchorEl,
979 setAnchorEl,
980 focusedTag,
981 groupedOptions
982 };
983}
\No newline at end of file