Combobox
View .mdText field combined with a dropdown for selecting an option or typing a custom value.
Default
Loading interactive example. Full source is available below.
View example source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { useId } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
export const Default = () => {
const comboId = useId('combo-default');
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = /* css */ `
.combobox-default-example {
display: grid;
grid-template-rows: repeat(1fr);
justify-items: start;
gap: var(--spacingVerticalXXS);
max-width: 400px;
}
`;
return (<>
<style>{styles}</style>
<div className="combobox-default-example">
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} placeholder="Select an animal">
{options.map((option) => (<Option key={option} disabled={option === 'Ferret'}>
{option}
</Option>))}
</Combobox>
</div>
</>);
};
export default Default;
API referenceGenerated CoreAI prop and type summaries
API
Generated CoreAI prop summaries, not replacement type declarations. Most native DOM/event and ARIA props are omitted. ? means optional; JSDoc shows defaults, deprecations, and conditional requirements. Defaults assume the standard CoreAI provider.
Combobox API
A Combobox component that extends Fluent UI's Combobox with an inline label.
interface ComboboxProps {
/**
* Controls the colors and borders of the combobox trigger.
* @default 'outline'
*/
appearance?: "outline" | "filled-darker" | "filled-lighter" | "underline";
as?: "input";
/**
* The primary slot, `<input>`, does not support children so we need to explicitly include it here.
*/
children?: ReactNode;
className?: string;
/**
* If set, the combobox will show an icon to clear the current value.
*/
clearable?: boolean;
/**
* The dropdown clear icon
*/
clearIcon?: WithSlotShorthandValue<{ as?: "span"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & { ref?: ((instance: HTMLSpanElement | null) => void) | RefObject<HTMLSpanElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* The default open state when open is uncontrolled
*/
defaultOpen?: boolean;
/**
* For an uncontrolled component, sets the initial selection.
* If this is set, the `defaultValue` prop MUST also be set.
*/
defaultSelectedOptions?: string[];
/**
* The default value displayed in the trigger input or button when the combobox's value is uncontrolled
*/
defaultValue?: string | (readonly string[] & string);
/**
* Disable auto-focusing on the first item when mounting.
* @default false
*/
disableAutoFocus?: boolean;
/**
* Collapse the control to its content width when nothing is selected and
* grow it with the typed/selected content. Only available when `label` is set.
*/
dynamicWidth?: boolean;
/**
* The dropdown arrow icon
*/
expandIcon?: WithSlotShorthandValue<{ as?: "span"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & { ref?: ((instance: HTMLSpanElement | null) => void) | RefObject<HTMLSpanElement> | null; }, "children"> & { children?: any; }> | null | undefined;
freeform?: boolean;
/**
* Render the combobox's popup inline in the DOM.
* This has accessibility benefits, particularly for touch screen readers.
*/
inlinePopup?: boolean;
/**
* The primary slot, an input with role="combobox"
*/
input?: ({ as?: "input"; } & Omit<Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref"> & { ref?: ((instance: HTMLInputElement | null) => void) | RefObject<HTMLInputElement> | null; }, "children"> & { children?: undefined; }) | undefined;
/**
* Optional inline label rendered inside the control, before the value.
*/
label?: string;
/**
* The dropdown listbox slot
*/
listbox?: WithSlotShorthandValue<Omit<ListboxSlots, "root"> & Omit<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }, "ref"> & SelectionProps & { disableAutoFocus?: boolean | undefined; } & RefAttributes<HTMLDivElement>> | null | undefined;
/**
* Where the portal children are mounted on DOM
* @default a new element on document.body without any styling
*/
mountNode?: HTMLElement | { element?: HTMLElement | null; className?: string; } | null | undefined;
/**
* Sets the selection type to multiselect.
* Set this to true for multiselect, even if fully controlling selection state.
* This enables styles and accessibility properties to be set.
* @default false
*/
multiselect?: boolean;
onActiveOptionChange?: EventHandler<ActiveOptionChangeData>;
/**
* Callback when the open/closed state of the dropdown changes
*/
onOpenChange?: ((e: ComboboxBaseOpenEvents, data: ComboboxBaseOpenChangeData) => void);
/**
* Callback when an option is selected
*/
onOptionSelect?: ((event: SelectionEvents, data: OptionOnSelectData) => void);
/**
* Sets the open/closed state of the dropdown.
* Use together with onOpenChange to fully control the dropdown's visibility
*/
open?: boolean;
/**
* If set, the placeholder will show when no value is selected
*/
placeholder?: string;
/**
* Configure the positioning of the combobox dropdown.
* Please refer to the [positioning documentation](https://react.fluentui.dev/?path=/docs/concepts-developer-positioning-components--default#anchor-to-target)
* for more details.
*/
positioning?: PositioningShorthand;
ref?: Ref<HTMLInputElement>;
/**
* The root combobox slot
*/
root?: NonNullable<WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null> | undefined;
/**
* An array of selected option keys.
* Use this with `onOptionSelect` to directly control the selected option(s)
* If this is set, the `value` prop MUST also be controlled.
*/
selectedOptions?: string[];
/**
* Controls the size of the combobox faceplate
* @default 'medium'
*/
size?: "small" | "medium" | "large";
style?: CSSProperties;
/**
* Controls text overflow behavior for the value. When true, long text is truncated with an ellipsis.
*/
truncateTextOverflow?: boolean;
/**
* The value displayed by the Combobox.
* Use this with `onOptionSelect` to directly control the displayed value string
*/
value?: string | (readonly string[] & string);
}
Listbox API
Listbox component: a standalone selection control, or the popup in a Combobox
interface ListboxProps {
as?: "div";
children?: any;
className?: string;
/**
* For an uncontrolled component, sets the initial selection.
* If this is set, the `defaultValue` prop MUST also be set.
*/
defaultSelectedOptions?: string[];
/**
* Disable auto-focusing on the first item when mounting.
* @default false
*/
disableAutoFocus?: boolean;
/**
* Sets the selection type to multiselect.
* Set this to true for multiselect, even if fully controlling selection state.
* This enables styles and accessibility properties to be set.
* @default false
*/
multiselect?: boolean;
/**
* Callback when an option is selected
*/
onOptionSelect?: ((event: SelectionEvents, data: OptionOnSelectData) => void);
ref?: Ref<HTMLDivElement>;
/**
* An array of selected option keys.
* Use this with `onOptionSelect` to directly control the selected option(s)
* If this is set, the `value` prop MUST also be controlled.
*/
selectedOptions?: string[];
style?: CSSProperties;
}
Option API
Option component: a styled child option of a Combobox
interface OptionProps {
as?: "div";
/**
* The check icon that is visible for selected options
*/
checkIcon?: WithSlotShorthandValue<{ as?: "span"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & { ref?: ((instance: HTMLSpanElement | null) => void) | RefObject<HTMLSpanElement> | null; }, "children"> & { children?: any; }> | null | undefined;
children?: any;
className?: string;
/**
* Sets an option to the `disabled` state.
* Disabled options cannot be selected, but are still keyboard navigable
*/
disabled?: boolean;
ref?: Ref<HTMLDivElement>;
style?: CSSProperties;
/**
* An optional override the string value of the Option's display text,
* defaulting to the Option's child content.
* This is used as the Dropdown button's or Combobox input's value when the option is selected,
* and as the comparison for type-to-find keyboard functionality.
* The string value of the Option's display text when the Option's children are not a string.
* This is used as the Dropdown button's or Combobox input's value when the option is selected,
* and as the comparison for type-to-find keyboard functionality.
*/
text?: string;
/**
* Defines a unique identifier for the option.
* Use this to control selectedOptions, or to get the option value in the onOptionSelect callback.
* Defaults to `text` if not provided.
*/
value?: string;
}
Implementation
CoreAI 0.12.0; Fluent 9.74.9.
Import components from @coreai-ds/react-components. CoreAI guidance above takes precedence over API demonstration examples.
Active Option Change
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const ActiveOptionChange = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-active-option-change');
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
const [activeOptionText, setActiveOptionText] = React.useState('');
const onActiveOptionChange = React.useCallback<NonNullable<ComboboxProps['onActiveOptionChange']>>((_, data) => {
if (data?.nextOption?.text) {
setActiveOptionText(data?.nextOption?.text);
}
}, [setActiveOptionText]);
const onMouseEnter = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => {
setActiveOptionText(`${e.currentTarget.textContent} (Mouse enter)`);
}, [setActiveOptionText]);
return (<div className={styles.root}>
{activeOptionText}
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} placeholder="Select an animal" onActiveOptionChange={onActiveOptionChange} {...props}>
{options.map(option => (<Option key={option} disabled={option === 'Ferret'} onMouseEnter={onMouseEnter}>
{option}
</Option>))}
</Combobox>
</div>);
};
export default ActiveOptionChange;
Appearance
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '20px',
maxWidth: '400px',
'> div': {
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
// need padding to see the background color for filled variants
padding: '5px 20px 10px',
},
},
// filledLighter and filledDarker appearances depend on particular background colors
filledLighter: {
backgroundColor: tokens.colorNeutralBackgroundInverted,
'> label': {
color: tokens.colorNeutralForegroundInverted2,
},
},
filledDarker: {
backgroundColor: tokens.colorNeutralBackgroundInverted,
'> label': {
color: tokens.colorNeutralForegroundInverted2,
},
},
});
export const Appearance = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combobox');
const styles = useStyles();
return (<div className={styles.root}>
<div>
<label id={`${comboId}-outline`}>Outline</label>
<Combobox aria-labelledby={`${comboId}-outline`} placeholder="Select a color" appearance="outline" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
<div>
<label id={`${comboId}-underline`}>Underline</label>
<Combobox aria-labelledby={`${comboId}-underline`} placeholder="Select a color" appearance="underline" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
<div className={styles.filledDarker}>
<label id={`${comboId}-filledDarker`}>Filled Darker</label>
<Combobox aria-labelledby={`${comboId}-filledDarker`} placeholder="Select a color" appearance="filled-darker" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
<div className={styles.filledLighter}>
<label id={`${comboId}-filledLighter`}>Filled Lighter</label>
<Combobox aria-labelledby={`${comboId}-filledLighter`} placeholder="Select a color" appearance="filled-lighter" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
</div>);
};
export default Appearance;
Clearable
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Label } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
display: 'grid',
gridTemplateRows: 'auto auto',
justifyItems: 'start',
gap: '2px',
},
});
export const Clearable = (): JSXElement => {
const comboboxId = useId('combobox');
const styles = useStyles();
return (<div className={styles.root}>
<Label id={comboboxId}>Pick a color</Label>
<Combobox clearable aria-labelledby={comboboxId} placeholder="Select a color">
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>);
};
export default Clearable;
Complex Options
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import { Persona } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const ComplexOptions = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-default');
const styles = useStyles();
return (<div className={styles.root}>
<label id={comboId}>Schedule a meeting</label>
<Combobox aria-labelledby={comboId} {...props}>
<Option text="Katri Athokas">
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name="Katri Athokas" presence={{
status: 'available',
}} secondaryText="Available"/>
</Option>
<Option text="Elvia Atkins">
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name="Elvia Atkins" presence={{
status: 'busy',
}} secondaryText="Busy"/>
</Option>
<Option text="Cameron Evans">
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name="Cameron Evans" presence={{
status: 'away',
}} secondaryText="Away"/>
</Option>
<Option text="Wanda Howard">
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name="Wanda Howard" presence={{
status: 'out-of-office',
}} secondaryText="Out of office"/>
</Option>
</Combobox>
</div>);
};
export default ComplexOptions;
Controlled
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import { Persona } from "@coreai-ds/react-components";
import type { PresenceBadgeStatus } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
justifyItems: 'start',
gap: '20px',
maxWidth: '400px',
},
field: {
display: 'grid',
justifyItems: 'start',
gap: '2px',
},
});
const optionData = [
{ text: 'Katri Athokas', value: 'kathok', presence: 'available', secondaryText: 'Available' },
{ text: 'Elvia Atkins', value: 'eatkins', presence: 'busy', secondaryText: 'Busy' },
{ text: 'Cameron Evans', value: 'cevans', presence: 'away', secondaryText: 'Away' },
{ text: 'Wanda Howard', value: 'whoward', presence: 'out-of-office', secondaryText: 'Out of office' },
];
export const Controlled = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-controlled');
const styles = useStyles();
const [selectedOptions, setSelectedOptions] = React.useState<string[]>(['eatkins']);
const [value, setValue] = React.useState('Elvia Atkins');
const [open, setOpen] = React.useState(false);
const onOptionSelect: (typeof props)['onOptionSelect'] = (ev, data) => {
setSelectedOptions(data.selectedOptions);
setValue(data.optionText ?? '');
};
const onInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
setValue(ev.target.value);
};
// Reset the typed value when the Combobox closes to reflect the selected options
React.useEffect(() => {
if (!open) {
setValue(selectedOptions.join(', '));
}
}, [open, selectedOptions]);
return (<div className={styles.root}>
<div className={styles.field}>
<label htmlFor={`${comboId}-default`}>Schedule a meeting (default selection)</label>
<Combobox id={`${comboId}-default`} {...props} defaultValue="Elvia Atkins" defaultSelectedOptions={['eatkins']}>
{optionData.map(opt => (<Option key={opt.value} text={opt.text} value={opt.value}>
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name={opt.text} presence={{
status: opt.presence as PresenceBadgeStatus,
}} secondaryText={opt.secondaryText}/>
</Option>))}
</Combobox>
</div>
<div className={styles.field}>
<label htmlFor={`${comboId}-controlled`}>Schedule a meeting (controlled selection)</label>
<Combobox id={`${comboId}-controlled`} {...props} value={value} selectedOptions={selectedOptions} onInput={onInput} onOptionSelect={onOptionSelect} onOpenChange={(_, data) => setOpen(data.open)}>
{optionData.map(opt => (<Option key={opt.value} text={opt.text} value={opt.value}>
<Persona avatar={{ color: 'colorful', 'aria-hidden': true }} name={opt.text} presence={{
status: opt.presence as PresenceBadgeStatus,
}} secondaryText={opt.secondaryText}/>
</Option>))}
</Combobox>
</div>
</div>);
};
export default Controlled;
Controlling Open And Close
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import type { CheckboxProps } from "@coreai-ds/react-components";
import { Checkbox } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const ControllingOpenAndClose = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-default');
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
const [open, setOpen] = React.useState(false);
const handleOpenChange: ComboboxProps['onOpenChange'] = (e, data) => setOpen(data.open);
const onChange: CheckboxProps['onChange'] = (e, data) => setOpen(!!data.checked);
return (<div className={styles.root}>
<Checkbox value="open" name="state" label="open" capture="user" checked={open} onChange={onChange}/>
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} placeholder="Select an animal" open={open} onOpenChange={handleOpenChange} {...props}>
{options.map(option => (<Option key={option} disabled={option === 'Ferret'}>
{option}
</Option>))}
</Combobox>
</div>);
};
export default ControllingOpenAndClose;
Custom Options
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import type { OptionGroupProps } from "@coreai-ds/react-components";
import { OptionGroup } from "@coreai-ds/react-components";
import type { OptionProps } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import { CheckmarkCircle20Filled } from "@fluentui/react-icons";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
const CustomOption = (props: OptionProps) => {
return <Option {...props} checkIcon={<CheckmarkCircle20Filled />}/>;
};
const CustomOptionGroup = (props: Partial<OptionGroupProps> & {
options: string[];
}) => {
const { label, options, ...optionGroupProps } = props;
const labelSlot = typeof label === 'object' ? label : { children: label };
return (<OptionGroup label={{ style: { fontStyle: 'italic' }, ...labelSlot }} {...optionGroupProps}>
{options.map(option => (<CustomOption key={option} disabled={option === 'Ferret'}>
{option}
</CustomOption>))}
</OptionGroup>);
};
export const CustomOptions = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-default');
const land = ['Cat', 'Dog', 'Ferret', 'Hamster'];
const water = ['Fish', 'Jellyfish', 'Octopus', 'Seal'];
const styles = useStyles();
return (<div className={styles.root}>
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} placeholder="Select an animal" {...props}>
<CustomOptionGroup label="Land" options={land}/>
<CustomOptionGroup label="Sea" options={water}/>
</Combobox>
</div>);
};
export default CustomOptions;
Disabled
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const Disabled = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-disabled');
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
return (<div className={styles.root}>
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} disabled placeholder="Select an animal" {...props}>
{options.map(option => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>);
};
export default Disabled;
Filtering
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import * as React from 'react';
import { useComboboxFilter } from "@coreai-ds/react-components";
import type { ComboboxProps } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
const options = [
{ children: 'Alligator', value: 'Alligator' },
{ children: 'Bee', value: 'Bee' },
{ children: 'Bird', value: 'Bird' },
{ children: 'Cheetah', disabled: true, value: 'Cheetah' },
{ children: 'Dog', value: 'Dog' },
{ children: 'Dolphin', value: 'Dolphin' },
{ children: 'Ferret', value: 'Ferret' },
{ children: 'Firefly', value: 'Firefly' },
{ children: 'Fish', value: 'Fish' },
{ children: 'Goat', value: 'Goat' },
{ children: 'Horse', value: 'Horse' },
{ children: 'Lion', value: 'Lion' },
];
export const Filtering = (): JSXElement => {
const comboId = useId();
const styles = useStyles();
const [query, setQuery] = React.useState<string>('');
const children = useComboboxFilter(query, options, {
noOptionsMessage: 'No animals match your search.',
});
const onOptionSelect: ComboboxProps['onOptionSelect'] = (e, data) => {
setQuery(data.optionText ?? '');
};
return (<div className={styles.root}>
<label id={comboId}>Best pet</label>
<Combobox onOptionSelect={onOptionSelect} aria-labelledby={comboId} placeholder="Select an animal" onChange={ev => setQuery(ev.target.value)} value={query}>
{children}
</Combobox>
</div>);
};
export default Filtering;
Freeform
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const Freeform = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-default');
const options = [
'Cat',
'Caterpillar',
'Catfish',
'Cheetah',
'Chicken',
'Cockatiel',
'Cow',
'Dog',
'Dolphin',
'Ferret',
'Firefly',
'Fish',
'Fox',
'Fox Terrier',
'Frog',
'Hamster',
'Snake',
];
const [matchingOptions, setMatchingOptions] = React.useState([...options]);
const [customSearch, setCustomSearch] = React.useState<string | undefined>();
const styles = useStyles();
const onChange: ComboboxProps['onChange'] = event => {
const value = event.target.value.trim();
const matches = options.filter(option => option.toLowerCase().indexOf(value.toLowerCase()) === 0);
setMatchingOptions(matches);
if (value.length && matches.length < 1) {
setCustomSearch(value);
}
else {
setCustomSearch(undefined);
}
};
const onOptionSelect: ComboboxProps['onOptionSelect'] = (event, data) => {
const matchingOption = data.optionText && options.includes(data.optionText);
if (matchingOption) {
setCustomSearch(undefined);
}
else {
setCustomSearch(data.optionText);
}
};
return (<div className={styles.root}>
<label id={comboId}>Find pets</label>
<Combobox aria-labelledby={comboId} freeform placeholder="Select an animal" onChange={onChange} onOptionSelect={onOptionSelect} {...props}>
{customSearch ? (<Option key="freeform" text={customSearch}>
Search for "{customSearch}"
</Option>) : null}
{matchingOptions.map(option => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>);
};
export default Freeform;
Grouped
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { OptionGroup } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const Grouped = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-grouped');
const land = ['Cat', 'Dog', 'Ferret', 'Hamster'];
const water = ['Fish', 'Jellyfish', 'Octopus', 'Seal'];
const styles = useStyles();
return (<div className={styles.root}>
<label id={comboId}>Best pet</label>
<Combobox aria-labelledby={comboId} placeholder="Select an animal" {...props}>
<OptionGroup label="Land">
{land.map(option => (<Option key={option} disabled={option === 'Ferret'}>
{option}
</Option>))}
</OptionGroup>
<OptionGroup label="Sea">
{water.map(option => (<Option key={option}>{option}</Option>))}
</OptionGroup>
</Combobox>
</div>);
};
export default Grouped;
Inline Label
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as React from 'react';
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
export const InlineLabel = () => {
const statusOptions = ['Not started', 'In progress', 'Blocked', 'Completed'];
const [status, setStatus] = React.useState<string>('');
const styles = /* css */ `
.combobox-inline-label-example {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: var(--spacingVerticalL);
max-width: 320px;
padding: var(--spacingVerticalL);
}
.combobox-inline-label-example__field {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: var(--spacingVerticalXS);
}
.combobox-inline-label-example__prop-label {
font-family: var(--fontFamilyMonospace);
font-size: var(--fontSizeBase200);
color: var(--colorNeutralForeground3);
}
`;
return (<>
<style>{styles}</style>
<div className="combobox-inline-label-example">
<div className="combobox-inline-label-example__field">
<code className="combobox-inline-label-example__prop-label">label</code>
<Combobox label="Status" placeholder="Select a status" value={status} selectedOptions={status ? [status] : []} clearable onOptionSelect={(_event, data) => setStatus(data.optionText ?? '')}>
{statusOptions.map((option) => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>
<div className="combobox-inline-label-example__field">
<code className="combobox-inline-label-example__prop-label">label, dynamicWidth</code>
<Combobox label="Status" dynamicWidth clearable onOptionSelect={() => undefined}>
{statusOptions.map((option) => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>
</div>
</>);
};
export default InlineLabel;
Multiselect
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { typographyStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
description: {
...typographyStyles.caption1,
},
});
export const Multiselect = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-multi');
const selectedListId = `${comboId}-selection`;
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
const onSelect: ComboboxProps['onOptionSelect'] = (event, data) => {
setSelectedOptions(data.selectedOptions);
};
const labelledBy = selectedOptions.length > 0 ? `${comboId} ${selectedListId}` : comboId;
return (<div className={styles.root}>
<label id={comboId}>Best pets</label>
<Combobox aria-labelledby={labelledBy} multiselect={true} placeholder="Select one or more animals" onOptionSelect={onSelect} {...props}>
{options.map(option => (<Option key={option} disabled={option === 'Ferret'}>
{option}
</Option>))}
</Combobox>
{selectedOptions.length ? (<span id={selectedListId} className={styles.description}>
Chosen pets: {selectedOptions.join(', ')}
</span>) : null}
</div>);
};
export default Multiselect;
Multiselect With Tags
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { Dismiss12Regular } from "@fluentui/react-icons";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
tagsList: {
listStyleType: 'none',
marginBottom: tokens.spacingVerticalXXS,
marginTop: 0,
paddingLeft: 0,
display: 'flex',
gridGap: tokens.spacingHorizontalXXS,
},
});
export const MultiselectWithTags = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
// generate ids for handling labelling
const comboId = useId('combo-multi');
const selectedListId = `${comboId}-selection`;
// refs for managing focus when removing tags
const selectedListRef = React.useRef<HTMLUListElement>(null);
const comboboxInputRef = React.useRef<HTMLInputElement>(null);
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
// Handle selectedOptions both when an option is selected or deselected in the Combobox,
// and when an option is removed by clicking on a tag
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
const onSelect: ComboboxProps['onOptionSelect'] = (event, data) => {
setSelectedOptions(data.selectedOptions);
};
const onTagClick = (option: string, index: number) => {
// remove selected option
setSelectedOptions(selectedOptions.filter(o => o !== option));
// focus previous or next option, defaulting to focusing back to the combo input
const indexToFocus = index === 0 ? 1 : index - 1;
const optionToFocus = selectedListRef.current?.querySelector(`#${comboId}-remove-${indexToFocus}`);
if (optionToFocus) {
(optionToFocus as HTMLButtonElement).focus();
}
else {
comboboxInputRef.current?.focus();
}
};
const labelledBy = selectedOptions.length > 0 ? `${comboId} ${selectedListId}` : comboId;
return (<div className={styles.root}>
<label id={comboId}>Best pets</label>
{selectedOptions.length ? (<ul id={selectedListId} className={styles.tagsList} ref={selectedListRef}>
{/* The "Remove" span is used for naming the buttons without affecting the Combobox name */}
<span id={`${comboId}-remove`} hidden>
Remove
</span>
{selectedOptions.map((option, i) => (<li key={option}>
<Button size="small" shape="circular" appearance="primary" icon={<Dismiss12Regular />} iconPosition="after" onClick={() => onTagClick(option, i)} id={`${comboId}-remove-${i}`} aria-labelledby={`${comboId}-remove ${comboId}-remove-${i}`}>
{option}
</Button>
</li>))}
</ul>) : null}
<Combobox aria-labelledby={labelledBy} multiselect={true} placeholder="Select one or more animals" selectedOptions={selectedOptions} onOptionSelect={onSelect} ref={comboboxInputRef} {...props}>
{options.map(option => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>);
};
export default MultiselectWithTags;
Multiselect With Value String
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
maxWidth: '400px',
},
});
export const MultiselectWithValueString = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combo-multi');
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
const [value, setValue] = React.useState('');
const options = ['Cat', 'Dog', 'Ferret', 'Fish', 'Hamster', 'Snake'];
const styles = useStyles();
const onSelect: ComboboxProps['onOptionSelect'] = (event, data) => {
// update selectedOptions
setSelectedOptions(data.selectedOptions);
// reset value to an empty string after selection
setValue('');
};
// clear value on focus
const onFocus = () => {
setValue('');
};
// update value to selected options on blur
const onBlur = () => {
setValue(selectedOptions.join(', '));
};
// update value on input change
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
return (<div className={styles.root}>
<label id={comboId}>Best pets</label>
<Combobox aria-labelledby={comboId} multiselect={true} placeholder="Select one or more animals" value={value} onBlur={onBlur} onChange={onChange} onFocus={onFocus} onOptionSelect={onSelect} {...props}>
{options.map(option => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>);
};
export default MultiselectWithValueString;
Size
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
import * as React from 'react';
const useStyles = makeStyles({
root: {
// Stack the label above the field with a gap
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '20px',
maxWidth: '400px',
'> div': {
display: 'grid',
gridTemplateRows: 'repeat(1fr)',
justifyItems: 'start',
gap: '2px',
},
},
});
export const Size = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combobox');
const styles = useStyles();
return (<div className={styles.root}>
<div>
<label id={`${comboId}-small`}>Small</label>
<Combobox aria-labelledby={`${comboId}-small`} placeholder="Select a color" size="small" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
<div>
<label htmlFor={`${comboId}-med`}>Medium</label>
<Combobox aria-labelledby={`${comboId}-med`} placeholder="Select a color" size="medium" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
<div>
<label htmlFor={`${comboId}-large`}>Large</label>
<Combobox aria-labelledby={`${comboId}-large`} placeholder="Select a color" size="large" {...props}>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>
</div>
</div>);
};
export default Size;
Truncate
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as React from 'react';
import { Combobox } from "@coreai-ds/react-components";
import { Option } from "@coreai-ds/react-components";
export const Truncate = () => {
const longOptions = [
'Awaiting stakeholder review and final sign-off',
'Blocked by an upstream dependency in another team',
'In progress — implementation roughly halfway complete',
'Completed and deployed to the production environment',
];
const [status, setStatus] = React.useState<string>(longOptions[0]);
const styles = /* css */ `
.combobox-truncate-example {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: var(--spacingVerticalL);
max-width: 320px;
padding: var(--spacingVerticalL);
}
.combobox-truncate-example__field {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: var(--spacingVerticalXS);
}
.combobox-truncate-example__prop-label {
font-family: var(--fontFamilyMonospace);
font-size: var(--fontSizeBase200);
color: var(--colorNeutralForeground3);
}
.combobox-truncate-example__combobox {
width: 220px;
}
`;
return (<>
<style>{styles}</style>
<div className="combobox-truncate-example">
<div className="combobox-truncate-example__field">
<code className="combobox-truncate-example__prop-label">
label, truncateTextOverflow
</code>
<Combobox className="combobox-truncate-example__combobox" label="Status" placeholder="Select a status" truncateTextOverflow value={status} selectedOptions={status ? [status] : []} clearable onOptionSelect={(_event, data) => setStatus(data.optionText ?? '')}>
{longOptions.map((option) => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>
<div className="combobox-truncate-example__field">
<code className="combobox-truncate-example__prop-label">truncateTextOverflow</code>
<Combobox className="combobox-truncate-example__combobox" placeholder="Select a status" truncateTextOverflow value={status} selectedOptions={status ? [status] : []} clearable onOptionSelect={(_event, data) => setStatus(data.optionText ?? '')}>
{longOptions.map((option) => (<Option key={option}>{option}</Option>))}
</Combobox>
</div>
</div>
</>);
};
export default Truncate;
Virtualizer
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { ComboboxProps } from "@coreai-ds/react-components";
import type { JSXElement } from "@coreai-ds/react-components";
import { useId } from "@coreai-ds/react-components";
import { useStaticVirtualizerMeasure } from "@fluentui/react-components/unstable";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { useMergedRefs } from "@coreai-ds/react-components";
import { useTimeout } from "@coreai-ds/react-components";
import { Combobox } from "@coreai-ds/react-components";
import { Virtualizer } from "@fluentui/react-components/unstable";
import { Option } from "@coreai-ds/react-components";
const useStyles = makeStyles({
listbox: {
// maxHeight will be applied only positioning autoSize set.
maxHeight: '250px',
},
option: {
height: '32px',
},
});
export const ComboboxVirtualizer = (props: Partial<Omit<ComboboxProps, "label" | "dynamicWidth">>): JSXElement => {
const comboId = useId('combobox');
//This should include the item height (32px) and account for rowGap (2px)
const itemHeight = 32;
const rowGap = 2;
const numberOfItems = 10000;
const { virtualizerLength, bufferItems, bufferSize, scrollRef, containerSizeRef } = useStaticVirtualizerMeasure({
defaultItemSize: itemHeight,
direction: 'vertical',
// We want at least 10 additional items on each side of visible items for page up/down (+ 1 buffer)
bufferItems: 11,
// We need to recalculate index when at least 10 items (+1px) from the bottom or top for page up/down
bufferSize: itemHeight * 10 + 1,
});
const selectedIndex = React.useRef(0);
const styles = useStyles();
const mergedRefs = useMergedRefs(scrollRef);
// Scroll timer required to post scrollTo on stack post-open state change
const [setScrollTimer, clearScrollTimer] = useTimeout();
return (<div>
<div>
<label htmlFor={`${comboId}`}>Medium</label>
<Combobox id={`${comboId}`} placeholder="Select a number" positioning={{ autoSize: true }} listbox={{ ref: mergedRefs, className: styles.listbox }} onOpenChange={(e, data) => {
clearScrollTimer();
if (data.open) {
setScrollTimer(() => {
mergedRefs.current?.scrollTo({ top: (itemHeight + rowGap) * selectedIndex.current });
}, 0);
}
}} onOptionSelect={(e, data) => {
if (data.optionValue) {
selectedIndex.current = parseInt(data.optionValue, 10);
}
}}>
<Virtualizer numItems={numberOfItems} virtualizerLength={virtualizerLength} bufferItems={bufferItems} bufferSize={bufferSize} itemSize={itemHeight} containerSizeRef={containerSizeRef} gap={rowGap}>
{index => {
return (<Option className={styles.option} aria-posinset={index} aria-setsize={numberOfItems} key={`item-${index}`} value={index.toString()}>
{`Item ${index + 1}`}
</Option>);
}}
</Virtualizer>
</Combobox>
</div>
</div>);
};
export default ComboboxVirtualizer;