mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Combobox: Add clear and reset onBlur (#90943)
* Add clear and reset onBlur * use selectItem * Use downshift hooks instead * Fix Clear bug and extract i18n * Remove useMemo from story * Add loading state to many options story * Set fallback to null * Fix unused import * Use onBlur and pass it to Downshift instead
This commit is contained in:
parent
a5795ad66e
commit
ff54333881
@ -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<typeof Combobox> = (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<typeof Combobox>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
||||
function generateOptions(amount: number): Option[] {
|
||||
async function generateOptions(amount: number): Promise<Option[]> {
|
||||
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<PropsAndCustomArgs> = ({ numberOfOptions, ...args }) => {
|
||||
const [value, setValue] = useState<Value | null>(null);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
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<PropsAndCustomArgs> = ({ numberOfOptions }) => {
|
||||
const [value, setValue] = useState<Value>(manyOptions[5].value);
|
||||
const options = useMemo(() => generateOptions(numberOfOptions), [numberOfOptions]);
|
||||
return (
|
||||
<Combobox
|
||||
{...args}
|
||||
loading={isLoading}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
setValue(val.value);
|
||||
setValue(val?.value || null);
|
||||
action('onChange')(val);
|
||||
}}
|
||||
/>
|
||||
|
@ -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<InputProps, 'width' | 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange'> {
|
||||
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<HTMLInputElement>(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 (
|
||||
<div>
|
||||
<Input
|
||||
suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />}
|
||||
suffix={
|
||||
<>
|
||||
{!!value && value === selectedItem?.value && (
|
||||
<Icon
|
||||
name="times"
|
||||
className={styles.clear}
|
||||
title={t('combobox.clear.title', 'Clear value')}
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
selectItem(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
selectItem(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Icon name={isOpen ? 'search' : 'angle-down'} />
|
||||
</>
|
||||
}
|
||||
{...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,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
|
@ -77,5 +77,12 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
|
||||
top: 0,
|
||||
},
|
||||
}),
|
||||
clear: css({
|
||||
label: 'grafana-select-clear',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: theme.colors.text.primary,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -272,6 +272,11 @@
|
||||
"success": "Copied"
|
||||
}
|
||||
},
|
||||
"combobox": {
|
||||
"clear": {
|
||||
"title": "Clear value"
|
||||
}
|
||||
},
|
||||
"command-palette": {
|
||||
"action": {
|
||||
"change-theme": "Change theme...",
|
||||
|
@ -272,6 +272,11 @@
|
||||
"success": "Cőpįęđ"
|
||||
}
|
||||
},
|
||||
"combobox": {
|
||||
"clear": {
|
||||
"title": "Cľęäř väľūę"
|
||||
}
|
||||
},
|
||||
"command-palette": {
|
||||
"action": {
|
||||
"change-theme": "Cĥäʼnģę ŧĥęmę...",
|
||||
|
Loading…
Reference in New Issue
Block a user