@base-ui-components/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
105 lines (100 loc) • 3.56 kB
JavaScript
'use client';
import * as React from 'react';
import { useStableCallback } from '@base-ui-components/utils/useStableCallback';
import { AriaCombobox } from "../../combobox/root/AriaCombobox.js";
import { useCoreFilter } from "../../combobox/root/utils/useFilter.js";
import { stringifyAsLabel } from "../../utils/resolveValueLabel.js";
import { REASONS } from "../../utils/reasons.js";
/**
* Groups all parts of the autocomplete.
* Doesn't render its own HTML element.
*
* Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
*/
import { jsx as _jsx } from "react/jsx-runtime";
export function AutocompleteRoot(props) {
const {
openOnInputClick = false,
value,
defaultValue,
onValueChange,
mode = 'list',
itemToStringValue,
...other
} = props;
const enableInline = mode === 'inline' || mode === 'both';
const staticItems = mode === 'inline' || mode === 'none';
// Mirror the typed value for uncontrolled usage so we can compose the temporary
// inline input value.
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
const [inlineInputValue, setInlineInputValue] = React.useState('');
React.useEffect(() => {
if (isControlled) {
setInlineInputValue('');
}
}, [value, isControlled]);
// Compose the input value shown to the user: inline value takes precedence when present.
let resolvedInputValue;
if (enableInline && inlineInputValue !== '') {
resolvedInputValue = inlineInputValue;
} else if (isControlled) {
resolvedInputValue = value ?? '';
} else {
resolvedInputValue = internalValue;
}
const handleValueChange = useStableCallback((nextValue, eventDetails) => {
setInlineInputValue('');
if (!isControlled) {
setInternalValue(nextValue);
}
onValueChange?.(nextValue, eventDetails);
});
const collator = useCoreFilter();
const baseFilter = React.useMemo(() => {
if (other.filter) {
return other.filter;
}
return (item, query, toString) => {
return collator.contains(stringifyAsLabel(item, toString), query);
};
}, [other, collator]);
const resolvedQuery = String(isControlled ? value : internalValue).trim();
// In "both", wrap filtering to use only the typed value, ignoring the inline value.
const resolvedFilter = React.useMemo(() => {
if (mode !== 'both') {
return staticItems ? null : other.filter;
}
return (item, _query, toString) => {
return baseFilter(item, resolvedQuery, toString);
};
}, [baseFilter, mode, other.filter, resolvedQuery, staticItems]);
const handleItemHighlighted = useStableCallback((highlightedValue, eventDetails) => {
props.onItemHighlighted?.(highlightedValue, eventDetails);
if (eventDetails.reason === REASONS.pointer) {
return;
}
if (enableInline) {
if (highlightedValue == null) {
setInlineInputValue('');
} else {
setInlineInputValue(stringifyAsLabel(highlightedValue, itemToStringValue));
}
} else {
setInlineInputValue('');
}
});
return /*#__PURE__*/_jsx(AriaCombobox, {
...other,
itemToStringLabel: itemToStringValue,
openOnInputClick: openOnInputClick,
selectionMode: "none",
fillInputOnItemPress: true,
filter: resolvedFilter,
autoComplete: mode,
inputValue: resolvedInputValue,
defaultInputValue: defaultValue,
onInputValueChange: handleValueChange,
onItemHighlighted: handleItemHighlighted
});
}