mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 23:34:15 +08:00
New Select: Use virtual list (#89290)
* use react-virtual * Render story with 100k items * Dyanmic height and TanStack * Remove weird item * Add numberOfOptions to story * Update class name * Update class name
This commit is contained in:
@ -61,6 +61,7 @@
|
||||
"@react-aria/focus": "3.17.1",
|
||||
"@react-aria/overlays": "3.22.1",
|
||||
"@react-aria/utils": "3.24.1",
|
||||
"@tanstack/react-virtual": "^3.5.1",
|
||||
"ansicolor": "1.1.100",
|
||||
"calculate-size": "1.1.1",
|
||||
"classnames": "2.5.1",
|
||||
|
@ -1,10 +1,15 @@
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import React, { useState } from 'react';
|
||||
import { Meta, StoryFn, StoryObj } from '@storybook/react';
|
||||
import { Chance } from 'chance';
|
||||
import React, { ComponentProps, useMemo, useState } from 'react';
|
||||
|
||||
import { Combobox } from './Combobox';
|
||||
import { Combobox, Option, Value } from './Combobox';
|
||||
|
||||
const meta: Meta<typeof Combobox> = {
|
||||
const chance = new Chance();
|
||||
|
||||
type PropsAndCustomArgs = ComponentProps<typeof Combobox> & { numberOfOptions: number };
|
||||
|
||||
const meta: Meta<PropsAndCustomArgs> = {
|
||||
title: 'Forms/Combobox',
|
||||
component: Combobox,
|
||||
args: {
|
||||
@ -28,9 +33,11 @@ const meta: Meta<typeof Combobox> = {
|
||||
],
|
||||
value: 'banana',
|
||||
},
|
||||
|
||||
render: (args) => <BasicWithState {...args} />,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn<typeof Combobox> = (args) => {
|
||||
const BasicWithState: StoryFn<typeof Combobox> = (args) => {
|
||||
const [value, setValue] = useState(args.value);
|
||||
return (
|
||||
<Combobox
|
||||
@ -44,4 +51,43 @@ export const Basic: StoryFn<typeof Combobox> = (args) => {
|
||||
);
|
||||
};
|
||||
|
||||
type Story = StoryObj<typeof Combobox>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
|
||||
function generateOptions(amount: number): Option[] {
|
||||
return Array.from({ length: amount }, () => ({
|
||||
label: chance.name(),
|
||||
value: chance.guid(),
|
||||
description: chance.sentence(),
|
||||
}));
|
||||
}
|
||||
|
||||
const manyOptions = generateOptions(1e5);
|
||||
manyOptions.push({ label: 'Banana', value: 'banana', description: 'A yellow fruit' });
|
||||
|
||||
const ManyOptionsStory: StoryFn<PropsAndCustomArgs> = ({ numberOfOptions }) => {
|
||||
const [value, setValue] = useState<Value>(manyOptions[5].value);
|
||||
const options = useMemo(() => generateOptions(numberOfOptions), [numberOfOptions]);
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
setValue(val.value);
|
||||
action('onChange')(val);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ManyOptions: StoryObj<PropsAndCustomArgs> = {
|
||||
args: {
|
||||
numberOfOptions: 1e5,
|
||||
options: undefined,
|
||||
value: undefined,
|
||||
},
|
||||
render: ManyOptionsStory,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useCombobox } from 'downshift';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Input, Props as InputProps } from '../Input/Input';
|
||||
|
||||
type Value = string | number;
|
||||
type Option = {
|
||||
export type Value = string | number;
|
||||
export type Option = {
|
||||
label: string;
|
||||
value: Value;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
interface ComboboxProps
|
||||
@ -33,32 +37,86 @@ function itemFilter(inputValue: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function estimateSize() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => {
|
||||
const [items, setItems] = useState(options);
|
||||
const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => listRef.current,
|
||||
estimateSize,
|
||||
overscan: 2,
|
||||
});
|
||||
|
||||
const { getInputProps, getMenuProps, getItemProps, isOpen } = 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<Input suffix={<Icon name={isOpen ? 'search' : 'angle-down'} />} {...restProps} {...getInputProps()} />
|
||||
<ul {...getMenuProps()}>
|
||||
{isOpen &&
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<li key={item.value} {...getItemProps({ item, index })}>
|
||||
{item.label}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className={styles.dropdown} {...getMenuProps({ ref: listRef })}>
|
||||
{isOpen && (
|
||||
<ul style={{ height: rowVirtualizer.getTotalSize() }}>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
return (
|
||||
<li
|
||||
key={items[virtualRow.index].value}
|
||||
{...getItemProps({ item: items[virtualRow.index], index: virtualRow.index })}
|
||||
data-index={virtualRow.index}
|
||||
ref={rowVirtualizer.measureElement}
|
||||
className={styles.menuItem}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<span>{items[virtualRow.index].label}</span>
|
||||
{items[virtualRow.index].description && <span>{items[virtualRow.index].description}</span>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => ({
|
||||
dropdown: css({
|
||||
position: 'absolute',
|
||||
height: 400,
|
||||
width: 600,
|
||||
overflowY: 'scroll',
|
||||
contain: 'strict',
|
||||
}),
|
||||
menuItem: css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'&:first-child': {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
20
yarn.lock
20
yarn.lock
@ -3684,6 +3684,7 @@ __metadata:
|
||||
"@storybook/react": "npm:^8.1.6"
|
||||
"@storybook/react-webpack5": "npm:^8.1.6"
|
||||
"@storybook/theming": "npm:^8.1.6"
|
||||
"@tanstack/react-virtual": "npm:^3.5.1"
|
||||
"@testing-library/dom": "npm:10.0.0"
|
||||
"@testing-library/jest-dom": "npm:6.4.2"
|
||||
"@testing-library/react": "npm:15.0.2"
|
||||
@ -7803,6 +7804,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-virtual@npm:^3.5.1":
|
||||
version: 3.5.1
|
||||
resolution: "@tanstack/react-virtual@npm:3.5.1"
|
||||
dependencies:
|
||||
"@tanstack/virtual-core": "npm:3.5.1"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10/11c8e9e2391fa0c947848a720b7dccccb1e35a78ac3169d1c34629bbec4ec713eed78d4c17a3e540e01386ee25b600a53254357597ae91a5fe35c7436651e975
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/virtual-core@npm:3.5.1":
|
||||
version: 3.5.1
|
||||
resolution: "@tanstack/virtual-core@npm:3.5.1"
|
||||
checksum: 10/611ea09d37cf9183a51d2dfce401c3802b0d91f014e9bbaf32a6220ec7301b873b308130b795d935c0f5b73a43fd8358274915885da692d3e991eeeab6f8711b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/dom@npm:10.0.0, @testing-library/dom@npm:>=7, @testing-library/dom@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "@testing-library/dom@npm:10.0.0"
|
||||
|
Reference in New Issue
Block a user