@grafana/ui: adds a virtualized options for the Select component (#55629)

This commit is contained in:
Gilles De Mey 2022-10-05 10:33:47 +02:00 committed by GitHub
parent 7ac7f844f4
commit 015651f860
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 6 deletions

View File

@ -10,7 +10,7 @@ import { getAvailableIcons } from '../../types';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './Select.mdx';
import { generateOptions } from './mockOptions';
import { generateOptions, generateThousandsOfOptions } from './mockOptions';
import { SelectCommonProps } from './types';
const meta: Meta = {
@ -105,6 +105,24 @@ export const Basic: Story<StoryProps> = (args) => {
</>
);
};
export const BasicVirtualizedList: Story<StoryProps> = (args) => {
const [value, setValue] = useState<SelectableValue<string>>();
return (
<>
<Select
options={generateThousandsOfOptions()}
virtualized
value={value}
onChange={(v) => {
setValue(v);
action('onChange')(v);
}}
{...args}
/>
</>
);
};
/**
* Uses plain values instead of SelectableValue<T>
*/

View File

@ -4,7 +4,7 @@ import { SelectableValue } from '@grafana/data';
import { SelectBase } from './SelectBase';
import { SelectContainer, SelectContainerProps } from './SelectContainer';
import { SelectCommonProps, MultiSelectCommonProps, SelectAsyncProps } from './types';
import { SelectCommonProps, MultiSelectCommonProps, SelectAsyncProps, VirtualizedSelectProps } from './types';
export function Select<T>(props: SelectCommonProps<T>) {
return <SelectBase {...props} />;
@ -24,6 +24,10 @@ export function AsyncSelect<T>(props: AsyncSelectProps<T>) {
return <SelectBase {...props} />;
}
export function VirtualizedSelect<T>(props: VirtualizedSelectProps<T>) {
return <SelectBase virtualized {...props} />;
}
interface AsyncMultiSelectProps<T> extends Omit<MultiSelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
value?: Array<SelectableValue<T>>;

View File

@ -15,14 +15,14 @@ import { IndicatorsContainer } from './IndicatorsContainer';
import { InputControl } from './InputControl';
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
import { SelectContainer } from './SelectContainer';
import { SelectMenu, SelectMenuOptions } from './SelectMenu';
import { SelectMenu, SelectMenuOptions, VirtualizedSelectMenu } from './SelectMenu';
import { SelectOptionGroup } from './SelectOptionGroup';
import { SingleValue } from './SingleValue';
import { ValueContainer } from './ValueContainer';
import { getSelectStyles } from './getSelectStyles';
import { useCustomSelectStyles } from './resetSelectStyles';
import { ActionMeta, SelectBaseProps } from './types';
import { cleanValue, findSelectedValue } from './utils';
import { cleanValue, findSelectedValue, omitDescriptions } from './utils';
interface ExtraValuesIndicatorProps {
maxVisibleValues?: number | undefined;
@ -139,6 +139,7 @@ export function SelectBase<T>({
showAllSelectedWhenOpen = true,
tabSelectsValue = true,
value,
virtualized = false,
width,
isValidNewOption,
formatOptionLabel,
@ -241,7 +242,7 @@ export function SelectBase<T>({
onFocus,
formatOptionLabel,
openMenuOnFocus,
options,
options: virtualized ? omitDescriptions(options) : options,
placeholder,
prefix,
renderControl,
@ -268,12 +269,14 @@ export function SelectBase<T>({
};
}
const SelectMenuComponent = virtualized ? VirtualizedSelectMenu : SelectMenu;
return (
<>
<ReactSelectComponent
ref={reactSelectRef}
components={{
MenuList: SelectMenu,
MenuList: SelectMenuComponent,
Group: SelectOptionGroup,
ValueContainer,
IndicatorsContainer(props: any) {

View File

@ -1,5 +1,8 @@
import { cx } from '@emotion/css';
import { max } from 'lodash';
import React, { FC, RefCallback } from 'react';
import { MenuListProps } from 'react-select';
import { FixedSizeList as List } from 'react-window';
import { SelectableValue, toIconName } from '@grafana/data';
@ -30,6 +33,54 @@ export const SelectMenu: FC<SelectMenuProps> = ({ children, maxHeight, innerRef,
SelectMenu.displayName = 'SelectMenu';
const VIRTUAL_LIST_ITEM_HEIGHT = 37;
const VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER = 7;
// A virtualized version of the SelectMenu, descriptions for SelectableValue options not supported since those are of a variable height.
//
// To support the virtualized list we have to "guess" the width of the menu container based on the longest available option.
// the reason for this is because all of the options will be positioned absolute, this takes them out of the document and no space
// is created for them, thus the container can't grow to accomodate.
//
// VIRTUAL_LIST_ITEM_HEIGHT and WIDTH_ESTIMATE_MULTIPLIER are both magic numbers.
// Some characters (such as emojis and other unicode characters) may consist of multiple code points in which case the width would be inaccurate (but larger than needed).
export const VirtualizedSelectMenu: FC<MenuListProps<SelectableValue>> = ({
children,
maxHeight,
options,
getValue,
}) => {
const theme = useTheme2();
const styles = getSelectStyles(theme);
const [value] = getValue();
const valueIndex = value ? options.findIndex((option: SelectableValue<unknown>) => option.value === value.value) : 0;
const initialOffset = valueIndex * VIRTUAL_LIST_ITEM_HEIGHT;
if (!Array.isArray(children)) {
return null;
}
const longestOption = max(options.map((option) => option.label?.length)) ?? 0;
const widthEstimate = longestOption * VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER;
return (
<List
className={styles.menu}
height={maxHeight}
width={widthEstimate}
aria-label="Select options menu"
itemCount={children.length}
itemSize={VIRTUAL_LIST_ITEM_HEIGHT}
initialScrollOffset={initialOffset}
>
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{children[index]}</div>}
</List>
);
};
VirtualizedSelectMenu.displayName = 'VirtualizedSelectMenu';
interface SelectMenuOptionProps<T> {
isDisabled: boolean;
isFocused: boolean;

View File

@ -32,3 +32,13 @@ export const generateOptions = (desc = false) => {
description: desc ? `This is a description of ${name}` : undefined,
}));
};
export const generateThousandsOfOptions = () => {
const options: Array<SelectableValue<string>> = new Array(10000).fill(null).map((_, index) => ({
value: String(index),
label: 'Option ' + index,
description: 'This is option number ' + index,
}));
return options;
};

View File

@ -77,6 +77,8 @@ export interface SelectCommonProps<T> {
renderControl?: ControlComponent<T>;
tabSelectsValue?: boolean;
value?: T | SelectValue<T> | null;
/** Will wrap the MenuList in a react-window FixedSizeVirtualList for improved performance, does not support options with "description" properties */
virtualized?: boolean;
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
width?: number | 'auto';
isOptionDisabled?: () => boolean;
@ -103,6 +105,11 @@ export interface SelectAsyncProps<T> {
loadingMessage?: string;
}
/** The VirtualizedSelect component uses a slightly different SelectableValue, description and other props are not supported */
export interface VirtualizedSelectProps<T> extends Omit<SelectCommonProps<T>, 'virtualized'> {
options?: Array<Pick<SelectableValue<T>, 'label' | 'value'>>;
}
export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'onChange' | 'isMulti' | 'value'> {
value?: Array<SelectableValue<T>> | T[];
onChange: (item: Array<SelectableValue<T>>) => {} | void;

View File

@ -43,3 +43,10 @@ export const findSelectedValue = (
return null;
};
/**
* Omit descriptions from an array of options
*/
export const omitDescriptions = (options: SelectableValue[]): SelectableValue[] => {
return options.map(({ description, ...rest }) => rest);
};