mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Add grouping to Combobox
This commit is contained in:
parent
8ea9396102
commit
26a0bce6aa
@ -8,7 +8,7 @@ import { Field } from '../Forms/Field';
|
||||
|
||||
import { Combobox, ComboboxProps } from './Combobox';
|
||||
import mdx from './Combobox.mdx';
|
||||
import { fakeSearchAPI, generateOptions } from './storyUtils';
|
||||
import { fakeSearchAPI, generateGroupingOptions, generateOptions } from './storyUtils';
|
||||
import { ComboboxOption } from './types';
|
||||
|
||||
type PropsAndCustomArgs<T extends string | number = string> = ComboboxProps<T> & {
|
||||
@ -109,6 +109,14 @@ export const CustomValue: Story = {
|
||||
render: BaseCombobox,
|
||||
};
|
||||
|
||||
export const Groups: Story = {
|
||||
args: {
|
||||
options: await generateGroupingOptions(123),
|
||||
value: '34',
|
||||
},
|
||||
render: BaseCombobox,
|
||||
};
|
||||
|
||||
export const ManyOptions: Story = {
|
||||
args: {
|
||||
numberOfOptions: 1e5,
|
||||
|
@ -8,15 +8,18 @@ import { t } from '../../utils/i18n';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { AutoSizeInput } from '../Input/AutoSizeInput';
|
||||
import { Input, Props as InputProps } from '../Input/Input';
|
||||
import { Stack } from '../Layout/Stack/Stack';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
|
||||
|
||||
import { AsyncError, NotFoundError } from './MessageRows';
|
||||
import { OptionListItem } from './OptionListItem';
|
||||
import { itemToString } from './filter';
|
||||
import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles';
|
||||
import { ComboboxOption } from './types';
|
||||
import { useComboboxFloat } from './useComboboxFloat';
|
||||
import { useOptions } from './useOptions';
|
||||
import { isNewGroup } from './utils';
|
||||
|
||||
// TODO: It would be great if ComboboxOption["label"] was more generic so that if consumers do pass it in (for async),
|
||||
// then the onChange handler emits ComboboxOption with the label as non-undefined.
|
||||
@ -158,8 +161,20 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
|
||||
const virtualizerOptions = {
|
||||
count: filteredOptions.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: (index: number) =>
|
||||
filteredOptions[index].description ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT,
|
||||
// estimateSize: (index: number) =>
|
||||
// filteredOptions[index].description ? MENU_OPTION_HEIGHT_DESCRIPTION : MENU_OPTION_HEIGHT,
|
||||
estimateSize: (index: number) => {
|
||||
const firstGroupItem = isNewGroup(filteredOptions[index], index > 0 ? filteredOptions[index - 1] : undefined);
|
||||
const hasDescription = 'description' in filteredOptions[index];
|
||||
let itemHeight = MENU_OPTION_HEIGHT;
|
||||
if (hasDescription) {
|
||||
itemHeight = MENU_OPTION_HEIGHT_DESCRIPTION;
|
||||
}
|
||||
if (firstGroupItem) {
|
||||
itemHeight += MENU_OPTION_HEIGHT;
|
||||
}
|
||||
return itemHeight;
|
||||
},
|
||||
overscan: VIRTUAL_OVERSCAN_ITEMS,
|
||||
};
|
||||
|
||||
@ -328,17 +343,15 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
|
||||
<ul style={{ height: rowVirtualizer.getTotalSize() }} className={styles.menuUlContainer}>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const item = filteredOptions[virtualRow.index];
|
||||
const startingNewGroup = isNewGroup(item, filteredOptions[virtualRow.index - 1]);
|
||||
const itemId = 'multicombobox-option-' + item.value.toString(); // TODO
|
||||
const groupHeaderid = 'multicombobox-option-group-' + item.value.toString(); // TODO
|
||||
|
||||
return (
|
||||
<li
|
||||
key={`${item.value}-${virtualRow.index}`}
|
||||
data-index={virtualRow.index}
|
||||
className={cx(
|
||||
styles.optionBasic,
|
||||
styles.option,
|
||||
selectedItem && item.value === selectedItem.value && styles.optionSelected,
|
||||
highlightedIndex === virtualRow.index && styles.optionFocused
|
||||
)}
|
||||
className={styles.optionBasic}
|
||||
style={{
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
@ -348,10 +361,32 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
|
||||
index: virtualRow.index,
|
||||
})}
|
||||
>
|
||||
<div className={styles.optionBody}>
|
||||
<span className={styles.optionLabel}>{item.label ?? item.value}</span>
|
||||
{item.description && <span className={styles.optionDescription}>{item.description}</span>}
|
||||
</div>
|
||||
<Stack direction="column" justifyContent="space-between" width="100%" height="100%" gap={0}>
|
||||
{startingNewGroup && (
|
||||
<div className={styles.optionGroup}>
|
||||
<OptionListItem
|
||||
label={item.group ?? t('combobox.group.undefined', 'No group')}
|
||||
id={groupHeaderid}
|
||||
isGroup={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
styles.option,
|
||||
selectedItem && item.value === selectedItem.value && styles.optionSelected,
|
||||
highlightedIndex === virtualRow.index && styles.optionFocused
|
||||
)}
|
||||
>
|
||||
<OptionListItem
|
||||
label={item.label ?? item.value}
|
||||
description={item.description}
|
||||
id={itemId}
|
||||
isGroup={false}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
@ -27,6 +27,7 @@ import { useComboboxFloat } from './useComboboxFloat';
|
||||
import { MAX_SHOWN_ITEMS, useMeasureMulti } from './useMeasureMulti';
|
||||
import { useMultiInputAutoSize } from './useMultiInputAutoSize';
|
||||
import { useOptions } from './useOptions';
|
||||
import { isNewGroup } from './utils';
|
||||
|
||||
interface MultiComboboxBaseProps<T extends string | number> extends Omit<ComboboxBaseProps<T>, 'value' | 'onChange'> {
|
||||
value?: T[] | Array<ComboboxOption<T>>;
|
||||
@ -373,7 +374,7 @@ export const MultiCombobox = <T extends string | number>(props: MultiComboboxPro
|
||||
<div className={styles.optionGroup}>
|
||||
<OptionListItem
|
||||
label={item.group ?? t('combobox.group.undefined', 'No group')}
|
||||
id={id}
|
||||
id={id} // TODO: uses same ID twice
|
||||
isGroup={true}
|
||||
/>
|
||||
</div>
|
||||
@ -453,17 +454,3 @@ function isComboboxOptions<T extends string | number>(
|
||||
): value is Array<ComboboxOption<T>> {
|
||||
return typeof value[0] === 'object';
|
||||
}
|
||||
|
||||
const isNewGroup = <T extends string | number>(option: ComboboxOption<T>, prevOption?: ComboboxOption<T>) => {
|
||||
const currentGroup = option.group;
|
||||
|
||||
if (!currentGroup) {
|
||||
return prevOption?.group ? true : false;
|
||||
}
|
||||
|
||||
if (!prevOption) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return prevOption.group !== currentGroup;
|
||||
};
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
|
||||
import { getComboboxStyles } from './getComboboxStyles';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
description?: string;
|
||||
label: ReactNode;
|
||||
description?: ReactNode;
|
||||
id: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
15
packages/grafana-ui/src/components/Combobox/utils.ts
Normal file
15
packages/grafana-ui/src/components/Combobox/utils.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ComboboxOption } from './types';
|
||||
|
||||
export const isNewGroup = <T extends string | number>(option: ComboboxOption<T>, prevOption?: ComboboxOption<T>) => {
|
||||
const currentGroup = option.group;
|
||||
|
||||
if (!currentGroup) {
|
||||
return prevOption?.group ? true : false;
|
||||
}
|
||||
|
||||
if (!prevOption) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return prevOption.group !== currentGroup;
|
||||
};
|
Loading…
Reference in New Issue
Block a user