@grafana/ui
Version:
Grafana Components Library
376 lines (371 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var jsxRuntime = require('react/jsx-runtime');
var css = require('@emotion/css');
var downshift = require('downshift');
var React = require('react');
var i18n = require('@grafana/i18n');
var ThemeContext = require('../../themes/ThemeContext.cjs');
var FieldContext = require('../Forms/FieldContext.cjs');
var Icon = require('../Icon/Icon.cjs');
var Box = require('../Layout/Box/Box.cjs');
var Portal = require('../Portal/Portal.cjs');
var Text = require('../Text/Text.cjs');
var Tooltip = require('../Tooltip/Tooltip.cjs');
var ComboboxList = require('./ComboboxList.cjs');
var SuffixIcon = require('./SuffixIcon.cjs');
var ValuePill = require('./ValuePill.cjs');
var filter = require('./filter.cjs');
var getComboboxStyles = require('./getComboboxStyles.cjs');
var getMultiComboboxStyles = require('./getMultiComboboxStyles.cjs');
var types = require('./types.cjs');
var useComboboxFloat = require('./useComboboxFloat.cjs');
var useMeasureMulti = require('./useMeasureMulti.cjs');
var useMultiInputAutoSize = require('./useMultiInputAutoSize.cjs');
var useOptions = require('./useOptions.cjs');
;
const MultiCombobox = (props) => {
const {
placeholder,
onChange,
value,
width,
enableAllOption,
invalid: invalidProp,
disabled: disabledProp,
loading: loadingProp,
minWidth,
maxWidth,
isClearable,
createCustomValue = false,
customValueDescription,
"aria-labelledby": ariaLabelledBy,
"data-testid": dataTestId,
prefixIcon,
id: idProp,
options: optionsProp
} = props;
const styles = ThemeContext.useStyles2(getComboboxStyles.getComboboxStyles);
const [inputValue, setInputValue] = React.useState("");
const fieldContext = FieldContext.useFieldContext();
const id = idProp != null ? idProp : fieldContext.id;
const disabled = disabledProp != null ? disabledProp : fieldContext.disabled;
const invalid = invalidProp != null ? invalidProp : fieldContext.invalid;
const ariaDescribedBy = fieldContext["aria-describedby"];
const {
options: baseOptions,
updateOptions,
asyncLoading,
asyncError
} = useOptions.useOptions(optionsProp, createCustomValue, customValueDescription);
const loading = loadingProp || fieldContext.loading || asyncLoading;
const selectedItems = React.useMemo(() => {
if (!value) {
return [];
}
return getSelectedItemsFromValue(value, typeof optionsProp !== "function" ? optionsProp : baseOptions);
}, [value, optionsProp, baseOptions]);
const allOptionItem = React.useMemo(() => {
const isFiltered = inputValue !== "";
const realBaseOptions = baseOptions.filter((opt) => !opt.infoOption);
let label;
if (isFiltered) {
const anyFilteredSelected = realBaseOptions.some((opt) => selectedItems.some((s) => s.value === opt.value));
label = anyFilteredSelected ? i18n.t("multicombobox.all.title-deselect-filtered", "Deselect all (filtered)") : i18n.t("multicombobox.all.title-select-filtered", "Select all (filtered)");
} else {
label = selectedItems.length > 0 ? i18n.t("multicombobox.all.title-deselect", "Deselect all") : i18n.t("multicombobox.all.title-select", "Select all");
}
return { label, value: types.ALL_OPTION_VALUE };
}, [inputValue, selectedItems, baseOptions]);
const options = React.useMemo(() => {
const addAllOption = enableAllOption && baseOptions.length > 1;
return addAllOption ? [allOptionItem, ...baseOptions] : baseOptions;
}, [baseOptions, enableAllOption, allOptionItem]);
const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti.useMeasureMulti(
selectedItems,
width,
disabled
);
const isOptionSelected = React.useCallback(
(item) => selectedItems.some((opt) => opt.value === item.value),
[selectedItems]
);
const handleInputChange = React.useCallback((event) => {
setInputValue(event.target.value);
}, []);
const { getSelectedItemProps, getDropdownProps, setSelectedItems, addSelectedItem, removeSelectedItem, reset } = downshift.useMultipleSelection({
selectedItems,
// initially selected items,
onStateChange: ({ type, selectedItems: newSelectedItems }) => {
switch (type) {
case downshift.useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
case downshift.useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case downshift.useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case downshift.useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
case downshift.useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem:
case downshift.useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems:
case downshift.useMultipleSelection.stateChangeTypes.FunctionReset:
onChange(newSelectedItems != null ? newSelectedItems : []);
break;
default:
break;
}
},
stateReducer: (_state, actionAndChanges) => {
const { changes } = actionAndChanges;
return {
...changes,
/**
* TODO: Fix Hack!
* This prevents the menu from closing when the user unselects an item in the dropdown at the expense
* of breaking keyboard navigation.
*
* Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item
* in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed.
* This only seems to happen when you deselect the last item in the selectedItems list.
*
* Check out:
* - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75
* - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72
*
* Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes)
* and prevents the if statement in useMultipleSelection from focusing anything.
*/
activeIndex: -999
};
}
});
const { isOpen, highlightedIndex, getMenuProps, getInputProps, getItemProps, getToggleButtonProps } = downshift.useCombobox({
items: options,
itemToString: filter.itemToString,
inputId: id,
inputValue,
selectedItem: null,
isItemDisabled: (item) => !!(item == null ? void 0 : item.infoOption),
stateReducer: (state, actionAndChanges) => {
const { type } = actionAndChanges;
let { changes } = actionAndChanges;
const menuBeingOpened = state.isOpen === false && changes.isOpen === true;
if (menuBeingOpened && changes.inputValue === state.inputValue) {
changes = {
...changes,
inputValue: ""
};
}
switch (type) {
case downshift.useCombobox.stateChangeTypes.InputKeyDownEnter:
case downshift.useCombobox.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true,
highlightedIndex: state.highlightedIndex
};
case downshift.useCombobox.stateChangeTypes.InputBlur:
setInputValue("");
default:
return changes;
}
},
onIsOpenChange: ({ isOpen: isOpen2, inputValue: inputValue2 }) => {
if (isOpen2 && inputValue2 === "") {
updateOptions(inputValue2);
}
},
onStateChange: ({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) => {
switch (type) {
case downshift.useCombobox.stateChangeTypes.InputKeyDownEnter:
case downshift.useCombobox.stateChangeTypes.ItemClick:
if (newSelectedItem == null ? void 0 : newSelectedItem.infoOption) {
break;
}
if ((newSelectedItem == null ? void 0 : newSelectedItem.value) === types.ALL_OPTION_VALUE) {
const isFiltered = inputValue !== "";
const realOptions = options.slice(1).filter((option) => !option.infoOption);
if (isFiltered) {
const filteredSet = new Set(realOptions.map((item) => item.value));
const anyFilteredSelected = selectedItems.some((item) => filteredSet.has(item.value));
if (anyFilteredSelected) {
setSelectedItems(selectedItems.filter((item) => !filteredSet.has(item.value)));
} else {
setSelectedItems([.../* @__PURE__ */ new Set([...selectedItems, ...realOptions])]);
}
} else {
setSelectedItems(selectedItems.length > 0 ? [] : realOptions);
}
} else if (newSelectedItem && isOptionSelected(newSelectedItem)) {
const itemToRemove = selectedItems.find((item) => item.value === newSelectedItem.value);
if (itemToRemove) {
removeSelectedItem(itemToRemove);
}
} else if (newSelectedItem) {
addSelectedItem(newSelectedItem);
}
break;
case downshift.useCombobox.stateChangeTypes.InputChange:
updateOptions(newInputValue != null ? newInputValue : "");
break;
default:
break;
}
}
});
const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat.useComboboxFloat(options, isOpen);
const multiStyles = ThemeContext.useStyles2(
getMultiComboboxStyles.getMultiComboboxStyles,
isOpen,
invalid,
disabled,
width,
minWidth,
maxWidth,
isClearable
);
const visibleItems = isOpen ? selectedItems.slice(0, useMeasureMulti.MAX_SHOWN_ITEMS) : selectedItems.slice(0, shownItems);
const { inputRef, inputWidth } = useMultiInputAutoSize.useMultiInputAutoSize(inputValue);
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: multiStyles.container, ref: containerRef, children: [
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: css.cx(multiStyles.wrapper, { [multiStyles.disabled]: disabled }), ref: measureRef, children: [
prefixIcon && /* @__PURE__ */ jsxRuntime.jsx(Box.Box, { marginLeft: 0.5, children: /* @__PURE__ */ jsxRuntime.jsx(Text.Text, { color: "secondary", children: /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { name: prefixIcon }) }) }),
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: multiStyles.pillWrapper, children: [
visibleItems.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
ValuePill.ValuePill,
{
disabled,
onRemove: () => {
removeSelectedItem(item);
},
...getSelectedItemProps({ selectedItem: item, index }),
children: filter.itemToString(item)
},
`${item.value}${index}`
)),
selectedItems.length > visibleItems.length && /* @__PURE__ */ jsxRuntime.jsxs(Box.Box, { display: "flex", direction: "row", marginLeft: 0.5, gap: 1, ref: counterMeasureRef, children: [
/* @__PURE__ */ jsxRuntime.jsx(Text.Text, { children: "..." }),
/* @__PURE__ */ jsxRuntime.jsx(
Tooltip.Tooltip,
{
interactive: true,
content: /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: selectedItems.slice(visibleItems.length).map((item) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: filter.itemToString(item) }, item.value)) }),
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: multiStyles.restNumber, children: selectedItems.length - visibleItems.length })
}
)
] }),
/* @__PURE__ */ jsxRuntime.jsx(
"input",
{
className: multiStyles.input,
...getInputProps({
...getDropdownProps({
disabled,
preventKeyAction: isOpen,
placeholder: visibleItems.length === 0 ? placeholder : "",
ref: inputRef,
style: { width: inputWidth }
}),
"aria-describedby": ariaDescribedBy,
// Description should be handled with the Field component
"aria-labelledby": ariaLabelledBy,
// Label should be handled with the Field component
"data-testid": dataTestId,
onChange: handleInputChange,
onKeyDown: (event) => {
if (event.key === "Escape" && isOpen) {
event.stopPropagation();
}
}
})
}
),
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: multiStyles.suffix, ref: suffixMeasureRef, children: [
isClearable && selectedItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
Icon.Icon,
{
name: "times",
className: styles.clear,
title: i18n.t("multicombobox.clear.title", "Clear all"),
tabIndex: 0,
role: "button",
onClick: (e) => {
e.stopPropagation();
reset();
},
onKeyDown: (e) => {
if (e.key === "Enter" || e.key === " ") {
reset();
}
}
}
),
/* @__PURE__ */ jsxRuntime.jsx(
SuffixIcon.SuffixIcon,
{
isLoading: loading || false,
isOpen,
...getToggleButtonProps({ disabled, role: "button" })
}
)
] })
] })
] }),
/* @__PURE__ */ jsxRuntime.jsx(Portal.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
"div",
{
className: css.cx(styles.menu, !isOpen && styles.menuClosed),
style: {
...floatStyles,
width: floatStyles.width + 24,
// account for checkbox
pointerEvents: "auto"
// Override container's pointer-events: none
},
...getMenuProps({ ref: floatingRef }),
children: isOpen && /* @__PURE__ */ jsxRuntime.jsx(
ComboboxList.ComboboxList,
{
loading,
options,
highlightedIndex,
selectedItems,
scrollRef,
getItemProps,
enableAllOption,
isMultiSelect: true,
error: asyncError
}
)
}
) })
] });
};
function getSelectedItemsFromValue(value, options) {
if (isComboboxOptions(value)) {
return value;
}
const valueMap = /* @__PURE__ */ new Map();
let index = 0;
for (const val of value) {
if (!valueMap.has(val)) {
valueMap.set(val, index);
index++;
}
}
const resultingItems = [];
for (const option of options) {
const index2 = valueMap.get(option.value);
if (index2 !== void 0) {
resultingItems[index2] = option;
valueMap.delete(option.value);
}
if (valueMap.size === 0) {
break;
}
}
for (const [val, index2] of valueMap) {
resultingItems[index2] = { value: val };
}
return resultingItems;
}
function isComboboxOptions(value) {
return typeof value[0] === "object";
}
exports.MultiCombobox = MultiCombobox;
//# sourceMappingURL=MultiCombobox.cjs.map