diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx index 329362149ae..05a331b7606 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx @@ -1,7 +1,7 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryFn, StoryObj } from '@storybook/react'; import { Chance } from 'chance'; -import { ComponentProps, useMemo, useState } from 'react'; +import { ComponentProps, useEffect, useState } from 'react'; import { Combobox, Option, Value } from './Combobox'; @@ -44,10 +44,7 @@ const BasicWithState: StoryFn = (args) => { {...args} value={value} onChange={(val) => { - if (!val) { - return; - } - setValue(val.value); + setValue(val?.value || null); action('onChange')(val); }} /> @@ -58,7 +55,7 @@ type Story = StoryObj; export const Basic: Story = {}; -function generateOptions(amount: number): Option[] { +async function generateOptions(amount: number): Promise { return Array.from({ length: amount }, () => ({ label: chance.name(), value: chance.guid(), @@ -66,21 +63,30 @@ function generateOptions(amount: number): Option[] { })); } -const manyOptions = generateOptions(1e5); -manyOptions.push({ label: 'Banana', value: 'banana', description: 'A yellow fruit' }); +const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...args }) => { + const [value, setValue] = useState(null); + const [options, setOptions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setTimeout(() => { + generateOptions(numberOfOptions).then((options) => { + setIsLoading(false); + setOptions(options); + setValue(options[5].value); + console.log("I've set stuff"); + }); + }, 1000); + }, [numberOfOptions]); -const ManyOptionsStory: StoryFn = ({ numberOfOptions }) => { - const [value, setValue] = useState(manyOptions[5].value); - const options = useMemo(() => generateOptions(numberOfOptions), [numberOfOptions]); return ( { - if (!val) { - return; - } - setValue(val.value); + setValue(val?.value || null); action('onChange')(val); }} /> diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index a045d938ade..6951d0dc98c 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -2,9 +2,10 @@ import { cx } from '@emotion/css'; import { autoUpdate, flip, useFloating } from '@floating-ui/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; -import { useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useRef, useState } from 'react'; import { useStyles2 } from '../../themes'; +import { t } from '../../utils/i18n'; import { Icon } from '../Icon/Icon'; import { Input, Props as InputProps } from '../Input/Input'; @@ -20,12 +21,12 @@ export type Option = { interface ComboboxProps extends Omit { onChange: (val: Option | null) => void; - value: Value; + value: Value | null; options: Option[]; } function itemToString(item: Option | null) { - return item?.label || ''; + return item?.label ?? ''; } function itemFilter(inputValue: string) { @@ -48,6 +49,7 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro const MIN_WIDTH = 400; const [items, setItems] = useState(options); const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]); + const inputRef = useRef(null); const floatingRef = useRef(null); const styles = useStyles2(getComboboxStyles); @@ -59,21 +61,35 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro overscan: 2, }); - const { getInputProps, getMenuProps, getItemProps, isOpen, highlightedIndex } = useCombobox({ - items, - itemToString, - selectedItem, - scrollIntoView: () => {}, - onInputValueChange: ({ inputValue }) => { - setItems(options.filter(itemFilter(inputValue))); - }, - onSelectedItemChange: ({ selectedItem }) => onChange(selectedItem), - onHighlightedIndexChange: ({ highlightedIndex, type }) => { - if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) { - rowVirtualizer.scrollToIndex(highlightedIndex); - } - }, - }); + const { getInputProps, getMenuProps, getItemProps, isOpen, highlightedIndex, setInputValue, selectItem } = + useCombobox({ + items, + itemToString, + selectedItem, + scrollIntoView: () => {}, + onInputValueChange: ({ inputValue }) => { + setItems(options.filter(itemFilter(inputValue))); + }, + onIsOpenChange: ({ isOpen }) => { + // Default to displaying all values when opening + if (isOpen) { + setItems(options); + return; + } + }, + onSelectedItemChange: ({ selectedItem }) => { + onChange(selectedItem); + }, + onHighlightedIndexChange: ({ highlightedIndex, type }) => { + if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) { + rowVirtualizer.scrollToIndex(highlightedIndex); + } + }, + }); + + const onBlur = useCallback(() => { + setInputValue(selectedItem?.label ?? ''); + }, [selectedItem, setInputValue]); // the order of middleware is important! const middleware = [ @@ -98,7 +114,27 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro return (
} + suffix={ + <> + {!!value && value === selectedItem?.value && ( + { + selectItem(null); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + selectItem(null); + } + }} + /> + )} + + + } {...restProps} {...getInputProps({ ref: inputRef, @@ -107,6 +143,7 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro * Downshift repo: https://github.com/downshift-js/downshift/tree/master */ onChange: () => {}, + onBlur, })} />
{ top: 0, }, }), + clear: css({ + label: 'grafana-select-clear', + cursor: 'pointer', + '&:hover': { + color: theme.colors.text.primary, + }, + }), }; }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 48ac04b212b..d38e110f275 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -272,6 +272,11 @@ "success": "Copied" } }, + "combobox": { + "clear": { + "title": "Clear value" + } + }, "command-palette": { "action": { "change-theme": "Change theme...", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index cc0649df82d..176c116f3ed 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -272,6 +272,11 @@ "success": "Cőpįęđ" } }, + "combobox": { + "clear": { + "title": "Cľęäř väľūę" + } + }, "command-palette": { "action": { "change-theme": "Cĥäʼnģę ŧĥęmę...",