@grafana/ui
Version:
Grafana Components Library
264 lines (261 loc) • 9.19 kB
JavaScript
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { cx } from '@emotion/css';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCombobox } from 'downshift';
import React__default, { useId, useMemo, useCallback } from 'react';
import { t } from '@grafana/i18n';
import { useStyles2 } from '../../themes/ThemeContext.mjs';
import { Icon } from '../Icon/Icon.mjs';
import { AutoSizeInput } from '../Input/AutoSizeInput.mjs';
import { Input } from '../Input/Input.mjs';
import { Portal } from '../Portal/Portal.mjs';
import { ComboboxList } from './ComboboxList.mjs';
import { SuffixIcon } from './SuffixIcon.mjs';
import { itemToString } from './filter.mjs';
import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles.mjs';
import { useComboboxFloat } from './useComboboxFloat.mjs';
import { useOptions } from './useOptions.mjs';
import { isNewGroup } from './utils.mjs';
const noop = () => {
};
const VIRTUAL_OVERSCAN_ITEMS = 4;
const Combobox = (props) => {
const {
options: allOptions,
onChange,
value: valueProp,
placeholder: placeholderProp,
isClearable,
// this should be default false, but TS can't infer the conditional type if you do
createCustomValue = false,
id,
width,
minWidth,
maxWidth,
"aria-labelledby": ariaLabelledBy,
"data-testid": dataTestId,
autoFocus,
onBlur,
disabled,
invalid
} = props;
const value = typeof valueProp === "object" ? valueProp == null ? void 0 : valueProp.value : valueProp;
const baseId = useId().replace(/:/g, "--");
const {
options: filteredOptions,
groupStartIndices,
updateOptions,
asyncLoading,
asyncError
} = useOptions(props.options, createCustomValue);
const isAsync = typeof allOptions === "function";
const selectedItemIndex = useMemo(() => {
if (isAsync) {
return null;
}
if (valueProp === void 0 || valueProp === null) {
return null;
}
const index = allOptions.findIndex((option) => option.value === value);
if (index === -1) {
return null;
}
return index;
}, [valueProp, allOptions, value, isAsync]);
const selectedItem = useMemo(() => {
if (valueProp === void 0 || valueProp === null) {
return null;
}
if (selectedItemIndex !== null && !isAsync) {
return allOptions[selectedItemIndex];
}
return typeof valueProp === "object" ? valueProp : { value: valueProp, label: valueProp.toString() };
}, [selectedItemIndex, isAsync, valueProp, allOptions]);
const menuId = `${baseId}-downshift-menu`;
const labelId = `${baseId}-downshift-label`;
const styles = useStyles2(getComboboxStyles);
const rangeExtractor = useCallback(
(range) => {
const startIndex = Math.max(0, range.startIndex - range.overscan);
const endIndex = Math.min(filteredOptions.length - 1, range.endIndex + range.overscan);
const rangeToReturn = Array.from({ length: endIndex - startIndex + 1 }, (_, i) => startIndex + i);
const firstDisplayedOption = filteredOptions[rangeToReturn[0]];
if (firstDisplayedOption == null ? void 0 : firstDisplayedOption.group) {
const groupStartIndex = groupStartIndices.get(firstDisplayedOption.group);
if (groupStartIndex !== void 0 && groupStartIndex < rangeToReturn[0]) {
rangeToReturn.unshift(groupStartIndex);
}
}
return rangeToReturn;
},
[filteredOptions, groupStartIndices]
);
const rowVirtualizer = useVirtualizer({
count: filteredOptions.length,
getScrollElement: () => scrollRef.current,
estimateSize: (index) => {
const firstGroupItem = isNewGroup(filteredOptions[index], index > 0 ? filteredOptions[index - 1] : void 0);
const hasDescription = "description" in filteredOptions[index];
const hasGroup = "group" in filteredOptions[index];
let itemHeight = MENU_OPTION_HEIGHT;
if (hasDescription) {
itemHeight = MENU_OPTION_HEIGHT_DESCRIPTION;
}
if (firstGroupItem && hasGroup) {
itemHeight += MENU_OPTION_HEIGHT;
}
return itemHeight;
},
overscan: VIRTUAL_OVERSCAN_ITEMS,
rangeExtractor
});
const {
isOpen,
highlightedIndex,
getInputProps,
getMenuProps,
getItemProps,
selectItem
} = useCombobox({
menuId,
labelId,
inputId: id,
items: filteredOptions,
itemToString,
selectedItem,
// Don't change downshift state in the onBlahChange handlers. Instead, use the stateReducer to make changes.
// Downshift calls change handlers on the render after so you can get sync/flickering issues if you change its state
// in them.
// Instead, stateReducer is called in the same tick as state changes, before that state is committed and rendered.
onSelectedItemChange: ({ selectedItem: selectedItem2 }) => {
if (isClearable) {
onChange(selectedItem2);
} else if (selectedItem2 !== null) {
onChange(selectedItem2);
}
},
defaultHighlightedIndex: selectedItemIndex != null ? selectedItemIndex : 0,
scrollIntoView: () => {
},
onIsOpenChange: ({ isOpen: isOpen2, inputValue }) => {
if (isOpen2 && inputValue === "") {
updateOptions(inputValue);
}
},
onHighlightedIndexChange: ({ highlightedIndex: highlightedIndex2, type }) => {
if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) {
rowVirtualizer.scrollToIndex(highlightedIndex2);
}
},
onStateChange: ({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) => {
switch (type) {
case useCombobox.stateChangeTypes.InputChange:
updateOptions(newInputValue != null ? newInputValue : "");
break;
}
},
stateReducer(state, actionAndChanges) {
let { changes } = actionAndChanges;
const menuBeingOpened = state.isOpen === false && changes.isOpen === true;
const menuBeingClosed = state.isOpen === true && changes.isOpen === false;
if (menuBeingOpened && changes.inputValue === state.inputValue) {
changes = {
...changes,
inputValue: ""
};
}
if (menuBeingClosed) {
if (changes.selectedItem) {
changes = {
...changes,
inputValue: itemToString(changes.selectedItem)
};
} else if (changes.inputValue !== "") {
changes = {
...changes,
inputValue: ""
};
}
}
return changes;
}
});
const { inputRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(filteredOptions, isOpen);
const isAutoSize = width === "auto";
const InputComponent = isAutoSize ? AutoSizeInput : Input;
const placeholder = (isOpen ? itemToString(selectedItem) : null) || placeholderProp;
const loading = props.loading || asyncLoading;
const inputSuffix = /* @__PURE__ */ jsxs(Fragment, { children: [
value !== void 0 && value === (selectedItem == null ? void 0 : selectedItem.value) && isClearable && /* @__PURE__ */ jsx(
Icon,
{
name: "times",
className: styles.clear,
title: t("combobox.clear.title", "Clear value"),
tabIndex: 0,
role: "button",
onClick: () => {
selectItem(null);
},
onKeyDown: (e) => {
if (e.key === "Enter" || e.key === " ") {
selectItem(null);
}
}
}
),
/* @__PURE__ */ jsx(SuffixIcon, { isLoading: loading || false, isOpen })
] });
const { Wrapper, wrapperProps } = isAutoSize ? {
Wrapper: "div",
wrapperProps: { className: styles.adaptToParent }
} : { Wrapper: React__default.Fragment };
return /* @__PURE__ */ jsxs(Wrapper, { ...wrapperProps, children: [
/* @__PURE__ */ jsx(
InputComponent,
{
width: isAutoSize ? void 0 : width,
...isAutoSize ? { minWidth, maxWidth } : {},
autoFocus,
onBlur,
disabled,
invalid,
className: styles.input,
suffix: inputSuffix,
...getInputProps({
ref: inputRef,
onChange: noop,
// Empty onCall to avoid TS error https://github.com/downshift-js/downshift/issues/718
"aria-labelledby": ariaLabelledBy,
// Label should be handled with the Field component
placeholder,
"data-testid": dataTestId
})
}
),
/* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
"div",
{
className: cx(styles.menu, !isOpen && styles.menuClosed),
style: floatStyles,
...getMenuProps({
ref: floatingRef,
"aria-labelledby": ariaLabelledBy
}),
children: isOpen && /* @__PURE__ */ jsx(
ComboboxList,
{
options: filteredOptions,
highlightedIndex,
selectedItems: selectedItem ? [selectedItem] : [],
scrollRef,
getItemProps,
error: asyncError
}
)
}
) })
] });
};
export { Combobox, VIRTUAL_OVERSCAN_ITEMS };
//# sourceMappingURL=Combobox.mjs.map