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 && ( -