@material-ui/lab
Version:
Material-UI Lab - Incubator for Material-UI React components.
1,090 lines (899 loc) • 30.7 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
/* eslint-disable no-constant-condition */
import * as React from 'react';
import PropTypes from 'prop-types';
import { setRef, useEventCallback, useControlled } from '@material-ui/core/utils'; // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
// Give up on IE 11 support for this feature
function stripDiacritics(string) {
return typeof string.normalize !== 'undefined' ? string.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : string;
}
export function createFilterOptions(config = {}) {
const {
ignoreAccents = true,
ignoreCase = true,
limit,
matchFrom = 'any',
startAfter = 0,
stringify,
trim = false
} = config;
return (options, {
inputValue,
getOptionLabel
}) => {
let input = trim ? inputValue.trim() : inputValue;
if (ignoreCase) {
input = input.toLowerCase();
}
if (ignoreAccents) {
input = stripDiacritics(input);
}
if (startAfter > 0 && input.length <= startAfter) {
return [];
}
const filteredOptions = options.filter(option => {
let candidate = (stringify || getOptionLabel)(option);
if (ignoreCase) {
candidate = candidate.toLowerCase();
}
if (ignoreAccents) {
candidate = stripDiacritics(candidate);
}
return matchFrom === 'start' ? candidate.indexOf(input) === 0 : candidate.indexOf(input) > -1;
});
return typeof limit === 'number' ? filteredOptions.slice(0, limit) : filteredOptions;
};
} // To replace with .findIndex() once we stop IE 11 support.
function findIndex(array, comp) {
for (let i = 0; i < array.length; i += 1) {
if (comp(array[i])) {
return i;
}
}
return -1;
}
const defaultFilterOptions = createFilterOptions(); // Number of options to jump in list box when pageup and pagedown keys are used.
const pageSize = 5;
export default function useAutocomplete(props) {
const {
autoComplete = false,
autoHighlight = false,
autoSelect = false,
blurOnSelect = false,
clearOnEscape = false,
componentName = 'useAutocomplete',
debug = false,
defaultValue = props.multiple ? [] : null,
disableClearable = false,
disableCloseOnSelect = false,
disabledItemsFocusable = false,
disableListWrap = false,
filterOptions = defaultFilterOptions,
filterSelectedOptions = false,
freeSolo = false,
getOptionDisabled,
getOptionLabel = x => x,
getOptionSelected = (option, value) => option === value,
groupBy,
id: idProp,
includeInputInList = false,
inputValue: inputValueProp,
multiple = false,
onChange,
onClose,
onInputChange,
onOpen,
open: openProp,
openOnFocus = false,
options,
selectOnFocus = !props.freeSolo,
value: valueProp
} = props;
const [defaultId, setDefaultId] = React.useState();
const id = idProp || defaultId;
React.useEffect(() => {
// Fallback to this default id when possible.
// Use the random value for client-side rendering only.
// We can't use it server-side.
setDefaultId(`mui-autocomplete-${Math.round(Math.random() * 1e5)}`);
}, []);
const ignoreFocus = React.useRef(false);
const firstFocus = React.useRef(true);
const inputRef = React.useRef(null);
const listboxRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const [focusedTag, setFocusedTag] = React.useState(-1);
const defaultHighlighted = autoHighlight ? 0 : -1;
const highlightedIndexRef = React.useRef(defaultHighlighted);
const setHighlightedIndex = useEventCallback((index, mouse = false) => {
highlightedIndexRef.current = index; // does the index exist?
if (index === -1) {
inputRef.current.removeAttribute('aria-activedescendant');
} else {
inputRef.current.setAttribute('aria-activedescendant', `${id}-option-${index}`);
}
if (!listboxRef.current) {
return;
}
const prev = listboxRef.current.querySelector('[data-focus]');
if (prev) {
prev.removeAttribute('data-focus');
}
const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]'); // "No results"
if (!listboxNode) {
return;
}
if (index === -1) {
listboxNode.scrollTop = 0;
return;
}
const option = listboxRef.current.querySelector(`[data-option-index="${index}"]`);
if (!option) {
return;
}
option.setAttribute('data-focus', 'true'); // Scroll active descendant into view.
// Logic copied from https://www.w3.org/TR/wai-aria-practices/examples/listbox/js/listbox.js
//
// Consider this API instead once it has a better browser support:
// .scrollIntoView({ scrollMode: 'if-needed', block: 'nearest' });
if (listboxNode.scrollHeight > listboxNode.clientHeight && !mouse) {
const element = option;
const scrollBottom = listboxNode.clientHeight + listboxNode.scrollTop;
const elementBottom = element.offsetTop + element.offsetHeight;
if (elementBottom > scrollBottom) {
listboxNode.scrollTop = elementBottom - listboxNode.clientHeight;
} else if (element.offsetTop - element.offsetHeight * (groupBy ? 1.3 : 0) < listboxNode.scrollTop) {
listboxNode.scrollTop = element.offsetTop - element.offsetHeight * (groupBy ? 1.3 : 0);
}
}
});
const [value, setValue] = useControlled({
controlled: valueProp,
default: defaultValue,
name: componentName
});
const {
current: isInputValueControlled
} = React.useRef(inputValueProp != null);
const [inputValueState, setInputValue] = React.useState('');
const inputValue = isInputValueControlled ? inputValueProp : inputValueState;
const [focused, setFocused] = React.useState(false);
const resetInputValue = useEventCallback((event, newValue) => {
let newInputValue;
if (multiple) {
newInputValue = '';
} else if (newValue == null) {
newInputValue = '';
} else {
const optionLabel = getOptionLabel(newValue);
if (process.env.NODE_ENV !== 'production') {
if (typeof optionLabel !== 'string') {
const erroneousReturn = optionLabel === undefined ? 'undefined' : `${typeof optionLabel} (${optionLabel})`;
console.error(`Material-UI: the \`getOptionLabel\` method of ${componentName} returned ${erroneousReturn} instead of a string for ${JSON.stringify(newValue)}.`);
}
}
newInputValue = typeof optionLabel === 'string' ? optionLabel : '';
}
if (inputValue === newInputValue) {
return;
}
setInputValue(newInputValue);
if (onInputChange) {
onInputChange(event, newInputValue, 'reset');
}
});
React.useEffect(() => {
resetInputValue(null, value);
}, [value, resetInputValue]);
const {
current: isOpenControlled
} = React.useRef(openProp != null);
const [openState, setOpenState] = React.useState(false);
const open = isOpenControlled ? openProp : openState;
const inputValueIsSelectedValue = !multiple && value != null && inputValue === getOptionLabel(value);
let popupOpen = open;
const filteredOptions = popupOpen ? filterOptions(options.filter(option => {
if (filterSelectedOptions && (multiple ? value : [value]).some(value2 => value2 !== null && getOptionSelected(option, value2))) {
return false;
}
return true;
}), // we use the empty string to manipulate `filterOptions` to not filter any options
// i.e. the filter predicate always returns true
{
inputValue: inputValueIsSelectedValue ? '' : inputValue,
getOptionLabel
}) : [];
popupOpen = freeSolo && filteredOptions.length === 0 ? false : popupOpen;
if (process.env.NODE_ENV !== 'production') {
if (value !== null && !freeSolo && options.length > 0) {
const missingValue = (multiple ? value : [value]).filter(value2 => !options.some(option => getOptionSelected(option, value2)));
if (missingValue.length > 0) {
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'));
}
}
}
const focusTag = useEventCallback(tagToFocus => {
if (tagToFocus === -1) {
inputRef.current.focus();
} else {
anchorEl.querySelector(`[data-tag-index="${tagToFocus}"]`).focus();
}
}); // Ensure the focusedTag is never inconsistent
React.useEffect(() => {
if (multiple && focusedTag > value.length - 1) {
setFocusedTag(-1);
focusTag(-1);
}
}, [value, multiple, focusedTag, focusTag]);
function validOptionIndex(index, direction) {
if (!listboxRef.current || index === -1) {
return -1;
}
let nextFocus = index;
while (true) {
// Out of range
if (direction === 'next' && nextFocus === filteredOptions.length || direction === 'previous' && nextFocus === -1) {
return -1;
}
const option = listboxRef.current.querySelector(`[data-option-index="${nextFocus}"]`); // Same logic as MenuList.js
const nextFocusDisabled = disabledItemsFocusable ? false : option && (option.disabled || option.getAttribute('aria-disabled') === 'true');
if (option && !option.hasAttribute('tabindex') || nextFocusDisabled) {
// Move to the next element.
nextFocus += direction === 'next' ? 1 : -1;
} else {
return nextFocus;
}
}
}
const changeHighlightedIndex = useEventCallback((diff, direction) => {
if (!popupOpen) {
return;
}
const getNextIndex = () => {
const maxIndex = filteredOptions.length - 1;
if (diff === 'reset') {
return defaultHighlighted;
}
if (diff === 'start') {
return 0;
}
if (diff === 'end') {
return maxIndex;
}
const newIndex = highlightedIndexRef.current + diff;
if (newIndex < 0) {
if (newIndex === -1 && includeInputInList) {
return -1;
}
if (disableListWrap && highlightedIndexRef.current !== -1 || Math.abs(diff) > 1) {
return 0;
}
return maxIndex;
}
if (newIndex > maxIndex) {
if (newIndex === maxIndex + 1 && includeInputInList) {
return -1;
}
if (disableListWrap || Math.abs(diff) > 1) {
return maxIndex;
}
return 0;
}
return newIndex;
};
const nextIndex = validOptionIndex(getNextIndex(), direction);
setHighlightedIndex(nextIndex);
if (autoComplete && diff !== 'reset') {
if (nextIndex === -1) {
inputRef.current.value = inputValue;
} else {
const option = getOptionLabel(filteredOptions[nextIndex]);
inputRef.current.value = option; // The portion of the selected suggestion that has not been typed by the user,
// a completion string, appears inline after the input cursor in the textbox.
const index = option.toLowerCase().indexOf(inputValue.toLowerCase());
if (index === 0 && inputValue.length > 0) {
inputRef.current.setSelectionRange(inputValue.length, option.length);
}
}
}
});
React.useEffect(() => {
if (!open) {
return;
}
const valueItem = multiple ? value[0] : value; // The popup is empty
if (filteredOptions.length === 0 || valueItem == null) {
changeHighlightedIndex('reset', 'next');
return;
} // Synchronize the value with the highlighted index
if (!filterSelectedOptions && valueItem != null) {
const itemIndex = findIndex(filteredOptions, optionItem => getOptionSelected(optionItem, valueItem));
setHighlightedIndex(itemIndex);
return;
} // Keep the index in the boundaries
if (highlightedIndexRef.current >= filteredOptions.length - 1) {
setHighlightedIndex(filteredOptions.length - 1);
} // Ignore filterOptions => options, getOptionSelected, getOptionLabel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, open, filterSelectedOptions, changeHighlightedIndex, setHighlightedIndex, inputValue, multiple]);
const handleOpen = event => {
if (open) {
return;
}
if (onOpen) {
onOpen(event);
}
if (!isOpenControlled) {
setOpenState(true);
}
};
const handleClose = (event, reason) => {
if (!open) {
return;
}
if (onClose) {
onClose(event, reason);
}
if (!isOpenControlled) {
setOpenState(false);
}
};
const handleValue = (event, newValue, reason, details) => {
if (value === newValue) {
return;
}
if (onChange) {
onChange(event, newValue, reason, details);
}
setValue(newValue);
};
const selectNewValue = (event, option, reasonProp = 'select-option', origin = 'options') => {
let reason = reasonProp;
let newValue = option;
if (multiple) {
newValue = Array.isArray(value) ? [...value] : [];
if (process.env.NODE_ENV !== 'production') {
const matches = newValue.filter(val => getOptionSelected(option, val));
if (matches.length > 1) {
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'));
}
}
const itemIndex = findIndex(newValue, valueItem => getOptionSelected(option, valueItem));
if (itemIndex === -1) {
newValue.push(option);
} else if (origin !== 'freeSolo') {
newValue.splice(itemIndex, 1);
reason = 'remove-option';
}
}
resetInputValue(event, newValue);
handleValue(event, newValue, reason, {
option
});
if (!disableCloseOnSelect) {
handleClose(event, reason);
}
};
function validTagIndex(index, direction) {
if (index === -1) {
return -1;
}
let nextFocus = index;
while (true) {
// Out of range
if (direction === 'next' && nextFocus === value.length || direction === 'previous' && nextFocus === -1) {
return -1;
}
const option = anchorEl.querySelector(`[data-tag-index="${nextFocus}"]`); // Same logic as MenuList.js
if (option && (!option.hasAttribute('tabindex') || option.disabled || option.getAttribute('aria-disabled') === 'true')) {
nextFocus += direction === 'next' ? 1 : -1;
} else {
return nextFocus;
}
}
}
const handleFocusTag = (event, direction) => {
if (!multiple) {
return;
}
handleClose(event, 'toggleInput');
let nextTag = focusedTag;
if (focusedTag === -1) {
if (inputValue === '' && direction === 'previous') {
nextTag = value.length - 1;
}
} else {
nextTag += direction === 'next' ? 1 : -1;
if (nextTag < 0) {
nextTag = 0;
}
if (nextTag === value.length) {
nextTag = -1;
}
}
nextTag = validTagIndex(nextTag, direction);
setFocusedTag(nextTag);
focusTag(nextTag);
};
const handleClear = event => {
ignoreFocus.current = true;
setInputValue('');
if (onInputChange) {
onInputChange(event, '', 'clear');
}
handleValue(event, multiple ? [] : null, 'clear');
};
const handleKeyDown = other => event => {
if (focusedTag !== -1 && ['ArrowLeft', 'ArrowRight'].indexOf(event.key) === -1) {
setFocusedTag(-1);
focusTag(-1);
}
switch (event.key) {
case 'Home':
if (popupOpen) {
// Prevent scroll of the page
event.preventDefault();
changeHighlightedIndex('start', 'next');
}
break;
case 'End':
if (popupOpen) {
// Prevent scroll of the page
event.preventDefault();
changeHighlightedIndex('end', 'previous');
}
break;
case 'PageUp':
// Prevent scroll of the page
event.preventDefault();
changeHighlightedIndex(-pageSize, 'previous');
handleOpen(event);
break;
case 'PageDown':
// Prevent scroll of the page
event.preventDefault();
changeHighlightedIndex(pageSize, 'next');
handleOpen(event);
break;
case 'ArrowDown':
// Prevent cursor move
event.preventDefault();
changeHighlightedIndex(1, 'next');
handleOpen(event);
break;
case 'ArrowUp':
// Prevent cursor move
event.preventDefault();
changeHighlightedIndex(-1, 'previous');
handleOpen(event);
break;
case 'ArrowLeft':
handleFocusTag(event, 'previous');
break;
case 'ArrowRight':
handleFocusTag(event, 'next');
break;
case 'Enter':
// Wait until IME is settled.
if (event.which === 229) {
break;
}
if (highlightedIndexRef.current !== -1 && popupOpen) {
const option = filteredOptions[highlightedIndexRef.current];
const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
if (disabled) {
return;
} // We don't want to validate the form.
event.preventDefault();
selectNewValue(event, option, 'select-option'); // Move the selection to the end.
if (autoComplete) {
inputRef.current.setSelectionRange(inputRef.current.value.length, inputRef.current.value.length);
}
} else if (freeSolo && inputValue !== '' && inputValueIsSelectedValue === false) {
if (multiple) {
// Allow people to add new values before they submit the form.
event.preventDefault();
}
selectNewValue(event, inputValue, 'create-option', 'freeSolo');
}
break;
case 'Escape':
if (popupOpen) {
// Avoid Opera to exit fullscreen mode.
event.preventDefault(); // Avoid the Modal to handle the event.
event.stopPropagation();
handleClose(event, 'escape');
} else if (clearOnEscape && (inputValue !== '' || multiple && value.length > 0)) {
// Avoid Opera to exit fullscreen mode.
event.preventDefault(); // Avoid the Modal to handle the event.
event.stopPropagation();
handleClear(event);
}
break;
case 'Backspace':
if (multiple && inputValue === '' && value.length > 0) {
const index = focusedTag === -1 ? value.length - 1 : focusedTag;
const newValue = [...value];
newValue.splice(index, 1);
handleValue(event, newValue, 'remove-option', {
option: value[index]
});
}
break;
default:
}
if (other.onKeyDown) {
other.onKeyDown(event);
}
};
const handleFocus = event => {
setFocused(true);
if (openOnFocus && !ignoreFocus.current) {
handleOpen(event);
}
};
const handleBlur = event => {
// Ignore the event when using the scrollbar with IE 11
if (listboxRef.current !== null && document.activeElement === listboxRef.current.parentElement) {
inputRef.current.focus();
return;
}
setFocused(false);
firstFocus.current = true;
ignoreFocus.current = false;
if (debug && inputValue !== '') {
return;
}
if (autoSelect && highlightedIndexRef.current !== -1 && popupOpen) {
selectNewValue(event, filteredOptions[highlightedIndexRef.current], 'blur');
} else if (autoSelect && freeSolo && inputValue !== '') {
selectNewValue(event, inputValue, 'blur', 'freeSolo');
} else if (!freeSolo) {
resetInputValue(event, value);
}
handleClose(event, 'blur');
};
const handleInputChange = event => {
const newValue = event.target.value;
if (inputValue !== newValue) {
setInputValue(newValue);
if (onInputChange) {
onInputChange(event, newValue, 'input');
}
}
if (newValue === '') {
if (!disableClearable && !multiple) {
handleValue(event, null, 'clear');
}
} else {
handleOpen(event);
}
};
const handleOptionMouseOver = event => {
const index = Number(event.currentTarget.getAttribute('data-option-index'));
setHighlightedIndex(index, 'mouse');
};
const isTouch = React.useRef(false);
const handleOptionTouchStart = () => {
isTouch.current = true;
};
const handleOptionClick = event => {
const index = Number(event.currentTarget.getAttribute('data-option-index'));
selectNewValue(event, filteredOptions[index], 'select-option');
if (blurOnSelect === true || blurOnSelect === 'touch' && isTouch.current || blurOnSelect === 'mouse' && !isTouch.current) {
inputRef.current.blur();
}
isTouch.current = false;
};
const handleTagDelete = index => event => {
const newValue = [...value];
newValue.splice(index, 1);
handleValue(event, newValue, 'remove-option', {
option: value[index]
});
};
const handleListboxRef = useEventCallback(node => {
setRef(listboxRef, node);
if (!node) {
return;
} // Automatically select the first option as the listbox become visible.
if (highlightedIndexRef.current === -1 && autoHighlight) {
changeHighlightedIndex('reset', 'next');
} else {
// Restore the focus to the correct option.
setHighlightedIndex(highlightedIndexRef.current);
}
});
const handlePopupIndicator = event => {
if (open) {
handleClose(event, 'toggleInput');
} else {
handleOpen(event);
}
}; // Prevent input blur when interacting with the combobox
const handleMouseDown = event => {
if (event.target.getAttribute('id') !== id) {
event.preventDefault();
}
}; // Focus the input when interacting with the combobox
const handleClick = () => {
inputRef.current.focus();
if (selectOnFocus && firstFocus.current && inputRef.current.selectionEnd - inputRef.current.selectionStart === 0) {
inputRef.current.select();
}
firstFocus.current = false;
};
const handleInputMouseDown = event => {
if (inputValue === '') {
handlePopupIndicator(event);
}
};
let dirty = freeSolo && inputValue.length > 0;
dirty = dirty || (multiple ? value.length > 0 : value !== null);
let groupedOptions = filteredOptions;
if (groupBy) {
const result = []; // used to keep track of key and indexes in the result array
const indexByKey = new Map();
let currentResultIndex = 0;
filteredOptions.forEach(option => {
const key = groupBy(option);
if (indexByKey.get(key) === undefined) {
indexByKey.set(key, currentResultIndex);
result.push({
key,
options: []
});
currentResultIndex += 1;
}
result[indexByKey.get(key)].options.push(option);
}); // now we can add the `index` property based on the options length
let indexCounter = 0;
result.forEach(option => {
option.index = indexCounter;
indexCounter += option.options.length;
});
groupedOptions = result;
}
return {
getRootProps: (other = {}) => _extends({
'aria-owns': popupOpen ? `${id}-popup` : null,
role: 'combobox',
'aria-expanded': popupOpen
}, other, {
onKeyDown: handleKeyDown(other),
onMouseDown: handleMouseDown,
onClick: handleClick
}),
getInputLabelProps: () => ({
id: `${id}-label`,
htmlFor: id
}),
getInputProps: () => ({
id,
value: inputValue,
onBlur: handleBlur,
onFocus: handleFocus,
onChange: handleInputChange,
onMouseDown: handleInputMouseDown,
// if open then this is handled imperativeley so don't let react override
// only have an opinion about this when closed
'aria-activedescendant': popupOpen ? '' : null,
'aria-autocomplete': autoComplete ? 'both' : 'list',
'aria-controls': popupOpen ? `${id}-popup` : null,
// Disable browser's suggestion that might overlap with the popup.
// Handle autocomplete but not autofill.
autoComplete: 'off',
ref: inputRef,
autoCapitalize: 'none',
spellCheck: 'false'
}),
getClearProps: () => ({
tabIndex: -1,
onClick: handleClear
}),
getPopupIndicatorProps: () => ({
tabIndex: -1,
onClick: handlePopupIndicator
}),
getTagProps: ({
index
}) => ({
key: index,
'data-tag-index': index,
tabIndex: -1,
onDelete: handleTagDelete(index)
}),
getListboxProps: () => ({
role: 'listbox',
id: `${id}-popup`,
'aria-labelledby': `${id}-label`,
ref: handleListboxRef,
onMouseDown: event => {
// Prevent blur
event.preventDefault();
}
}),
getOptionProps: ({
index,
option
}) => {
const selected = (multiple ? value : [value]).some(value2 => value2 != null && getOptionSelected(option, value2));
const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
return {
key: index,
tabIndex: -1,
role: 'option',
id: `${id}-option-${index}`,
onMouseOver: handleOptionMouseOver,
onClick: handleOptionClick,
onTouchStart: handleOptionTouchStart,
'data-option-index': index,
'aria-disabled': disabled,
'aria-selected': selected
};
},
id,
inputValue,
value,
dirty,
popupOpen,
focused: focused || focusedTag !== -1,
anchorEl,
setAnchorEl,
focusedTag,
groupedOptions
};
}
useAutocomplete.propTypes = {
/**
* If `true`, the portion of the selected suggestion that has not been typed by the user,
* known as the completion string, appears inline after the input cursor in the textbox.
* The inline completion string is visually highlighted and has a selected state.
*/
autoComplete: PropTypes.bool,
/**
* If `true`, the first option is automatically highlighted.
*/
autoHighlight: PropTypes.bool,
/**
* If `true`, the selected option becomes the value of the input
* when the Autocomplete loses focus unless the user chooses
* a different option or changes the character string in the input.
*/
autoSelect: PropTypes.bool,
/**
* Override or extend the styles applied to the component.
* See [CSS API](#css) below for more details.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* If `true`, clear all values when the user presses escape and the popup is closed.
*/
clearOnEscape: PropTypes.bool,
/**
* The component name that is using this hook. Used for warnings.
*/
componentName: PropTypes.string,
/**
* If `true`, the popup will ignore the blur event if the input is filled.
* You can inspect the popup markup with your browser tools.
* Consider this option when you need to customize the component.
*/
debug: PropTypes.bool,
/**
* The default input value. Use when the component is not controlled.
*/
defaultValue: PropTypes.any,
/**
* If `true`, the input can't be cleared.
*/
disableClearable: PropTypes.bool,
/**
* If `true`, the popup won't close when a value is selected.
*/
disableCloseOnSelect: PropTypes.bool,
/**
* If `true`, will allow focus on disabled items.
*/
disabledItemsFocusable: PropTypes.bool,
/**
* If `true`, the list box in the popup will not wrap focus.
*/
disableListWrap: PropTypes.bool,
/**
* A filter function that determins the options that are eligible.
*
* @param {any} options The options to render.
* @param {object} state The state of the component.
* @returns {boolean}
*/
filterOptions: PropTypes.func,
/**
* If `true`, hide the selected options from the list box.
*/
filterSelectedOptions: PropTypes.bool,
/**
* If `true`, the Autocomplete is free solo, meaning that the user input is not bound to provided options.
*/
freeSolo: PropTypes.bool,
/**
* Used to determine the disabled state for a given option.
*/
getOptionDisabled: PropTypes.func,
/**
* Used to determine the string value for a given option.
* It's used to fill the input (and the list box options if `renderOption` is not provided).
*/
getOptionLabel: PropTypes.func,
/**
* If provided, the options will be grouped under the returned string.
* The groupBy value is also used as the text for group headings when `renderGroup` is not provided.
*
* @param {any} options The option to group.
* @returns {string}
*/
groupBy: PropTypes.func,
/**
* This prop is used to help implement the accessibility logic.
* If you don't provide this prop. It falls back to a randomly generated id.
*/
id: PropTypes.string,
/**
* If `true`, the highlight can move to the input.
*/
includeInputInList: PropTypes.bool,
/**
* If `true`, `value` must be an array and the menu will support multiple selections.
*/
multiple: PropTypes.bool,
/**
* Callback fired when the value changes.
*
* @param {object} event The event source of the callback
* @param {any} value
* @param {string} reason One of "create-option", "select-option", "remove-option", "blur" or "clear".
*/
onChange: PropTypes.func,
/**
* Callback fired when the popup requests to be closed.
* Use in controlled mode (see open).
*
* @param {object} event The event source of the callback.
*/
onClose: PropTypes.func,
/**
* Callback fired when the text input value changes.
*
* @param {object} event The event source of the callback.
* @param {string} value The new value of the text input.
* @param {string} reason One of "input" (user input) or "reset" (programmatic change).
*/
onInputChange: PropTypes.func,
/**
* Callback fired when the popup requests to be opened.
* Use in controlled mode (see open).
*
* @param {object} event The event source of the callback.
*/
onOpen: PropTypes.func,
/**
* Control the popup` open state.
*/
open: PropTypes.bool,
/**
* If `true`, the popup will open on input focus.
*/
openOnFocus: PropTypes.bool,
/**
* Array of options.
*/
options: PropTypes.array,
/**
* The input value.
*/
value: PropTypes.any
};