diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.mdx b/packages/grafana-ui/src/components/Combobox/Combobox.mdx
index 7c93072b57a..b4b5e8eba17 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.mdx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.mdx
@@ -4,53 +4,66 @@ import { Combobox } from './Combobox';
-## Usage
+# Combobox
-**Do**
+A performant and accessible combobox component that supports both synchronous and asynchronous options loading. It provides type-ahead filtering, keyboard navigation, and virtual scrolling for handling large datasets efficiently.
-- Use in inline query editors
-- Use when you require async calls from a select input
+**Use Combobox when you need:**
-**Don't**
+- A searchable dropdown with keyboard navigation
+- Asynchronous loading of options (e.g., API calls)
+- Support for large datasets (1000+ items)
+- Type-ahead filtering functionality
+- Custom value creation
+- Inline form usage (e.g., query editors)
-- Use the async functionality, when all items are only loaded on the initial load
-- Use when fewer than 4 items are needed, as a `RadioButtonGroup` may be more suitable (not for inline use cases)
-- Use this component if you need custom option styling
+**Consider alternatives when:**
-## ComboboxOption
+- You have fewer than 4 options (consider `RadioButtonGroup` instead)
+- You need complex custom option styling
-The `ComboboxOption` currently supports 3 properties:
+## Usage & Guidelines
-- `label` - The text that is visible in the menu.
-- `value` (required) - The value that is selected.
-- `description` - A longer description that describes the choice.
+### Options
-If no `label` is given, `value` will be used as a display value.
+Options are supplied through the `options` prop as either:
-## Sizing
+- An array of options for synchronous usage
+- An async function that returns a promise resolving to options for user input.
-The recommended way to set the width is by sizing the container element. This is so it may reflect a similar size as other inputs in the context.
+Options can be an array of objects with seperate label and values, or an array of strings which will be used as both the label and value.
-If that is not possible, the width can be set directly on the component, by setting a number, which is a multiple of `8px`.
+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.
-For inline usage, such as in query editors, it may be useful to size the input based on the content. Set `width="auto"` to achieve this. In this case, it is also recommended to set `maxWidth` and `minWidth`.
+### Async behaviour
-## Async Usage
+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.
-The `options` prop can accept an async function:
+Consider the following when implementing async behaviour:
-- When the menu opens, the `options` function is called with `''`, to load all options.
-- When the user types, the `options` function is called with the current input value.
+- Consumers should return filtered options matching the input. This is bested suited for APIs that support filtering/search.
+- When the menu is opened with blank input (e.g. initial click with no selected value) the function will be called with an empty string.
+- Consumers should only ever load top-n options from APIs using this async function. If your API does not support filtering, consider loading options yourself and just passing the sync options array in
+- Combobox does not cache calls to the async function. If you need this, implement your own caching.
+- Calls to the async function are debounced, so consumers should not need to implement this themselves.
-Note: The calls are debounced. Old calls are invalidated when a new call is made.
+### Value
-## Unit testing
+The `value` prop is used to set the selected value of the combobox. A scalar value (the value of options) is preferred over a full option object.
-Writing unit tests with Combobox requires mocking the `getBoundingClientRect` method because of [the virtual list library](https://github.com/TanStack/virtual/issues/29#issuecomment-657519522)
+When using async options with seperate labels and values, the `value` prop can be a full option object to ensure the correct label is displayed.
-This code sets up the mocking before all tests:
+### Sizing
-```js
+Combobox defaults to filling the width of its container to match other inputs. If that's not desired, set the `width` prop to control the exact input width.
+
+For inline usage, such as in query editors, it may be useful to size the input based on the text content. Set width="auto" to achieve this. In this case, it is also recommended to set maxWidth and minWidth.
+
+### Unit tests
+
+The component requires mocking `getBoundingClientRect` because of virtualisation:
+
+```typescript
beforeAll(() => {
const mockGetBoundingClientRect = jest.fn(() => ({
width: 120,
@@ -67,13 +80,9 @@ beforeAll(() => {
});
```
-### Selecting an option
+#### Select an option by mouse
-To select an option, you can use any `*ByRole` methods, as Combobox has proper roles for accessibility.
-
-#### Selecting option by clicking
-
-```js
+```jsx
render();
const input = screen.getByRole('combobox');
@@ -84,9 +93,9 @@ await userEvent.click(item);
expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument();
```
-#### Selecting option by typing
+#### Select an option by keyboard
-```js
+```jsx
render();
const input = screen.getByRole('combobox');
@@ -96,6 +105,32 @@ await userEvent.keyboard('{ArrowDown}{Enter}');
expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument();
```
+## Migrating from Select
+
+Combobox's API is similar to Select, but is greatly simplified. Any workarounds you may have implemented to workaround Select's slow performance are no longer necessary.
+
+Some differences to note:
+
+- Virtualisation is built in, so no separate `VirtualizedSelect` component is needed.
+- Async behaviour is built in so a seperate `AsyncSelect` component is not needed
+- `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.
+
+For all async behaviour, pass in a function that returns `Promise` that will be called when the menu is opened, and on keypress.
+
+```tsx
+const loadOptions = useCallback(async (input: string) => {
+ const response = await fetch(`/api/options?query=${input}`);
+ return response.json();
+}, []);
+
+;
+```
+
## Props
diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
index 5575d21df0e..fe3c9bca3f5 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx
@@ -1,9 +1,8 @@
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import { Meta, StoryFn, StoryObj } from '@storybook/react';
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
-import { Alert } from '../Alert/Alert';
import { Field } from '../Forms/Field';
import { Combobox, ComboboxProps } from './Combobox';
@@ -64,7 +63,6 @@ const meta: Meta = {
],
value: 'banana',
},
- decorators: [InDevDecorator],
};
export default meta;
@@ -257,17 +255,3 @@ export const PositioningTest: Story = {
);
},
};
-
-function InDevDecorator(Story: React.ElementType) {
- return (
-
-
- Combobox is still in development and not able to be used externally.
-
- Within the Grafana repo, it can be used by importing it from{' '}
- @grafana/ui/src/unstable
-
-
-
- );
-}
diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx
index 62f231975e3..f69d15b3038 100644
--- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx
+++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx
@@ -35,17 +35,32 @@ export interface ComboboxBaseProps
* Allows the user to set a value which is not in the list of options.
*/
createCustomValue?: boolean;
- options: Array> | ((inputValue: string) => Promise>>);
- onChange: (option: ComboboxOption) => void;
+
/**
- * Most consumers should pass value in as a scalar string | number. However, sometimes with Async because we don't
- * have the full options loaded to match the value to, consumers may also pass in an Option with a label to display.
+ * An array of options, or a function that returns a promise resolving to an array of options.
+ * If a function, it will be called when the menu is opened and on keypress with the current search query.
+ */
+ options: Array> | ((inputValue: string) => Promise>>);
+
+ /**
+ * onChange handler is called with the newly selected option.
+ */
+ onChange: (option: ComboboxOption) => void;
+
+ /**
+ * Current selected value. Most consumers should pass a scalar value (string | number). However, sometimes with Async
+ * it may be better to pass in an Option with a label to display.
*/
value?: T | ComboboxOption | null;
+
/**
- * Defaults to 100%. Number is a multiple of 8px. 'auto' will size the input to the content.
+ * Defaults to full width of container. Number is a multiple of the spacing unit. 'auto' will size the input to the content.
* */
width?: number | 'auto';
+
+ /**
+ * Called when the input loses focus.
+ */
onBlur?: () => void;
}
@@ -53,6 +68,9 @@ const RECOMMENDED_ITEMS_AMOUNT = 100_000;
type ClearableConditionals =
| {
+ /**
+ * Allow the user to clear the selected value. `null` is emitted from the onChange handler
+ */
isClearable: true;
/**
* The onChange handler is called with `null` when clearing the Combobox.