ComboboxMultiSelect
ComboboxMultiSelect allows users to choose multiple options from a predefined list. When the input is focused, a popover appears below the input, which displays a list of all available options. Users can filter the options based on string match, by typing into the input element. Each selected option is presented as a Tag, which includes a dismiss button to remove that selection. selected.
The options must be known as the components are rendered, which means this component is not suitable for server-side searches.
Available from eds-core/1.20.0
Quick Start
- Installation
npm install @adaptavant/eds-core
- Import
import { ComboboxMultiSelect } from '@adaptavant/eds-core';
Open Menu
By default, the menu opens when the user types in the trigger input. Also, using menuTrigger="focus"
prop allows you to open the menu when the input is focused.
const initialOptions = [
{
id: "opiasri",
name: "Oscar Piastri",
},
{
id: "dricciardo",
name: "Daniel Ricciardo",
},
{
id: "kchandhok",
name: "Karun Chandhok",
},
{
id: "jdaruvala",
name: "Jehan Daruvala",
},
{
id: "rkubia",
name: "Robert Kubica",
},
];
const selectionReducer = (state, action) => {
switch (action.type) {
case "ADD":
return [...state, action.payload];
case "REMOVE":
return state.filter((item) => item.id !== action.payload.id);
case 'DESELECT_ALL':
return [];
case 'DROP_LAST':
if(state.length === 0) return state;
return state.slice(0, -1);
default:
return state;
}
};
const [selectedOptions, dispatch] = React.useReducer(selectionReducer, []);
const [searchTerm, setSearchTerm] = React.useState("");
const filteredOptions = React.useMemo(() => {
if (searchTerm === "") return initialOptions;
return initialOptions.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const handleSelectionChange = (item) => {
if(!item) return;
const isSelected = selectedOptions.some(
(selected) => selected.id === item.id
);
dispatch({
type: isSelected ? "REMOVE" : "ADD",
payload: item,
});
setSearchTerm("");
};
return (
<Field label="Select Items">
<ComboboxMultiSelect
inputValue={searchTerm}
menuTrigger="focus"
onClose={() => setSearchTerm("")}
onClearAll={() => dispatch({ type: 'DESELECT_ALL' })}
onClearInput={() => setSearchTerm("")}
onRemoveLast={() => dispatch({ type: 'DROP_LAST' })}
onInputChange={setSearchTerm}
onSelectionChange={handleSelectionChange}
options={filteredOptions}
selectedKey="id"
selectedOptions={selectedOptions}
>
<ComboboxMultiSelectTextInput
placeholder="Search..."
renderItem={({ name }) => name}
/>
<ComboboxMultiSelectPopover inputPlaceholder="Search...">
<ComboboxMultiSelectListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => (
<ComboboxMultiSelectItem option={item}>
{item.name}
</ComboboxMultiSelectItem>
)}
</ComboboxMultiSelectListbox>
</ComboboxMultiSelectPopover>
</ComboboxMultiSelect>
</Field>
);
Select All Option
Use the ComboboxMultiSelectAll
component to render a “Select all” option inside the listbox. This lets users select or deselect all options at once and also supports the indeterminate state when only some items are selected
// Data
const allOptions = [
{
id: 'opiasri',
name: 'Oscar Piastri',
},
{
id: 'dricciardo',
name: 'Daniel Ricciardo',
},
{
id: 'kchandhok',
name: 'Karun Chandhok',
},
{
id: 'jdaruvala',
name: 'Jehan Daruvala',
},
{
id: 'rkubia',
name: 'Robert Kubica',
},
];
const selectionKey = 'id';
// Actions
const createSelectionReducer = (selectionKey) => {
return (state, action) => {
switch (action.type) {
case 'ADD':
return [...state, action.payload];
case 'REMOVE':
return state.filter(
(item) => item[selectionKey] !== action.payload[selectionKey]
);
case 'DESELECT_ALL':
return [];
case 'DROP_LAST':
if (state.length === 0) return state;
return state.slice(0, -1);
case 'REPLACE':
return action.payload;
default:
return state;
}
};
};
// States
const [searchTerm, setSearchTerm] = React.useState('');
const deferredSearchTerm = React.useDeferredValue(searchTerm);
const selectionReducer = createSelectionReducer(selectionKey);
const [selectedOptions, dispatch] = React.useReducer(selectionReducer, []);
const isAllSelected = selectedOptions.length === allOptions.length;
const isPartiallySelected = selectedOptions.length > 0 && !isAllSelected;
// Handlers
function handleSelectAll() {
if (isAllSelected) {
dispatch({ type: 'DESELECT_ALL' });
} else {
dispatch({
type: 'REPLACE',
payload: allOptions,
});
}
}
const handleSelectionChange = (tappedOption) => {
if (!tappedOption) return;
const isSelected = selectedOptions.some(
(selected) => selected[selectionKey] === tappedOption[selectionKey]
);
dispatch({
type: isSelected ? 'REMOVE' : 'ADD',
payload: tappedOption,
});
setSearchTerm('');
};
// Filter Options with selected options
const filteredOptions = React.useMemo(() => {
const filteredOptions =
deferredSearchTerm === ''
? allOptions
: allOptions.filter((user) =>
user.name
.toLowerCase()
.includes(deferredSearchTerm.toLowerCase())
);
const selectedFilteredOptions = filteredOptions.map((option) => {
const isSelected = selectedOptions.some(
(selected) => selected[selectionKey] === option[selectionKey]
);
return {
...option,
isSelected,
};
});
if (filteredOptions.length === allOptions.length) {
return [
{
[selectionKey]: 'select-all',
value: 'Select All',
isSelected: isAllSelected,
},
...selectedFilteredOptions,
];
}
return selectedFilteredOptions;
}, [
deferredSearchTerm,
selectedOptions,
selectionKey,
isAllSelected,
allOptions,
]);
return (
<Field label="Select Fruit">
<ComboboxMultiSelect
inputValue={searchTerm}
menuTrigger="focus"
onClearAll={() => dispatch({ type: 'DESELECT_ALL' })}
onClearInput={() => setSearchTerm('')}
onClose={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onRemoveLast={() => dispatch({ type: 'DROP_LAST' })}
onSelectionChange={handleSelectionChange}
selectedKey="name"
selectedOptions={selectedOptions}
>
<ComboboxMultiSelectTextInput
placeholder="Search..."
renderItem={({ name }) => name}
/>
<ComboboxMultiSelectPopover>
<ComboboxMultiSelectListbox
noResultsFallback={<Text className="text-secondary text-center text-body-12 py-4">No matching results</Text>}
options={filteredOptions}
>
{(user) => {
const isSelectAll = user.id === 'select-all';
if (isSelectAll) {
return (
<ComboboxMultiSelectAll
indeterminate={isPartiallySelected}
isSelected={isAllSelected}
onClick={handleSelectAll}
showSelectionIndicator
>
Select all
</ComboboxMultiSelectAll>
);
}
return (
<ComboboxMultiSelectItem id={user.name} option={user}>
{user.name}
</ComboboxMultiSelectItem>
);
}}
</ComboboxMultiSelectListbox>
</ComboboxMultiSelectPopover>
</ComboboxMultiSelect>
</Field>
);
Max Visible Items
Use maxVisibleItems
prop to control how many selected tags can be shown in the input field. If the selection exceeds this, the rest will be collapsed into a "+X more" indicator to save space.
const initialOptions = [
{
id: "opiasri",
name: "Oscar Piastri",
},
{
id: "dricciardo",
name: "Daniel Ricciardo",
},
{
id: "kchandhok",
name: "Karun Chandhok",
},
{
id: "jdaruvala",
name: "Jehan Daruvala",
},
{
id: "rkubia",
name: "Robert Kubica",
},
];
const selectionReducer = (state, action) => {
switch (action.type) {
case "ADD":
return [...state, action.payload];
case "REMOVE":
return state.filter((item) => item.id !== action.payload.id);
case 'DESELECT_ALL':
return [];
case 'DROP_LAST':
if(state.length === 0) return state;
return state.slice(0, -1);
default:
return state;
}
};
const [selectedOptions, dispatch] = React.useReducer(selectionReducer, []);
const [searchTerm, setSearchTerm] = React.useState("");
const filteredOptions = React.useMemo(() => {
if (searchTerm === "") return initialOptions;
return initialOptions.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const handleSelectionChange = (item) => {
if(!item) return;
const isSelected = selectedOptions.some(
(selected) => selected.id === item.id
);
dispatch({
type: isSelected ? "REMOVE" : "ADD",
payload: item,
});
setSearchTerm("");
};
return (
<Field label="Select Items">
<ComboboxMultiSelect
inputValue={searchTerm}
menuTrigger="focus"
onClose={() => setSearchTerm("")}
onClearAll={() => dispatch({ type: 'DESELECT_ALL' })}
onClearInput={() => setSearchTerm("")}
onRemoveLast={() => dispatch({ type: 'DROP_LAST' })}
onInputChange={setSearchTerm}
onSelectionChange={handleSelectionChange}
options={filteredOptions}
selectedKey="id"
selectedOptions={selectedOptions}
>
<ComboboxMultiSelectTextInput
maxVisibleItems={2} // Display maximum of 3 items, rest truncates
placeholder="Search..."
renderItem={({ name }) => name}
/>
<ComboboxMultiSelectPopover>
<ComboboxMultiSelectListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => (
<ComboboxMultiSelectItem option={item}>
{item.name}
</ComboboxMultiSelectItem>
)}
</ComboboxMultiSelectListbox>
</ComboboxMultiSelectPopover>
</ComboboxMultiSelect>
</Field>
);
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.
ComboboxMultiSelect parts
const initialOptions = [
{
id: "opiasri",
name: "Oscar Piastri",
},
{
id: "dricciardo",
name: "Daniel Ricciardo",
},
{
id: "kchandhok",
name: "Karun Chandhok",
},
{
id: "jdaruvala",
name: "Jehan Daruvala",
},
{
id: "rkubia",
name: "Robert Kubica",
},
];
const selectionReducer = (state, action) => {
switch (action.type) {
case "ADD":
return [...state, action.payload];
case "REMOVE":
return state.filter((item) => item.id !== action.payload.id);
case 'DROP_LAST':
if(state.length === 0) return state;
return state.slice(0, -1);
default:
return state;
}
};
const [selectedOptions, dispatch] = React.useReducer(selectionReducer, []);
const [searchTerm, setSearchTerm] = React.useState("");
const filteredOptions = React.useMemo(() => {
if (searchTerm === "") return initialOptions;
return initialOptions.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const handleSelectionChange = (item) => {
if(!item) return;
const isSelected = selectedOptions.some(
(selected) => selected.id === item.id
);
dispatch({
type: isSelected ? "REMOVE" : "ADD",
payload: item,
});
setSearchTerm("");
};
return (
<Field label="Select Items">
<ComboboxMultiSelect
inputValue={searchTerm}
menuTrigger="focus"
onClearInput={() => setSearchTerm("")}
onInputChange={setSearchTerm}
onRemoveLast={() => dispatch({ type: 'DROP_LAST' })}
onSelectionChange={handleSelectionChange}
options={filteredOptions}
selectedKey="id"
selectedOptions={selectedOptions}
>
<ComboboxMultiSelectTextInput
className="bg-palette-violet-background"
classNames={{
focusIndicator: 'border-palette-violet-border',
tag: 'bg-palette-blue-background',
tagLabel: 'text-palette-blue-text',
tagCloseButtonIcon: 'fill-palette-blue-border',
}}
placeholder="Search..."
renderItem={({ name }) => name}
/>
<ComboboxMultiSelectPopover className="bg-palette-violet-background">
<ComboboxMultiSelectListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => (
<ComboboxMultiSelectItem
option={item}
className={`
bg-palette-violet-background
active:bg-palette-violet-background-active
active:data-[highlighted]:bg-palette-violet-background-active
data-[highlighted]:bg-palette-violet-background-active
`}
classNames={{
checkboxControl:
'peer-checked:bg-palette-violet-background-active',
}}
>
{item.name}
</ComboboxMultiSelectItem>
)}
</ComboboxMultiSelectListbox>
</ComboboxMultiSelectPopover>
</ComboboxMultiSelect>
</Field>
);
No parts available. Only root.
ComboboxMultiSelectItem parts
Part | Description |
---|---|
center | The main area of the 'track' |
checkbox | The checkbox root |
checkboxControl | The interactive area of the checkbox |
checkboxIcon | The icon within the checkbox |
rail | Common style applied to both railStart and railEnd via styles.rail. Used for shared customization |
railEnd | The right fixed element. For icons, buttons, or trailing content. |
railStart | The left fixed element. For icons, buttons, or any content to appear before the main area . |
ComboboxMultiSelectTextInput parts
Part | Description |
---|---|
adornmentEnd | Container for the clear (remove all) icon that appears at the end. |
focusIndicator | A hidden span or visual indicator to show when the input is focused. |
inner | The flex element which contains the input and the tags |
input | Styles applied to the input field. |
root | Wrapper around the whole input, tags, and adornments. |
tag | The element which represents a selected item |
tagCloseButton | The button within a Tag which dismisses the selection |
tagCloseButtonIcon | The icon element within the TagCloseButton |
tagLabel | The text element within the tag |