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 e93dcdf4128..a0bbf86f2ff 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx @@ -97,9 +97,9 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...arg loading={isLoading} options={options} value={value} - onChange={(val) => { - setValue(val?.value || null); - action('onChange')(val); + onChange={(opt) => { + setValue(opt?.value || null); + action('onChange')(opt); }} /> ); diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 761adc97c13..b91cde4a618 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -79,7 +79,7 @@ describe('Combobox', () => { }); it('clears selected value', async () => { - render(); + render(); expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument(); const input = screen.getByRole('combobox'); diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index f3739086a24..3fe19ab9ac7 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -23,10 +23,12 @@ interface ComboboxProps onChange: (val: Option | null) => void; value: Value | null; options: Option[]; + isClearable?: boolean; + createCustomValue?: boolean; } function itemToString(item: Option | null) { - return item?.label ?? ''; + return item?.label ?? item?.value?.toString() ?? ''; } function itemFilter(inputValue: string) { @@ -51,13 +53,44 @@ const INDEX_WIDTH_CALCULATION = 100; // A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated. const WIDTH_MULTIPLIER = 7.3; -export const Combobox = ({ options, onChange, value, id, ...restProps }: ComboboxProps) => { +export const Combobox = ({ + options, + onChange, + value, + isClearable = false, + createCustomValue = false, + id, + ...restProps +}: ComboboxProps) => { const [items, setItems] = useState(options); - const selectedItemIndex = useMemo( - () => options.findIndex((option) => option.value === value) || null, - [options, value] - ); - const selectedItem = selectedItemIndex ? options[selectedItemIndex] : null; + + const selectedItemIndex = useMemo(() => { + if (value === null) { + return null; + } + + const index = options.findIndex((option) => option.value === value); + if (index === -1) { + return null; + } + + return index; + }, [options, value]); + + const selectedItem = useMemo(() => { + if (selectedItemIndex) { + return options[selectedItemIndex]; + } + + // Custom value + if (value !== null) { + return { + label: value.toString(), + value, + }; + } + return null; + }, [selectedItemIndex, options, value]); const inputRef = useRef(null); const floatingRef = useRef(null); @@ -82,18 +115,34 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo isOpen, highlightedIndex, setInputValue, - selectItem, openMenu, closeMenu, + selectItem, } = useCombobox({ inputId: id, items, itemToString, selectedItem, + onSelectedItemChange: ({ selectedItem, inputValue }) => { + onChange(selectedItem); + }, defaultHighlightedIndex: selectedItemIndex ?? undefined, scrollIntoView: () => {}, onInputValueChange: ({ inputValue }) => { - setItems(options.filter(itemFilter(inputValue))); + const filteredItems = options.filter(itemFilter(inputValue)); + if (createCustomValue && inputValue && filteredItems.findIndex((opt) => opt.label === inputValue) === -1) { + setItems([ + ...filteredItems, + { + label: inputValue, + value: inputValue, + description: t('combobox.custom-value.create', 'Create custom value'), + }, + ]); + return; + } else { + setItems(filteredItems); + } }, onIsOpenChange: ({ isOpen }) => { // Default to displaying all values when opening @@ -102,9 +151,6 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo return; } }, - onSelectedItemChange: ({ selectedItem }) => { - onChange(selectedItem); - }, onHighlightedIndexChange: ({ highlightedIndex, type }) => { if (type !== useCombobox.stateChangeTypes.MenuMouseLeave) { rowVirtualizer.scrollToIndex(highlightedIndex); @@ -113,8 +159,8 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo }); const onBlur = useCallback(() => { - setInputValue(selectedItem?.label ?? ''); - }, [selectedItem, setInputValue]); + setInputValue(selectedItem?.label ?? value?.toString() ?? ''); + }, [selectedItem, setInputValue, value]); // the order of middleware is important! const middleware = [ @@ -147,7 +193,7 @@ export const Combobox = ({ options, onChange, value, id, ...restProps }: Combobo - {!!value && value === selectedItem?.value && ( + {!!value && value === selectedItem?.value && isClearable && ( { return (