diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.mdx b/packages/grafana-ui/src/components/Combobox/Combobox.mdx
index b4b5e8eba17..9cb6c826a01 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.mdx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.mdx
@@ -35,6 +35,10 @@ Options can be an array of objects with seperate label and values, or an array o
While Combobox can handle large sets of options, you should consider both the user experience of searching through many options, and the performance of loading many options from an API.
+### Grouping
+
+Each option object may also have a `group` string property. Options with matching group names will be sorted together, as will options with undefined groups. Combobox will order these based on the first occurence of each `group` (or absence of `group`) in the `options` prop.
+
### Async behaviour
When using Combobox with options from a remote source as the user types, you can supply the `options` prop as an function that is called on each keypress with the current input value and returns a promise resolving to an array of options matching the input.
@@ -116,7 +120,6 @@ Some differences to note:
- `isLoading: boolean` has been renamed to `loading: boolean`
- `allowCustomValue` has been renamed to `createCustomValue`.
- When specifying `width="auto"`, `minWidth` is also required.
-- Groups are not supported at this time.
- Many props used to control subtle behaviour have been removed to simplify the API and improve performance.
- Custom render props, or label as ReactNode is not supported at this time. Reach out if you have a hard requirement for this and we can discuss.
diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
index 4a2ee2c627f..d8dc2ff2b7c 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
@@ -107,6 +107,26 @@ export const CustomValue: Story = {
render: BaseCombobox,
};
+export const GroupsWithMixedLabels: Story = {
+ args: {
+ options: [
+ { label: 'One', value: 'one', group: 'Group 1' },
+ { label: 'Two', value: 'two', group: 'Group 1' },
+ { label: 'Three', value: 'three', group: 'Group 3' },
+ { label: 'Four', value: 'four', group: 'Group 1' },
+ { label: 'Five', value: 'five' },
+ { label: 'Six', value: 'six' },
+ { label: 'Seven', value: 'seven', group: 'Group 2' },
+ { label: 'Eight', value: 'eight', group: 'Group 3' },
+ { label: 'Nine', value: 'nine', group: 'Group 3' },
+ { label: 'Ten', value: 'ten', group: 'Group 3' },
+ { label: 'Eleven', value: 'eleven' },
+ ],
+ value: '',
+ },
+ render: BaseCombobox,
+};
+
export const Groups: Story = {
args: {
options: await generateGroupingOptions(500),
diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx
index 9058f716ccf..7535cf76053 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx
@@ -14,6 +14,14 @@ const options: ComboboxOption[] = [
{ label: 'Option 3', value: '3', description: 'This is option 3' },
{ label: 'Option 4', value: '4' },
];
+const optionsWithGroups: ComboboxOption[] = [
+ { label: 'Option 1', value: '1', group: 'Group 1' },
+ { label: 'Option 2', value: '2' },
+ { label: 'Option 3', value: '3', group: 'Group 1' },
+ { label: 'Option 4', value: '4' },
+ { label: 'Option 5', value: '5', group: 'Group 2' },
+ { label: 'Option 6', value: '6', group: 'Group 2' },
+];
describe('Combobox', () => {
const onChangeHandler = jest.fn();
@@ -192,6 +200,41 @@ describe('Combobox', () => {
['Group 1', 'Option 1', 'Option 3', 'Option 6', 'Group 2', 'Option 2', 'Option 4', 'Option 5'].join('')
);
});
+
+ it('puts ungrouped options relative to first occurrence', async () => {
+ render();
+
+ const input = screen.getByRole('combobox');
+ await userEvent.click(input);
+
+ const listbox = await screen.findByRole('listbox');
+ expect(listbox).toHaveTextContent(
+ ['Group 1', 'Option 1', 'Option 3', 'Option 2', 'Option 4', 'Group 2', 'Option 5', 'Option 6'].join('')
+ );
+ });
+
+ it('does not render group header labels for ungrouped options', async () => {
+ render();
+
+ const input = screen.getByRole('combobox');
+ await userEvent.click(input);
+
+ const allHeaders = await screen.findAllByRole('presentation');
+
+ expect(allHeaders[0]).toHaveTextContent('Group 1');
+ expect(allHeaders[1]).toHaveTextContent('');
+ });
+
+ it('does not render a top border for the first group header', async () => {
+ render();
+
+ const input = screen.getByRole('combobox');
+ await userEvent.click(input);
+
+ const allHeaders = await screen.findAllByRole('presentation');
+
+ expect(allHeaders[0]).toHaveStyle('border-top: none');
+ });
});
describe('size support', () => {
diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx
index e7096544fe7..f383c400b94 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx
@@ -204,11 +204,13 @@ export const Combobox = (props: ComboboxProps) =>
estimateSize: (index: number) => {
const firstGroupItem = isNewGroup(filteredOptions[index], index > 0 ? filteredOptions[index - 1] : undefined);
const hasDescription = 'description' in filteredOptions[index];
+ const hasGroup = 'group' in filteredOptions[index];
+
let itemHeight = MENU_OPTION_HEIGHT;
if (hasDescription) {
itemHeight = MENU_OPTION_HEIGHT_DESCRIPTION;
}
- if (firstGroupItem) {
+ if (firstGroupItem && hasGroup) {
itemHeight += MENU_OPTION_HEIGHT;
}
return itemHeight;
@@ -402,7 +404,15 @@ export const Combobox = (props: ComboboxProps) =>
}}
>
{startingNewGroup && (
-