Add Combobox component to Backstage UI (#34118)
Introduces a Combobox component to Backstage UI for times when you want to allow users to choose from a list of values but also specify their own in some cases. --------- Signed-off-by: James Brooks <jamesbrooks@spotify.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
'use client';
|
||||
|
||||
import { Combobox } from '../../../../../packages/ui/src/components/Combobox/Combobox';
|
||||
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
|
||||
import { RiCloudLine } from '@remixicon/react';
|
||||
|
||||
const fontOptions = [
|
||||
{ value: 'sans', label: 'Sans-serif' },
|
||||
{ value: 'serif', label: 'Serif' },
|
||||
{ value: 'mono', label: 'Monospace' },
|
||||
{ value: 'cursive', label: 'Cursive' },
|
||||
];
|
||||
|
||||
const countries = [
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'mx', label: 'Mexico' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
{ value: 'fr', label: 'France' },
|
||||
{ value: 'de', label: 'Germany' },
|
||||
{ value: 'it', label: 'Italy' },
|
||||
{ value: 'es', label: 'Spain' },
|
||||
{ value: 'jp', label: 'Japan' },
|
||||
{ value: 'cn', label: 'China' },
|
||||
{ value: 'in', label: 'India' },
|
||||
{ value: 'br', label: 'Brazil' },
|
||||
{ value: 'au', label: 'Australia' },
|
||||
];
|
||||
|
||||
const sectionedFonts = [
|
||||
{
|
||||
title: 'Serif Fonts',
|
||||
options: [
|
||||
{ value: 'times', label: 'Times New Roman' },
|
||||
{ value: 'georgia', label: 'Georgia' },
|
||||
{ value: 'garamond', label: 'Garamond' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sans-Serif Fonts',
|
||||
options: [
|
||||
{ value: 'arial', label: 'Arial' },
|
||||
{ value: 'helvetica', label: 'Helvetica' },
|
||||
{ value: 'verdana', label: 'Verdana' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Monospace Fonts',
|
||||
options: [
|
||||
{ value: 'courier', label: 'Courier New' },
|
||||
{ value: 'consolas', label: 'Consolas' },
|
||||
{ value: 'fira', label: 'Fira Code' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const Preview = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
options={fontOptions}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
style={{ maxWidth: 260 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithLabelAndDescription = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
description="Choose a font family for your document"
|
||||
options={fontOptions}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Sizes = () => (
|
||||
<Flex direction="row" gap="2">
|
||||
<Combobox
|
||||
label="Small"
|
||||
size="small"
|
||||
options={fontOptions}
|
||||
name="font-small"
|
||||
placeholder="Pick a font"
|
||||
style={{ maxWidth: 260 }}
|
||||
/>
|
||||
<Combobox
|
||||
label="Medium"
|
||||
size="medium"
|
||||
options={fontOptions}
|
||||
name="font-medium"
|
||||
placeholder="Pick a font"
|
||||
style={{ maxWidth: 260 }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export const WithIcon = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
options={fontOptions}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
icon={<RiCloudLine />}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Disabled = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
options={fontOptions}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
isDisabled
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AllowsCustomValue = () => (
|
||||
<Combobox
|
||||
label="Country"
|
||||
options={countries}
|
||||
placeholder="Type any country"
|
||||
allowsCustomValue
|
||||
name="country"
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DisabledOption = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
options={fontOptions}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
disabledKeys={['serif']}
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithSections = () => (
|
||||
<Combobox
|
||||
label="Font Family"
|
||||
options={sectionedFonts}
|
||||
placeholder="Pick a font"
|
||||
name="font"
|
||||
style={{ width: 300 }}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,153 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { ReactAriaLink } from '@/components/ReactAriaLink';
|
||||
import {
|
||||
Preview,
|
||||
WithLabelAndDescription,
|
||||
Sizes,
|
||||
WithIcon,
|
||||
Disabled,
|
||||
DisabledOption,
|
||||
AllowsCustomValue,
|
||||
WithSections,
|
||||
} from './components';
|
||||
import { comboboxPropDefs } from './props-definition';
|
||||
import {
|
||||
optionPropDefs,
|
||||
optionSectionPropDefs,
|
||||
} from '../select/props-definition';
|
||||
import {
|
||||
comboboxUsageSnippet,
|
||||
comboboxDefaultSnippet,
|
||||
comboboxDescriptionSnippet,
|
||||
comboboxSizesSnippet,
|
||||
comboboxDisabledSnippet,
|
||||
comboboxResponsiveSnippet,
|
||||
comboboxIconSnippet,
|
||||
comboboxDisabledOptionsSnippet,
|
||||
comboboxCustomValueSnippet,
|
||||
comboboxSectionsSnippet,
|
||||
} from './snippets';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
import { ComboboxDefinition } from '../../../utils/definitions';
|
||||
|
||||
export const reactAriaUrls = {
|
||||
combobox: 'https://react-aria.adobe.com/ComboBox',
|
||||
};
|
||||
|
||||
<PageTitle
|
||||
title="Combobox"
|
||||
description="A text input paired with a filterable dropdown for selecting or typing a value."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<Preview />}
|
||||
code={comboboxDefaultSnippet}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={comboboxUsageSnippet} />
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={comboboxPropDefs} />
|
||||
|
||||
<ReactAriaLink component="ComboBox" href={reactAriaUrls.combobox} />
|
||||
|
||||
### Option types
|
||||
|
||||
The `options` prop accepts an array containing either of the following shapes.
|
||||
|
||||
#### `Option`
|
||||
|
||||
<PropsTable data={optionPropDefs} />
|
||||
|
||||
#### `OptionSection`
|
||||
|
||||
<PropsTable data={optionSectionPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Label and description
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<WithLabelAndDescription />}
|
||||
code={comboboxDescriptionSnippet}
|
||||
/>
|
||||
|
||||
### Sizes
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<Sizes />}
|
||||
code={comboboxSizesSnippet}
|
||||
/>
|
||||
|
||||
### With icon
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<WithIcon />}
|
||||
code={comboboxIconSnippet}
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<Disabled />}
|
||||
code={comboboxDisabledSnippet}
|
||||
/>
|
||||
|
||||
### Disabled options
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<DisabledOption />}
|
||||
code={comboboxDisabledOptionsSnippet}
|
||||
/>
|
||||
|
||||
### Custom values
|
||||
|
||||
Allow the user to type a value that is not in the option list by setting `allowsCustomValue`.
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<AllowsCustomValue />}
|
||||
code={comboboxCustomValueSnippet}
|
||||
/>
|
||||
|
||||
### With sections
|
||||
|
||||
Group options under section headings by passing objects with a `title` and a
|
||||
nested `options` array.
|
||||
|
||||
<Snippet
|
||||
layout="side-by-side"
|
||||
open
|
||||
preview={<WithSections />}
|
||||
code={comboboxSectionsSnippet}
|
||||
/>
|
||||
|
||||
### Responsive
|
||||
|
||||
Size can change at different breakpoints.
|
||||
|
||||
<CodeBlock code={comboboxResponsiveSnippet} />
|
||||
|
||||
<Theming definition={ComboboxDefinition} />
|
||||
|
||||
<ChangelogComponent component="combobox" />
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
import { Chip } from '@/components/Chip';
|
||||
|
||||
export const comboboxPropDefs: Record<string, PropDef> = {
|
||||
options: {
|
||||
type: 'enum',
|
||||
values: ['(Option | OptionSection)[]'],
|
||||
description: (
|
||||
<>
|
||||
Options to display in the dropdown. Pass <Chip>Option</Chip> objects
|
||||
directly, or <Chip>OptionSection</Chip> objects to render grouped
|
||||
options under section headings.
|
||||
</>
|
||||
),
|
||||
},
|
||||
allowsCustomValue: {
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description:
|
||||
'When true, the typed text is accepted as the value on blur or Enter even if it does not match any option.',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Controlled selected value.',
|
||||
},
|
||||
defaultValue: {
|
||||
type: 'string',
|
||||
description: 'Initial value for uncontrolled usage.',
|
||||
},
|
||||
onChange: {
|
||||
type: 'enum',
|
||||
values: ['(value: Key | null) => void'],
|
||||
description: 'Called when the selected option changes.',
|
||||
},
|
||||
inputValue: {
|
||||
type: 'string',
|
||||
description: 'Controlled input text.',
|
||||
},
|
||||
defaultInputValue: {
|
||||
type: 'string',
|
||||
description: 'Initial input text for uncontrolled usage.',
|
||||
},
|
||||
onInputChange: {
|
||||
type: 'enum',
|
||||
values: ['(value: string) => void'],
|
||||
description: 'Called when the input text changes.',
|
||||
},
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Visible label above the combobox.',
|
||||
},
|
||||
secondaryLabel: {
|
||||
type: 'string',
|
||||
description: (
|
||||
<>
|
||||
Secondary text shown next to the label. If not provided and isRequired
|
||||
is true, displays <Chip>Required</Chip>.
|
||||
</>
|
||||
),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Helper text displayed below the label.',
|
||||
},
|
||||
placeholder: {
|
||||
type: 'string',
|
||||
description: 'Text shown when the input is empty.',
|
||||
},
|
||||
size: {
|
||||
type: 'enum',
|
||||
values: ['small', 'medium'],
|
||||
default: 'small',
|
||||
responsive: true,
|
||||
description: 'Visual size of the combobox field.',
|
||||
},
|
||||
icon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'Icon displayed before the input.',
|
||||
},
|
||||
onOpenChange: {
|
||||
type: 'enum',
|
||||
values: ['(isOpen: boolean) => void'],
|
||||
description: 'Called when the dropdown opens or closes.',
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Prevents user interaction when true.',
|
||||
},
|
||||
disabledKeys: {
|
||||
type: 'enum',
|
||||
values: ['Iterable<Key>'],
|
||||
description: 'Keys of options that should be disabled.',
|
||||
},
|
||||
isRequired: {
|
||||
type: 'boolean',
|
||||
description: 'Marks the field as required for form validation.',
|
||||
},
|
||||
isInvalid: {
|
||||
type: 'boolean',
|
||||
description: 'Displays the combobox in an error state.',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Form field name for form submission.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
export const comboboxUsageSnippet = `import { Combobox } from '@backstage/ui';
|
||||
|
||||
<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
options={[
|
||||
{ value: 'sans', label: 'Sans-serif' },
|
||||
{ value: 'serif', label: 'Serif' },
|
||||
{ value: 'mono', label: 'Monospace' },
|
||||
{ value: 'cursive', label: 'Cursive' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const comboboxDefaultSnippet = `<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
placeholder="Pick a font"
|
||||
options={[
|
||||
{ value: 'sans', label: 'Sans-serif' },
|
||||
{ value: 'serif', label: 'Serif' },
|
||||
{ value: 'mono', label: 'Monospace' },
|
||||
{ value: 'cursive', label: 'Cursive' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const comboboxDescriptionSnippet = `<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
description="Choose a font family for your document"
|
||||
options={[ ... ]}
|
||||
/>`;
|
||||
|
||||
export const comboboxIconSnippet = `<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
icon={<RiCloudLine />}
|
||||
options={[ ... ]}
|
||||
/>`;
|
||||
|
||||
export const comboboxSizesSnippet = `<Flex>
|
||||
<Combobox
|
||||
size="small"
|
||||
label="Font family"
|
||||
options={[ ... ]}
|
||||
/>
|
||||
<Combobox
|
||||
size="medium"
|
||||
label="Font family"
|
||||
options={[ ... ]}
|
||||
/>
|
||||
</Flex>`;
|
||||
|
||||
export const comboboxDisabledSnippet = `<Combobox
|
||||
isDisabled
|
||||
label="Font family"
|
||||
options={[ ... ]}
|
||||
/>`;
|
||||
|
||||
export const comboboxResponsiveSnippet = `<Combobox
|
||||
size={{ initial: 'small', lg: 'medium' }}
|
||||
label="Font family"
|
||||
options={[ ... ]}
|
||||
/>`;
|
||||
|
||||
export const comboboxDisabledOptionsSnippet = `<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
placeholder="Pick a font"
|
||||
disabledKeys={['cursive', 'serif']}
|
||||
options={[
|
||||
{ value: 'sans', label: 'Sans-serif' },
|
||||
{ value: 'serif', label: 'Serif' },
|
||||
{ value: 'mono', label: 'Monospace' },
|
||||
{ value: 'cursive', label: 'Cursive' },
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const comboboxCustomValueSnippet = `<Combobox
|
||||
name="country"
|
||||
label="Country"
|
||||
allowsCustomValue
|
||||
placeholder="Type any country"
|
||||
options={[
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
{ value: 'fr', label: 'France' },
|
||||
{ value: 'de', label: 'Germany' },
|
||||
// ... more options
|
||||
]}
|
||||
/>`;
|
||||
|
||||
export const comboboxSectionsSnippet = `<Combobox
|
||||
name="font"
|
||||
label="Font Family"
|
||||
options={[
|
||||
{
|
||||
title: 'Serif Fonts',
|
||||
options: [
|
||||
{ value: 'times', label: 'Times New Roman' },
|
||||
{ value: 'georgia', label: 'Georgia' },
|
||||
{ value: 'garamond', label: 'Garamond' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Sans-Serif Fonts',
|
||||
options: [
|
||||
{ value: 'arial', label: 'Arial' },
|
||||
{ value: 'helvetica', label: 'Helvetica' },
|
||||
{ value: 'verdana', label: 'Verdana' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>`;
|
||||
@@ -49,6 +49,11 @@ export const components: Page[] = [
|
||||
title: 'CheckboxGroup',
|
||||
slug: 'checkbox-group',
|
||||
},
|
||||
{
|
||||
title: 'Combobox',
|
||||
slug: 'combobox',
|
||||
status: 'new',
|
||||
},
|
||||
{
|
||||
title: 'Container',
|
||||
slug: 'container',
|
||||
|
||||
Reference in New Issue
Block a user