UNPKG

@grafana/ui

Version:
376 lines (371 loc) • 15.5 kB
'use strict'; 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'); "use strict"; 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