UNPKG

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