Combobox: Add support for optional icon

pull/102621/head
kay delaney 4 months ago
parent 73436e3d55
commit db59136851
  1. 39
      packages/grafana-ui/src/components/Combobox/Combobox.tsx
  2. 15
      packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts
  3. 1
      packages/grafana-ui/src/components/Combobox/types.ts
  4. 24
      packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts

@ -13,7 +13,12 @@ import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import { AsyncError, NotFoundError } from './MessageRows';
import { itemToString } from './filter';
import { getComboboxStyles, MENU_OPTION_HEIGHT, MENU_OPTION_HEIGHT_DESCRIPTION } from './getComboboxStyles';
import {
getComboboxStyles,
MENU_ITEM_PADDING,
MENU_OPTION_HEIGHT,
MENU_OPTION_HEIGHT_DESCRIPTION,
} from './getComboboxStyles';
import { ComboboxOption } from './types';
import { useComboboxFloat } from './useComboboxFloat';
import { useOptions } from './useOptions';
@ -258,11 +263,10 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
rowVirtualizer.scrollToIndex(highlightedIndex);
}
},
onStateChange: ({ inputValue: newInputValue, type, selectedItem: newSelectedItem }) => {
onStateChange: ({ inputValue: newInputValue = '', type, selectedItem: newSelectedItem }) => {
switch (type) {
case useCombobox.stateChangeTypes.InputChange:
updateOptions(newInputValue ?? '');
updateOptions(newInputValue);
break;
default:
break;
@ -270,32 +274,23 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
},
stateReducer(state, actionAndChanges) {
let { changes } = actionAndChanges;
const menuBeingOpened = state.isOpen === false && changes.isOpen === true;
const menuBeingClosed = state.isOpen === true && changes.isOpen === false;
const menuBeingOpened = !state.isOpen && changes.isOpen;
const menuBeingClosed = state.isOpen && !changes.isOpen;
// Reset the input value when the menu is opened. If the menu is opened due to an input change
// then make sure we keep that.
// This will trigger onInputValueChange to load async options
if (menuBeingOpened && changes.inputValue === state.inputValue) {
changes = {
...changes,
inputValue: '',
};
changes.inputValue = '';
}
if (menuBeingClosed) {
// Flush the selected item to the input when the menu is closed
if (changes.selectedItem) {
changes = {
...changes,
inputValue: itemToString(changes.selectedItem),
};
changes.inputValue = itemToString(changes.selectedItem);
} else if (changes.inputValue !== '') {
// Otherwise if no selected value, clear any search from the input
changes = {
...changes,
inputValue: '',
};
changes.inputValue = '';
}
}
@ -422,6 +417,14 @@ export const Combobox = <T extends string | number>(props: ComboboxProps<T>) =>
})}
>
<div className={styles.optionBody}>
{item.imgUrl && (
<img
className={styles.icon}
height={virtualRow.size - 2 * MENU_ITEM_PADDING}
width={virtualRow.size - 2 * MENU_ITEM_PADDING}
src={item.imgUrl}
/>
)}
<span className={styles.optionLabel}>{item.label ?? item.value}</span>
{item.description && <span className={styles.optionDescription}>{item.description}</span>}
</div>

@ -85,16 +85,23 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
}),
optionBody: css({
label: 'combobox-option-body',
display: 'flex',
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gridTemplateRows: 'repeat(2, auto)',
fontWeight: theme.typography.fontWeightMedium,
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}),
icon: css({
gridRow: '1 / 3',
gridColumn: '1 / 2',
marginRight: MENU_ITEM_PADDING,
}),
optionLabel: css({
label: 'combobox-option-label',
textOverflow: 'ellipsis',
overflow: 'hidden',
gridColumn: '2 / 3',
gridRow: '1 / 2',
fontSize: MENU_ITEM_FONT_SIZE,
fontWeight: MENU_ITEM_FONT_WEIGHT,
lineHeight: MENU_ITEM_LINE_HEIGHT,
@ -109,6 +116,8 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
}),
optionDescription: css({
label: 'combobox-option-description',
gridColumn: '2 / 3',
gridRow: '2 / 3',
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,

@ -5,4 +5,5 @@ export type ComboboxOption<T extends string | number = string> = {
value: T;
description?: string;
group?: string;
imgUrl?: string;
};

@ -8,6 +8,7 @@ import {
MENU_ITEM_FONT_WEIGHT,
MENU_ITEM_PADDING,
MENU_OPTION_HEIGHT,
MENU_OPTION_HEIGHT_DESCRIPTION,
POPOVER_MAX_HEIGHT,
} from './getComboboxStyles';
import { ComboboxOption } from './types';
@ -59,17 +60,30 @@ export const useComboboxFloat = (items: Array<ComboboxOption<string | number>>,
});
const longestItemWidth = useMemo(() => {
const maxItemsLength = Math.min(items.length, WIDTH_CALCULATION_LIMIT_ITEMS);
const itemsToLookAt = items.slice(0, maxItemsLength);
let longestItem = '';
const itemsToLookAt = Math.min(items.length, WIDTH_CALCULATION_LIMIT_ITEMS);
let withIcon = false;
let withDescription = false;
for (let i = 0; i < itemsToLookAt; i++) {
const itemLabel = items[i].label ?? items[i].value.toString();
for (const item of itemsToLookAt) {
const itemLabel = item.label ?? item.value.toString();
longestItem = itemLabel.length > longestItem.length ? itemLabel : longestItem;
withIcon ||= !!item.imgUrl;
withDescription ||= !!item.description;
}
const size = measureText(longestItem, MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT).width;
const textWidth = measureText(longestItem, MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT).width;
let adjustedSize = textWidth + MENU_ITEM_PADDING * 2 + scrollbarWidth;
if (withIcon && withDescription) {
adjustedSize += MENU_OPTION_HEIGHT_DESCRIPTION - MENU_ITEM_PADDING;
} else if (withIcon) {
adjustedSize += MENU_OPTION_HEIGHT - MENU_ITEM_PADDING;
}
return size + MENU_ITEM_PADDING * 2 + scrollbarWidth;
return adjustedSize;
}, [items, scrollbarWidth]);
const floatStyles = {

Loading…
Cancel
Save