@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.
362 lines (356 loc) • 15.7 kB
JavaScript
"use strict";
'use client';
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ComboboxInput = void 0;
var React = _interopRequireWildcard(require("react"));
var _store = require("@base-ui-components/utils/store");
var _useStableCallback = require("@base-ui-components/utils/useStableCallback");
var _detectBrowser = require("@base-ui-components/utils/detectBrowser");
var _useBaseUiId = require("../../utils/useBaseUiId");
var _useRenderElement = require("../../utils/useRenderElement");
var _ComboboxRootContext = require("../root/ComboboxRootContext");
var _store2 = require("../store");
var _popupStateMapping = require("../../utils/popupStateMapping");
var _FieldRootContext = require("../../field/root/FieldRootContext");
var _constants = require("../../field/utils/constants");
var _LabelableContext = require("../../labelable-provider/LabelableContext");
var _ComboboxChipsContext = require("../chips/ComboboxChipsContext");
var _utils = require("../../floating-ui-react/utils");
var _ComboboxPositionerContext = require("../positioner/ComboboxPositionerContext");
var _createBaseUIEventDetails = require("../../utils/createBaseUIEventDetails");
var _reasons = require("../../utils/reasons");
var _DirectionContext = require("../../direction-provider/DirectionContext");
const stateAttributesMapping = {
..._popupStateMapping.pressableTriggerOpenStateMapping,
..._constants.fieldValidityMapping,
popupSide: side => side ? {
'data-popup-side': side
} : null,
listEmpty: empty => empty ? {
'data-list-empty': ''
} : null
};
/**
* A text input to search for items in the list.
* Renders an `<input>` element.
*/
const ComboboxInput = exports.ComboboxInput = /*#__PURE__*/React.forwardRef(function ComboboxInput(componentProps, forwardedRef) {
const {
render,
className,
disabled: disabledProp = false,
id: idProp,
...elementProps
} = componentProps;
const {
state: fieldState,
disabled: fieldDisabled,
setTouched,
setFocused,
validationMode,
validation
} = (0, _FieldRootContext.useFieldRootContext)();
const {
labelId
} = (0, _LabelableContext.useLabelableContext)();
const comboboxChipsContext = (0, _ComboboxChipsContext.useComboboxChipsContext)();
const positioning = (0, _ComboboxPositionerContext.useComboboxPositionerContext)(true);
const hasPositionerParent = Boolean(positioning);
const store = (0, _ComboboxRootContext.useComboboxRootContext)();
const {
filteredItems
} = (0, _ComboboxRootContext.useComboboxDerivedItemsContext)();
// `inputValue` can't be placed in the store.
// https://github.com/mui/base-ui/issues/2703
const inputValue = (0, _ComboboxRootContext.useComboboxInputValueContext)();
const id = (0, _useBaseUiId.useBaseUiId)(idProp);
const direction = (0, _DirectionContext.useDirection)();
const comboboxDisabled = (0, _store.useStore)(store, _store2.selectors.disabled);
const readOnly = (0, _store.useStore)(store, _store2.selectors.readOnly);
const name = (0, _store.useStore)(store, _store2.selectors.name);
const selectionMode = (0, _store.useStore)(store, _store2.selectors.selectionMode);
const autoHighlightMode = (0, _store.useStore)(store, _store2.selectors.autoHighlight);
const inputProps = (0, _store.useStore)(store, _store2.selectors.inputProps);
const triggerProps = (0, _store.useStore)(store, _store2.selectors.triggerProps);
const open = (0, _store.useStore)(store, _store2.selectors.open);
const mounted = (0, _store.useStore)(store, _store2.selectors.mounted);
const selectedValue = (0, _store.useStore)(store, _store2.selectors.selectedValue);
const popupSideValue = (0, _store.useStore)(store, _store2.selectors.popupSide);
const positionerElement = (0, _store.useStore)(store, _store2.selectors.positionerElement);
const autoHighlightEnabled = Boolean(autoHighlightMode);
const popupSide = mounted && positionerElement ? popupSideValue : null;
const disabled = fieldDisabled || comboboxDisabled || disabledProp;
const listEmpty = filteredItems.length === 0;
const [composingValue, setComposingValue] = React.useState(null);
const isComposingRef = React.useRef(false);
const setInputElement = (0, _useStableCallback.useStableCallback)(element => {
const isInsidePopup = hasPositionerParent || store.state.inline;
if (isInsidePopup && !store.state.hasInputValue) {
store.state.setInputValue('', (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.none));
}
store.update({
inputElement: element,
inputInsidePopup: isInsidePopup
});
});
const state = React.useMemo(() => ({
...fieldState,
open,
disabled,
readOnly,
popupSide,
listEmpty
}), [fieldState, open, disabled, readOnly, popupSide, listEmpty]);
function handleKeyDown(event) {
if (!comboboxChipsContext) {
return undefined;
}
let nextIndex;
const {
highlightedChipIndex
} = comboboxChipsContext;
if (highlightedChipIndex !== undefined) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
if (highlightedChipIndex > 0) {
nextIndex = highlightedChipIndex - 1;
} else {
nextIndex = undefined;
}
} else if (event.key === 'ArrowRight') {
event.preventDefault();
if (highlightedChipIndex < selectedValue.length - 1) {
nextIndex = highlightedChipIndex + 1;
} else {
nextIndex = undefined;
}
} else if (event.key === 'Backspace' || event.key === 'Delete') {
event.preventDefault();
// Move highlight appropriately after removal.
const computedNextIndex = highlightedChipIndex >= selectedValue.length - 1 ? selectedValue.length - 2 : highlightedChipIndex;
// If the computed index is negative, treat it as no highlight.
nextIndex = computedNextIndex >= 0 ? computedNextIndex : undefined;
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: 'keyboard'
});
}
return nextIndex;
}
// Handle navigation when no chip is highlighted
if (event.key === 'ArrowLeft' && (event.currentTarget.selectionStart ?? 0) === 0 && selectedValue.length > 0) {
event.preventDefault();
const lastChipIndex = Math.max(selectedValue.length - 1, 0);
nextIndex = lastChipIndex;
} else if (event.key === 'Backspace' && event.currentTarget.value === '' && selectedValue.length > 0) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: 'keyboard'
});
event.preventDefault();
}
return nextIndex;
}
const element = (0, _useRenderElement.useRenderElement)('input', componentProps, {
state,
ref: [forwardedRef, store.state.inputRef, setInputElement],
props: [inputProps, triggerProps, {
type: 'text',
value: componentProps.value ?? composingValue ?? inputValue,
'aria-readonly': readOnly || undefined,
'aria-labelledby': labelId,
disabled,
readOnly,
...(selectionMode === 'none' && name && {
name
}),
id,
onFocus() {
setFocused(true);
},
onBlur() {
setTouched(true);
setFocused(false);
if (validationMode === 'onBlur') {
const valueToValidate = selectionMode === 'none' ? inputValue : selectedValue;
validation.commit(valueToValidate);
}
},
onCompositionStart(event) {
if (_detectBrowser.isAndroid) {
return;
}
isComposingRef.current = true;
setComposingValue(event.currentTarget.value);
},
onCompositionEnd(event) {
isComposingRef.current = false;
const next = event.currentTarget.value;
setComposingValue(null);
store.state.setInputValue(next, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputChange, event.nativeEvent));
},
onChange(event) {
// During IME composition, avoid propagating controlled updates to prevent
// filtering the options prematurely so `Empty` won't show incorrectly.
// We can't rely on this check for Android due to how it handles composition
// events with some keyboards (e.g. Samsung keyboard with predictive text on
// treats all text as always-composing).
// https://github.com/mui/base-ui/issues/2942
if (isComposingRef.current) {
const nextVal = event.currentTarget.value;
setComposingValue(nextVal);
if (nextVal === '' && !store.state.openOnInputClick && !store.state.inputInsidePopup) {
store.state.setOpen(false, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputClear, event.nativeEvent));
}
const trimmed = nextVal.trim();
const shouldMaintainHighlight = autoHighlightEnabled && trimmed !== '';
if (!readOnly && !disabled) {
if (trimmed !== '') {
store.state.setOpen(true, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputChange, event.nativeEvent));
if (!autoHighlightEnabled) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
});
}
}
}
if (open && store.state.activeIndex !== null && !shouldMaintainHighlight) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
});
}
return;
}
store.state.setInputValue(event.currentTarget.value, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputChange, event.nativeEvent));
const empty = event.currentTarget.value === '';
const clearDetails = (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputClear, event.nativeEvent);
if (empty && !store.state.inputInsidePopup) {
if (selectionMode === 'single') {
store.state.setSelectedValue(null, clearDetails);
}
if (!store.state.openOnInputClick) {
store.state.setOpen(false, clearDetails);
}
}
const trimmed = event.currentTarget.value.trim();
if (!readOnly && !disabled) {
if (trimmed !== '') {
store.state.setOpen(true, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.inputChange, event.nativeEvent));
// When autoHighlight is enabled, keep the highlight (will be set to 0 in root).
if (!autoHighlightEnabled) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
});
}
}
}
// When the user types, ensure the list resets its highlight so that
// virtual focus returns to the input (aria-activedescendant is
// cleared).
if (open && store.state.activeIndex !== null && !autoHighlightEnabled) {
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
});
}
},
onKeyDown(event) {
if (disabled || readOnly) {
return;
}
if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
return;
}
store.state.keyboardActiveRef.current = true;
const input = event.currentTarget;
const scrollAmount = input.scrollWidth - input.clientWidth;
const isRTL = direction === 'rtl';
if (event.key === 'Home') {
(0, _utils.stopEvent)(event);
const cursor = _detectBrowser.isFirefox && isRTL ? input.value.length : 0;
input.setSelectionRange(cursor, cursor);
input.scrollLeft = 0;
return;
}
if (event.key === 'End') {
(0, _utils.stopEvent)(event);
const cursor = _detectBrowser.isFirefox && isRTL ? 0 : input.value.length;
input.setSelectionRange(cursor, cursor);
input.scrollLeft = isRTL ? -scrollAmount : scrollAmount;
return;
}
if (!mounted && event.key === 'Escape') {
const isClear = selectionMode === 'multiple' && Array.isArray(selectedValue) ? selectedValue.length === 0 : selectedValue === null;
const details = (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.escapeKey, event.nativeEvent);
const value = selectionMode === 'multiple' ? [] : null;
store.state.setInputValue('', details);
store.state.setSelectedValue(value, details);
if (!isClear && !store.state.inline && !details.isPropagationAllowed) {
event.stopPropagation();
}
return;
}
// Handle deletion when no chip is highlighted and the input is empty.
if (comboboxChipsContext && event.key === 'Backspace' && input.value === '' && comboboxChipsContext.highlightedChipIndex === undefined && Array.isArray(selectedValue) && selectedValue.length > 0) {
const newValue = selectedValue.slice(0, -1);
// If the removed item was also the active (highlighted) item, clear highlight
store.state.setIndices({
activeIndex: null,
selectedIndex: null,
type: store.state.keyboardActiveRef.current ? 'keyboard' : 'pointer'
});
store.state.setSelectedValue(newValue, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.none, event.nativeEvent));
return;
}
const nextIndex = handleKeyDown(event);
comboboxChipsContext?.setHighlightedChipIndex(nextIndex);
if (nextIndex !== undefined) {
comboboxChipsContext?.chipsRef.current[nextIndex]?.focus();
} else {
store.state.inputRef.current?.focus();
}
// event.isComposing
if (event.which === 229) {
return;
}
if (event.key === 'Enter' && open) {
const activeIndex = store.state.activeIndex;
const nativeEvent = event.nativeEvent;
if (activeIndex === null) {
// Allow form submission when no item is highlighted.
store.state.setOpen(false, (0, _createBaseUIEventDetails.createChangeEventDetails)(_reasons.REASONS.none, nativeEvent));
return;
}
(0, _utils.stopEvent)(event);
const listItem = store.state.listRef.current[activeIndex];
if (listItem) {
store.state.selectionEventRef.current = nativeEvent;
listItem.click();
store.state.selectionEventRef.current = null;
}
}
},
onPointerMove() {
store.state.keyboardActiveRef.current = false;
},
onPointerDown() {
store.state.keyboardActiveRef.current = false;
}
}, validation ? validation.getValidationProps(elementProps) : elementProps],
stateAttributesMapping
});
return element;
});
if (process.env.NODE_ENV !== "production") ComboboxInput.displayName = "ComboboxInput";