diff --git a/.betterer.results b/.betterer.results index 5819e039e20..3d31f01d4ef 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5720,9 +5720,6 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], - "packages/grafana-ui/src/components/Combobox/Combobox.story.tsx:5381": [ - [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] - ], "packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.mdx b/packages/grafana-ui/src/components/Combobox/Combobox.mdx new file mode 100644 index 00000000000..7c93072b57a --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/Combobox.mdx @@ -0,0 +1,101 @@ +import { Meta, Preview, ArgTypes } from '@storybook/blocks'; + +import { Combobox } from './Combobox'; + + + +## Usage + +**Do** + +- Use in inline query editors +- Use when you require async calls from a select input + +**Don't** + +- 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 + +## ComboboxOption + +The `ComboboxOption` currently supports 3 properties: + +- `label` - The text that is visible in the menu. +- `value` (required) - The value that is selected. +- `description` - A longer description that describes the choice. + +If no `label` is given, `value` will be used as a display value. + +## Sizing + +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. + +If that is not possible, the width can be set directly on the component, by setting a number, which is a multiple of `8px`. + +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 Usage + +The `options` prop can accept an async function: + +- 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. + +Note: The calls are debounced. Old calls are invalidated when a new call is made. + +## Unit testing + +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) + +This code sets up the mocking before all tests: + +```js +beforeAll(() => { + const mockGetBoundingClientRect = jest.fn(() => ({ + width: 120, + height: 120, + top: 0, + left: 0, + bottom: 0, + right: 0, + })); + + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: mockGetBoundingClientRect, + }); +}); +``` + +### Selecting an option + +To select an option, you can use any `*ByRole` methods, as Combobox has proper roles for accessibility. + +#### Selecting option by clicking + +```js +render(); + +const input = screen.getByRole('combobox'); +await userEvent.click(input); + +const item = await screen.findByRole('option', { name: 'Option 1' }); +await userEvent.click(item); +expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument(); +``` + +#### Selecting option by typing + +```js +render(); + +const input = screen.getByRole('combobox'); +await userEvent.type(input, 'Option 3'); +await userEvent.keyboard('{ArrowDown}{Enter}'); + +expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument(); +``` + +## Props + + diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index 967b7537b8d..1b63c36bec1 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -11,12 +11,18 @@ import { Field } from '../Forms/Field'; import { AsyncSelect, Select } from '../Select/Select'; import { Combobox, ComboboxOption } from './Combobox'; +import mdx from './Combobox.mdx'; type PropsAndCustomArgs = ComponentProps & { numberOfOptions: number }; const meta: Meta = { title: 'Forms/Combobox', component: Combobox, + parameters: { + docs: { + page: mdx, + }, + }, args: { loading: undefined, invalid: undefined, diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index eb8112ba6be..a0086614259 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -27,7 +27,13 @@ export type ComboboxOption = { // then the onChange handler emits ComboboxOption with the label as non-undefined. interface ComboboxBaseProps extends Omit { + /** + * An `X` appears in the UI, which clears the input and sets the value to `null`. Do not use if you have no `null` case. + */ isClearable?: boolean; + /** + * 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 | null) => void; @@ -45,7 +51,13 @@ interface ComboboxBaseProps type AutoSizeConditionals = | { width: 'auto'; + /** + * Needs to be set when width is 'auto' to prevent the input from shrinking too much + */ minWidth: number; + /** + * Recommended to set when width is 'auto' to prevent the input from growing too much. + */ maxWidth?: number; } | {