mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
@grafana/ui: adds a virtualized options for the Select component (#55629)
This commit is contained in:
parent
7ac7f844f4
commit
015651f860
@ -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>
|
||||
*/
|
||||
|
@ -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>>;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user