@mui/material
Version:
Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.
181 lines (168 loc) • 7.92 kB
JavaScript
;
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useOptionValue;
var React = _interopRequireWildcard(require("react"));
var _validateOptionValues = _interopRequireWildcard(require("./validateOptionValues"));
// The runtime helper handles all modes; public props enforce the narrower per-mode value types.
const defaultGetOptionValue = option => option;
const defaultResolveFromOptions = option => ({
option
});
/**
* Maps and matches Autocomplete values using cached option lookups and custom equality when supplied.
* Current options take precedence over retained selections created by `filterOptions`.
*
* Call `rememberSelectedOption` before resetting the input or publishing an option selection.
* Retained options survive popup closure and are discarded once no longer selected.
* Matches wrap the original option or free-solo string; unresolved mapped values return `null`.
*/
function useOptionValue({
options,
getOptionValue: getOptionValueProp,
isOptionEqualToValue: isOptionEqualToValueProp,
freeSolo,
multiple,
value
}) {
const getOptionValue = getOptionValueProp ?? defaultGetOptionValue;
const hasOptionValueMapping = getOptionValueProp !== undefined;
const reportedErrorMessages = React.useMemo(() => new Set(), []);
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
// Mapped values identify options and must therefore be valid, unique primitive keys.
(0, _validateOptionValues.default)({
options,
freeSolo,
getOptionValueProp,
duplicatedErrorMessages: reportedErrorMessages
});
}, [options, freeSolo, getOptionValueProp, reportedErrorMessages]);
}
// Determines if an option is equal to a value, considering custom equality and free-solo scenarios.
const isOptionEqualToValue = React.useCallback((option, value) => {
// With value mapping, strings are reserved for free-solo values and cannot identify options.
if (getOptionValueProp !== undefined && freeSolo && typeof value === 'string') {
return false;
}
// Custom equality takes precedence over comparing the option's mapped value.
if (isOptionEqualToValueProp) {
return isOptionEqualToValueProp(option, value);
}
return getOptionValue(option) === value;
}, [freeSolo, getOptionValue, getOptionValueProp, isOptionEqualToValueProp]);
// Creates a lookup map from option values to their corresponding options for efficient resolution.
const optionValueMap = React.useMemo(() => {
if (getOptionValueProp === undefined || isOptionEqualToValueProp) {
return null;
}
return new Map(options.map(option => [getOptionValueProp(option), option]));
}, [getOptionValueProp, isOptionEqualToValueProp, options]);
// Wrap matches so null/undefined options remain distinct from lookup misses.
const resolveFromOptions = React.useMemo(() => {
if (!hasOptionValueMapping) {
// Without value mapping, selected values are already options.
return defaultResolveFromOptions;
}
if (isOptionEqualToValueProp) {
const resolvedOptions = new Map();
// Custom equality defines matching behavior, so resolve the first matching option.
return value => {
if (freeSolo && typeof value === 'string') {
// Strings always represent free-solo values when mapping is enabled.
return {
option: value
};
}
// Cache misses as well as matches so unchanged selections do not rescan the options.
if (!resolvedOptions.has(value)) {
const index = options.findIndex(option => isOptionEqualToValueProp(option, value));
resolvedOptions.set(value, index === -1 ? null : {
option: options[index]
});
}
return resolvedOptions.get(value);
};
}
// Default equality uses mapped values as keys, so resolve them through the lookup map.
return value => {
if (freeSolo && typeof value === 'string') {
// Check before the map because a mapped string and free-solo text are indistinguishable.
return {
option: value
};
}
return optionValueMap.has(value) ? {
option: optionValueMap.get(value)
} : null;
};
}, [freeSolo, hasOptionValueMapping, isOptionEqualToValueProp, optionValueMap, options]);
// Maintains a ref to selected options generated by filterOptions to ensure they persist across renders.
const selectedGeneratedOptionsRef = React.useRef([]);
const getOptionFromValue = React.useCallback(value => {
const resolved = resolveFromOptions(value);
if (resolved !== null) {
return resolved;
}
// A selected option created by filterOptions must survive popup closure and later searches.
// Keep this fallback outside the options cache so cached misses cannot hide new selections.
const index = selectedGeneratedOptionsRef.current.findIndex(selectedOption => isOptionEqualToValue(selectedOption, value));
return index === -1 ? null : {
option: selectedGeneratedOptionsRef.current[index]
};
}, [resolveFromOptions, isOptionEqualToValue]);
const selectedValues = React.useMemo(() => {
// `multiple` distinguishes a list of selections from an option that is itself an array.
if (multiple) {
return value;
}
if (value != null) {
return [value];
}
return [];
}, [multiple, value]);
// Default equality supports set lookups; custom equality must compare options and values.
const selectedValuesSet = React.useMemo(() => {
if (isOptionEqualToValueProp || selectedValues.length === 0) {
return null;
}
return new Set(hasOptionValueMapping && freeSolo ? selectedValues.filter(selectedValue => typeof selectedValue !== 'string') : selectedValues);
}, [freeSolo, hasOptionValueMapping, isOptionEqualToValueProp, selectedValues]);
const isOptionSelected = React.useCallback(option => {
if (selectedValuesSet) {
return selectedValuesSet.has(getOptionValue(option));
}
return selectedValues.some(value2 => value2 != null && isOptionEqualToValue(option, value2));
}, [getOptionValue, isOptionEqualToValue, selectedValues, selectedValuesSet]);
// Cleans up selected generated options that are no longer valid after each render.
React.useEffect(() => {
// Also discard attempted selections rejected by a controlled value after the render commits.
if (selectedGeneratedOptionsRef.current.length > 0) {
selectedGeneratedOptionsRef.current = hasOptionValueMapping ? selectedGeneratedOptionsRef.current.filter(isOptionSelected) : [];
}
});
// Remembers a selected option that was generated by filterOptions but not present in the current options.
const rememberSelectedOption = React.useCallback((option, optionValue) => {
if (process.env.NODE_ENV !== 'production') {
if (hasOptionValueMapping) {
// filterOptions can synthesize options that were not covered by options-prop validation.
(0, _validateOptionValues.validateOptionValue)(optionValue, freeSolo, reportedErrorMessages);
}
}
if (!hasOptionValueMapping || resolveFromOptions(optionValue) !== null) {
return;
}
selectedGeneratedOptionsRef.current = [...selectedGeneratedOptionsRef.current.filter(selectedOption => isOptionSelected(selectedOption) && !isOptionEqualToValue(selectedOption, optionValue)), option];
}, [hasOptionValueMapping, resolveFromOptions, isOptionSelected, isOptionEqualToValue, freeSolo, reportedErrorMessages]);
return {
getOptionValue,
isOptionEqualToValue,
isOptionSelected,
getOptionFromValue,
rememberSelectedOption
};
}