UNPKG

@grafana/ui

Version:
264 lines (261 loc) • 9.19 kB
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