UNPKG

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