@itwin/itwinui-react
Version:
A react component library for iTwinUI
450 lines (449 loc) • 14.6 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', {
value: true,
});
Object.defineProperty(exports, 'ComboBox', {
enumerable: true,
get: function () {
return ComboBox;
},
});
const _interop_require_wildcard = require('@swc/helpers/_/_interop_require_wildcard');
const _react = /*#__PURE__*/ _interop_require_wildcard._(require('react'));
const _MenuExtraContent = require('../Menu/MenuExtraContent.js');
const _SelectTag = require('../Select/SelectTag.js');
const _Text = require('../Typography/Text.js');
const _index = require('../../utils/index.js');
const _Popover = require('../Popover/Popover.js');
const _helpers = require('./helpers.js');
const _ComboBoxEndIcon = require('./ComboBoxEndIcon.js');
const _ComboBoxInput = require('./ComboBoxInput.js');
const _ComboBoxInputContainer = require('./ComboBoxInputContainer.js');
const _ComboBoxMenu = require('./ComboBoxMenu.js');
const _ComboBoxMenuItem = require('./ComboBoxMenuItem.js');
const isMultipleEnabled = (variable, multiple) =>
multiple && (Array.isArray(variable) || null == variable);
const isSingleOnChange = (onChange, multiple) => !multiple;
const getOptionId = (option, idPrefix) =>
option.id ?? `${idPrefix}-option-${option.label.replace(/\s/g, '-')}`;
const ComboBox = _react.forwardRef((props, forwardedRef) => {
let idPrefix = (0, _index.useId)();
let defaultFilterFunction = _react.useCallback(
(options, inputValue) =>
options.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase()),
),
[],
);
let {
options,
value: valueProp,
onChange,
filterFunction = defaultFilterFunction,
inputProps,
endIconProps,
dropdownMenuProps: { middleware, ...dropdownMenuProps } = {},
emptyStateMessage = 'No options found',
itemRenderer,
enableVirtualization = false,
multiple = false,
onShow: onShowProp,
onHide: onHideProp,
id = inputProps?.id ? `iui-${inputProps.id}-cb` : idPrefix,
defaultValue,
clearFilterOnOptionToggle = true,
...rest
} = props;
let inputRef = _react.useRef(null);
let menuRef = _react.useRef(null);
let onChangeProp = (0, _index.useLatestRef)(onChange);
let optionsRef = (0, _index.useLatestRef)(options);
let filterFunctionRef = (0, _index.useLatestRef)(filterFunction);
let optionsExtraInfo = _react.useMemo(() => {
let newOptionsExtraInfo = {};
options.forEach((option, index) => {
newOptionsExtraInfo[getOptionId(option, id)] = {
__originalIndex: index,
};
});
return newOptionsExtraInfo;
}, [id, options]);
let getSelectedIndexes = _react.useCallback(
(value) => {
if (void 0 === value) return;
if (!isMultipleEnabled(value, multiple))
return options.findIndex((option) => option.value === value);
{
let indexArray = [];
value?.forEach((value) => {
let indexToAdd = options.findIndex(
(option) => option.value === value,
);
if (indexToAdd > -1) indexArray.push(indexToAdd);
});
return indexArray;
}
},
[multiple, options],
);
let [selectedIndexes, setSelectedIndexes] = (0, _index.useControlledState)(
getSelectedIndexes(defaultValue) ?? (multiple ? [] : -1),
getSelectedIndexes(valueProp),
);
let previousValue = _react.useRef(valueProp);
(0, _index.useLayoutEffect)(() => {
if (valueProp !== previousValue.current) {
previousValue.current = valueProp;
if (void 0 === valueProp)
isMultipleEnabled(selectedIndexes, multiple)
? setSelectedIndexes([])
: setSelectedIndexes(-1);
}
}, [multiple, selectedIndexes, setSelectedIndexes, valueProp]);
let [isOpen, setIsOpen] = _react.useState(false);
let [focusedIndex, setFocusedIndex] = _react.useState(-1);
let onShowRef = (0, _index.useLatestRef)(onShowProp);
let onHideRef = (0, _index.useLatestRef)(onHideProp);
let show = _react.useCallback(() => {
setIsOpen(true);
onShowRef.current?.();
}, [onShowRef]);
let hide = _react.useCallback(() => {
setIsOpen(false);
onHideRef.current?.();
}, [onHideRef]);
(0, _index.useLayoutEffect)(() => {
if (isOpen) {
inputRef.current?.focus();
if (!isMultipleEnabled(selectedIndexes, multiple))
setFocusedIndex(selectedIndexes ?? -1);
} else {
setFocusedIndex(-1);
isMultipleEnabled(selectedIndexes, multiple)
? setInputValue('')
: setInputValue(
selectedIndexes >= 0
? optionsRef.current[selectedIndexes]?.label ?? ''
: '',
);
setIsInputDirty(false);
}
}, [isOpen, multiple, optionsRef, selectedIndexes]);
let previousOptions = _react.useRef(options);
_react.useEffect(() => {
if (options !== previousOptions.current) {
previousOptions.current = options;
onOptionsChange();
}
function onOptionsChange() {
isMultipleEnabled(selectedIndexes, multiple)
? setFocusedIndex(-1)
: setFocusedIndex(selectedIndexes);
if (!isMultipleEnabled(selectedIndexes, multiple) && !isOpen)
setInputValue(
selectedIndexes >= 0 ? options[selectedIndexes]?.label : '',
);
}
}, [options, isOpen, multiple, selectedIndexes]);
let [inputValue, setInputValue] = _react.useState(
inputProps?.value?.toString() ?? '',
);
let [isInputDirty, setIsInputDirty] = _react.useState(false);
let filteredOptions = _react.useMemo(() => {
if (!isInputDirty) return options;
return filterFunctionRef.current?.(options, inputValue);
}, [filterFunctionRef, inputValue, options, isInputDirty]);
let [liveRegionSelection, setLiveRegionSelection] = _react.useState('');
let handleOnInput = _react.useCallback(
(event) => {
let { value } = event.currentTarget;
setInputValue(value);
show();
setIsInputDirty(true);
if (-1 != focusedIndex) setFocusedIndex(-1);
inputProps?.onChange?.(event);
},
[focusedIndex, inputProps, show],
);
let isMenuItemSelected = _react.useCallback(
(index) => {
if (isMultipleEnabled(selectedIndexes, multiple))
return selectedIndexes.includes(index);
return selectedIndexes === index;
},
[multiple, selectedIndexes],
);
let selectedChangeHandler = _react.useCallback(
(__originalIndex, action) => {
if (!isMultipleEnabled(selectedIndexes, multiple)) return;
if ('added' === action) return [...selectedIndexes, __originalIndex];
return selectedIndexes?.filter((index) => index !== __originalIndex);
},
[selectedIndexes, multiple],
);
let onChangeHandler = _react.useCallback(
(__originalIndex, actionType, newSelectedIndexes) => {
if (isSingleOnChange(onChangeProp.current, multiple))
onChangeProp.current?.(optionsRef.current[__originalIndex]?.value);
else
actionType &&
newSelectedIndexes &&
onChangeProp.current?.(
newSelectedIndexes?.map(
(index) => optionsRef.current[index]?.value,
),
{
value: optionsRef.current[__originalIndex]?.value,
type: actionType,
},
);
},
[multiple, onChangeProp, optionsRef],
);
let handleOptionSelection = _react.useCallback(
(__originalIndex) => {
inputRef.current?.focus({
preventScroll: true,
});
if (optionsRef.current[__originalIndex]?.disabled) return;
if (multiple) {
let actionType = isMenuItemSelected(__originalIndex)
? 'removed'
: 'added';
let newSelectedIndexes = selectedChangeHandler(
__originalIndex,
actionType,
);
if (null == newSelectedIndexes) return;
setSelectedIndexes(newSelectedIndexes);
onChangeHandler(__originalIndex, actionType, newSelectedIndexes);
setLiveRegionSelection(
newSelectedIndexes
.map((item) => optionsRef.current[item]?.label)
.filter(Boolean)
.join(', '),
);
if (clearFilterOnOptionToggle) {
setInputValue('');
setIsInputDirty(false);
}
} else {
setSelectedIndexes(__originalIndex);
hide();
onChangeHandler(__originalIndex);
}
},
[
optionsRef,
multiple,
isMenuItemSelected,
selectedChangeHandler,
setSelectedIndexes,
onChangeHandler,
clearFilterOnOptionToggle,
hide,
],
);
let getMenuItem = _react.useCallback(
(option, filteredIndex) => {
let optionId = getOptionId(option, id);
let { __originalIndex } = optionsExtraInfo[optionId];
let { icon, startIcon: startIconProp, label, ...restOptions } = option;
let startIcon = startIconProp ?? icon;
let customItem = itemRenderer
? itemRenderer(option, {
isFocused: focusedIndex === __originalIndex,
isSelected: isMenuItemSelected(__originalIndex),
index: __originalIndex,
id: optionId,
})
: null;
return customItem
? _react.cloneElement(customItem, {
onClick: (e) => {
handleOptionSelection(__originalIndex);
customItem.props.onClick?.(e);
},
focused: focusedIndex === __originalIndex,
'data-iui-index': __originalIndex,
'data-iui-filtered-index': filteredIndex,
ref: (0, _index.mergeRefs)(customItem.props.ref, (el) => {
if (!enableVirtualization && focusedIndex === __originalIndex)
el?.scrollIntoView({
block: 'nearest',
});
}),
})
: _react.createElement(
_ComboBoxMenuItem.ComboBoxMenuItem,
{
key: optionId,
id: optionId,
startIcon: startIcon,
...restOptions,
isSelected: isMenuItemSelected(__originalIndex),
onClick: () => {
handleOptionSelection(__originalIndex);
},
index: __originalIndex,
'data-iui-filtered-index': filteredIndex,
},
label,
);
},
[
enableVirtualization,
focusedIndex,
id,
isMenuItemSelected,
itemRenderer,
handleOptionSelection,
optionsExtraInfo,
],
);
let emptyContent = _react.useMemo(
() =>
_react.createElement(
_react.Fragment,
null,
_react.isValidElement(emptyStateMessage)
? emptyStateMessage
: _react.createElement(
_MenuExtraContent.MenuExtraContent,
null,
_react.createElement(
_Text.Text,
{
isMuted: true,
},
emptyStateMessage,
),
),
),
[emptyStateMessage],
);
let popover = (0, _Popover.usePopover)({
visible: isOpen,
onVisibleChange: (open) => (open ? show() : hide()),
matchWidth: true,
middleware: {
size: {
maxHeight: 'var(--iui-menu-max-height)',
},
...middleware,
},
closeOnOutsideClick: true,
interactions: {
click: false,
focus: true,
},
});
return _react.createElement(
_helpers.ComboBoxRefsContext.Provider,
{
value: _react.useMemo(
() => ({
inputRef,
menuRef,
optionsExtraInfo,
}),
[optionsExtraInfo],
),
},
_react.createElement(
_helpers.ComboBoxStateContext.Provider,
{
value: _react.useMemo(
() => ({
id,
isOpen,
focusedIndex,
setFocusedIndex,
onClickHandler: handleOptionSelection,
enableVirtualization,
filteredOptions,
getMenuItem,
multiple,
popover,
show,
hide,
}),
[
enableVirtualization,
filteredOptions,
focusedIndex,
getMenuItem,
handleOptionSelection,
hide,
id,
isOpen,
multiple,
popover,
show,
],
),
},
_react.createElement(
_ComboBoxInputContainer.ComboBoxInputContainer,
{
ref: forwardedRef,
disabled: inputProps?.disabled,
...rest,
},
_react.createElement(_ComboBoxInput.ComboBoxInput, {
value: inputValue,
disabled: inputProps?.disabled,
...inputProps,
onChange: handleOnInput,
'aria-describedby': [
multiple ? `${id}-selected-live` : void 0,
inputProps?.['aria-describedby'],
]
.filter(Boolean)
.join(' '),
selectTags: isMultipleEnabled(selectedIndexes, multiple)
? selectedIndexes
?.map((index) => {
let option = options[index];
let optionId = getOptionId(option, id);
let { __originalIndex } = optionsExtraInfo[optionId];
return _react.createElement(_SelectTag.SelectTag, {
key: option.label,
label: option.label,
onRemove: inputProps?.disabled
? void 0
: () => {
handleOptionSelection(__originalIndex);
hide();
},
});
})
.filter(Boolean)
: void 0,
}),
_react.createElement(_ComboBoxEndIcon.ComboBoxEndIcon, {
...endIconProps,
disabled: inputProps?.disabled,
isOpen: isOpen,
}),
multiple
? _react.createElement(_index.AutoclearingHiddenLiveRegion, {
text: liveRegionSelection,
id: `${id}-selected-live`,
})
: null,
),
_react.createElement(
_ComboBoxMenu.ComboBoxMenu,
{
as: 'div',
...dropdownMenuProps,
},
filteredOptions.length > 0 && !enableVirtualization
? filteredOptions.map(getMenuItem)
: emptyContent,
),
),
);
});
if ('development' === process.env.NODE_ENV) ComboBox.displayName = 'ComboBox';