feat(ui): add searchable and multiple selection support to Select component

Enhanced the Select component with search filtering and multi-selection capabilities through new searchable, selectionMode, and searchPlaceholder props.

Breaking changes:

- The SelectProps interface now accepts a generic type parameter for selection mode

Implementation details:

- Created SelectTrigger, SelectContent, and SelectListBox components for modular composition
- Integrated react-aria's Autocomplete and SearchField for search functionality
- Added support for multiple selection mode
- Added "No results found" empty state when search returns no matches
- Improved CSS organization and updated visual styling for better consistency
- Exported Option type as public API for type safety
- Updated API reports and added comprehensive Storybook stories
- Added documentation with examples for searchable and multiple selection modes

Migration guide:

If using SelectProps type directly, update the type parameter:

```diff
- SelectProps
+ SelectProps<'single' | 'multiple'>
```

Component usage remains backward compatible - existing Select implementations require no changes.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2025-11-04 15:04:46 +01:00
parent a00fb88bd7
commit 816af0fa39
12 changed files with 556 additions and 132 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/ui': minor
---
**BREAKING**: The `SelectProps` interface now accepts a generic type parameter for selection mode.
Added searchable and multiple selection support to Select component. The component now accepts `searchable`, `selectionMode`, and `searchPlaceholder` props to enable filtering and multi-selection modes.
Migration: If you're using `SelectProps` type directly, update from `SelectProps` to `SelectProps<'single' | 'multiple'>`. Component usage remains backward compatible.
+39
View File
@@ -11,6 +11,9 @@ import {
selectDisabledSnippet,
selectResponsiveSnippet,
selectIconSnippet,
selectSearchableSnippet,
selectMultipleSnippet,
selectSearchableMultipleSnippet,
} from './select.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -87,6 +90,42 @@ Here's a view when the select is disabled.
code={selectDisabledSnippet}
/>
### Searchable
Here's a view when the select has searchable filtering.
<Snippet
align="center"
py={4}
open
preview={<SelectSnippet story="Searchable" />}
code={selectSearchableSnippet}
/>
### Multiple Selection
Here's a view when the select allows multiple selections.
<Snippet
align="center"
py={4}
open
preview={<SelectSnippet story="MultipleSelection" />}
code={selectMultipleSnippet}
/>
### Searchable with Multiple Selection
Here's a view when the select combines search and multiple selection.
<Snippet
align="center"
py={4}
open
preview={<SelectSnippet story="SearchableMultiple" />}
code={selectSearchableMultipleSnippet}
/>
### Responsive
Here's a view when the select is responsive.
+71 -13
View File
@@ -23,6 +23,12 @@ export const selectPropDefs: Record<string, PropDef> = {
values: ['Array<{ value: string, label: string }>'],
required: true,
},
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
default: 'single',
responsive: false,
},
placeholder: {
type: 'string',
default: 'Select an item',
@@ -34,17 +40,23 @@ export const selectPropDefs: Record<string, PropDef> = {
responsive: false,
},
value: {
type: 'string',
type: 'enum',
values: ['string', 'string[]'],
responsive: false,
description:
'Selected value (controlled). String for single selection, array for multiple.',
},
defaultValue: {
type: 'string',
type: 'enum',
values: ['string', 'string[]'],
responsive: false,
description:
'Initial value (uncontrolled). String for single selection, array for multiple.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
default: 'small',
responsive: true,
},
isOpen: {
@@ -57,7 +69,7 @@ export const selectPropDefs: Record<string, PropDef> = {
},
disabledKeys: {
type: 'enum',
values: ['string[]'],
values: ['Iterable<Key>'],
responsive: false,
},
isDisabled: {
@@ -72,14 +84,6 @@ export const selectPropDefs: Record<string, PropDef> = {
type: 'boolean',
responsive: false,
},
selectedKey: {
type: 'string',
responsive: false,
},
defaultSelectedKey: {
type: 'string',
responsive: false,
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
@@ -87,7 +91,19 @@ export const selectPropDefs: Record<string, PropDef> = {
},
onSelectionChange: {
type: 'enum',
values: ['(key: Key | null) => void'],
values: ['(key: Key | null) => void', '(keys: Selection) => void'],
responsive: false,
description:
'Handler called when selection changes. Single mode: receives Key | null. Multiple mode: receives Selection.',
},
searchable: {
type: 'boolean',
default: 'false',
responsive: false,
},
searchPlaceholder: {
type: 'string',
default: 'Search...',
responsive: false,
},
...classNamePropDefs,
@@ -151,3 +167,45 @@ export const selectResponsiveSnippet = `<Select
label="Font family"
options={[ ... ]}
/>`;
export const selectSearchableSnippet = `<Select
name="country"
label="Country"
searchable
searchPlaceholder="Search countries..."
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 selectMultipleSnippet = `<Select
name="options"
label="Select multiple options"
selectionMode="multiple"
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
]}
/>`;
export const selectSearchableMultipleSnippet = `<Select
name="skills"
label="Skills"
searchable
selectionMode="multiple"
searchPlaceholder="Filter skills..."
options={[
{ value: 'react', label: 'React' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'python', label: 'Python' },
// ... more options
]}
/>`;
+20 -12
View File
@@ -664,12 +664,16 @@ export const componentDefinitions: {
readonly root: 'bui-Select';
readonly popover: 'bui-SelectPopover';
readonly trigger: 'bui-SelectTrigger';
readonly chevron: 'bui-SelectTriggerChevron';
readonly value: 'bui-SelectValue';
readonly icon: 'bui-SelectIcon';
readonly list: 'bui-SelectList';
readonly item: 'bui-SelectItem';
readonly itemIndicator: 'bui-SelectItemIndicator';
readonly itemLabel: 'bui-SelectItemLabel';
readonly searchWrapper: 'bui-SelectSearchWrapper';
readonly search: 'bui-SelectSearch';
readonly searchClear: 'bui-SelectSearchClear';
readonly noResults: 'bui-SelectNoResults';
};
readonly dataAttributes: {
readonly size: readonly ['small', 'medium'];
@@ -1170,6 +1174,14 @@ export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element;
// @public (undocumented)
export interface MenuTriggerProps extends MenuTriggerProps_2 {}
// @public (undocumented)
type Option_2 = {
value: string;
label: string;
disabled?: boolean;
};
export { Option_2 as Option };
// @public (undocumented)
export const Radio: ForwardRefExoticComponent<
RadioProps & RefAttributes<HTMLLabelElement>
@@ -1214,22 +1226,18 @@ export interface SearchFieldProps
// @public (undocumented)
export const Select: ForwardRefExoticComponent<
SelectProps & RefAttributes<HTMLDivElement>
SelectProps<'multiple' | 'single'> & RefAttributes<HTMLDivElement>
>;
// @public (undocumented)
export interface SelectProps
extends SelectProps_2<{
name: string;
value: string;
}>,
export interface SelectProps<T extends 'single' | 'multiple'>
extends SelectProps_2<Option_2, T>,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
icon?: ReactNode;
options?: Array<{
value: string;
label: string;
disabled?: boolean;
}>;
options?: Array<Option_2>;
searchable?: boolean;
searchPlaceholder?: string;
selectionMode?: T;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
@@ -17,6 +17,17 @@
@layer tokens, base, components, utilities;
@layer components {
.bui-Select,
.bui-SelectPopover {
&[data-size='small'] {
--select-item-height: 2rem;
}
&[data-size='medium'] {
--select-item-height: 2.5rem;
}
}
.bui-SelectPopover {
min-width: var(--trigger-width);
}
@@ -32,30 +43,29 @@
cursor: pointer;
gap: var(--bui-space-2);
width: 100%;
height: var(--select-item-height);
.bui-Select[data-size='small'] & {
padding-inline: var(--bui-space-3) 0;
}
.bui-Select[data-size='medium'] & {
padding-inline: var(--bui-space-4) 0;
}
& svg {
flex-shrink: 0;
color: var(--bui-fg-secondary);
}
&[data-size='small'] {
height: 2rem;
padding-inline: var(--bui-space-3);
}
.bui-Select[data-size='small'] & {
width: 1rem;
height: 1rem;
}
&[data-size='medium'] {
height: 3rem;
padding-inline: var(--bui-space-4);
}
&[data-size='small'] svg {
width: 1rem;
height: 1rem;
}
&[data-size='medium'] svg {
width: 1.25rem;
height: 1.25rem;
.bui-Select[data-size='medium'] & {
width: 1.25rem;
height: 1.25rem;
}
}
&::placeholder {
@@ -63,7 +73,7 @@
}
&:hover {
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
transition: border-color 0.2s ease-in-out;
border-color: var(--bui-border-hover);
}
@@ -72,16 +82,13 @@
outline: 0;
}
.bui-Select[data-invalid] &,
&[data-invalid] {
.bui-Select[data-invalid] & {
border-color: var(--bui-fg-danger);
}
&[data-invalid]:hover {
border-width: 2px;
}
&[data-invalid]:focus-visible {
border-width: 2px;
&:focus-visible,
&:hover {
outline: 1px solid var(--bui-fg-danger);
}
}
&[disabled] {
@@ -89,14 +96,15 @@
border-color: var(--bui-border-disabled);
color: var(--bui-fg-disabled);
}
}
&[disabled] .bui-SelectValue {
color: var(--bui-fg-disabled);
}
&[data-popup-open] .bui-SelectIcon {
transform: rotate(180deg);
}
.bui-SelectTriggerChevron {
display: grid;
place-content: center;
width: var(--select-item-height);
height: var(--select-item-height);
flex-shrink: 0;
flex-grow: 0;
}
.bui-SelectValue {
@@ -128,37 +136,32 @@
}
.bui-SelectItem {
box-sizing: border-box;
position: relative;
width: var(--anchor-width);
display: grid;
grid-template-areas: 'icon text';
grid-template-columns: 1rem 1fr;
align-items: center;
padding-block: var(--bui-space-2);
min-height: var(--select-item-height);
padding-block: var(--bui-space-1);
padding-left: var(--bui-space-3);
padding-right: var(--bui-space-4);
color: var(--bui-fg-primary);
border-radius: var(--bui-radius-3);
cursor: pointer;
user-select: none;
font-size: var(--bui-font-size-3);
gap: var(--bui-space-1);
gap: var(--bui-space-2);
outline: none;
&[data-focused] {
z-index: 0;
position: relative;
color: var(--bui-fg-primary);
}
&[data-focused]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--bui-bg-tint-hover);
inset-inline: var(--bui-space-1);
border-radius: var(--bui-radius-2);
background: var(--bui-bg-surface-2);
z-index: -1;
}
&[data-disabled] {
@@ -184,4 +187,73 @@
flex: 1;
grid-area: text;
}
.bui-SelectSearchWrapper {
flex-shrink: 0;
margin-bottom: var(--bui-space-1);
display: flex;
align-items: center;
padding-inline: var(--bui-space-3) 0;
border-bottom: 1px solid var(--bui-border);
}
.bui-SelectSearch {
border: none;
background-color: transparent;
padding: 0;
color: var(--bui-fg-primary);
flex: 1;
outline: none;
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
height: var(--select-item-height);
line-height: var(--select-item-height);
&::placeholder {
color: var(--bui-fg-secondary);
}
/* Hide native browser clear button */
&::-webkit-search-cancel-button,
&::-webkit-search-decoration {
-webkit-appearance: none;
}
}
.bui-SelectSearchClear {
flex: 0 0 auto;
display: grid;
place-content: center;
background-color: transparent;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
color: var(--bui-fg-secondary);
transition: color 0.2s ease-in-out;
width: var(--select-item-height);
height: var(--select-item-height);
input:placeholder-shown + & {
display: none;
}
&:hover {
color: var(--bui-fg-primary);
}
& svg {
width: 1rem;
height: 1rem;
}
}
.bui-SelectNoResults {
padding-inline: var(--bui-space-3);
padding-block: var(--bui-space-2);
color: var(--bui-fg-secondary);
font-size: var(--bui-font-size-3);
font-family: var(--bui-font-regular);
font-weight: var(--bui-font-weight-regular);
}
}
@@ -22,6 +22,9 @@ import { RiCloudLine } from '@remixicon/react';
const meta = {
title: 'Backstage UI/Select',
component: Select,
args: {
style: { width: 300 },
},
} satisfies Meta<typeof Select>;
export default meta;
@@ -34,6 +37,35 @@ const fontOptions = [
{ 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 skills = [
{ value: 'react', label: 'React' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'python', label: 'Python' },
{ value: 'java', label: 'Java' },
{ value: 'csharp', label: 'C#' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' },
{ value: 'kotlin', label: 'Kotlin' },
{ value: 'swift', label: 'Swift' },
];
export const Default: Story = {
args: {
options: fontOptions,
@@ -41,6 +73,38 @@ export const Default: Story = {
},
};
export const Searchable: Story = {
args: {
label: 'Country',
searchable: true,
searchPlaceholder: 'Search countries...',
options: countries,
},
};
export const MultipleSelection: Story = {
args: {
label: 'Select multiple options',
selectionMode: 'multiple',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
],
},
};
export const SearchableMultiple: Story = {
args: {
label: 'Skills',
searchable: true,
selectionMode: 'multiple',
searchPlaceholder: 'Filter skills...',
options: skills,
},
};
export const Preview: Story = {
args: {
label: 'Font Family',
+16 -52
View File
@@ -15,15 +15,7 @@
*/
import { forwardRef, useEffect } from 'react';
import {
Select as AriaSelect,
SelectValue,
Button,
Popover,
ListBox,
ListBoxItem,
Text,
} from 'react-aria-components';
import { Select as AriaSelect, Popover } from 'react-aria-components';
import clsx from 'clsx';
import { SelectProps } from './types';
import { useStyles } from '../../hooks/useStyles';
@@ -31,10 +23,14 @@ import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import styles from './Select.module.css';
import stylesPopover from '../Popover/Popover.module.css';
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react';
import { SelectTrigger } from './SelectTrigger';
import { SelectContent } from './SelectContent';
/** @public */
export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
export const Select = forwardRef<
HTMLDivElement,
SelectProps<'single' | 'multiple'>
>((props, ref) => {
const { classNames: popoverClassNames } = useStyles('Popover');
const { classNames, dataAttributes, cleanedProps } = useStyles('Select', {
size: 'small',
@@ -47,14 +43,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
label,
description,
options,
placeholder,
size,
icon,
searchable,
searchPlaceholder,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
isRequired,
secondaryLabel,
style,
...rest
} = cleanedProps;
@@ -66,7 +61,6 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
}
}, [label, ariaLabel, ariaLabelledBy]);
// If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required.
const secondaryLabelText = secondaryLabel || (isRequired ? 'Required' : null);
return (
@@ -83,16 +77,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
secondaryLabel={secondaryLabelText}
description={description}
/>
<Button
className={clsx(classNames.trigger, styles[classNames.trigger])}
data-size={dataAttributes['data-size']}
>
{icon}
<SelectValue
className={clsx(classNames.value, styles[classNames.value])}
/>
<RiArrowDownSLine aria-hidden="true" />
</Button>
<SelectTrigger icon={icon} />
<FieldError />
<Popover
className={clsx(
@@ -101,34 +86,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
classNames.popover,
styles[classNames.popover],
)}
{...dataAttributes}
>
<ListBox className={clsx(classNames.list, styles[classNames.list])}>
{options?.map(option => (
<ListBoxItem
key={option.value}
id={option.value}
className={clsx(classNames.item, styles[classNames.item])}
>
<div
className={clsx(
classNames.itemIndicator,
styles[classNames.itemIndicator],
)}
>
<RiCheckLine />
</div>
<Text
slot="label"
className={clsx(
classNames.itemLabel,
styles[classNames.itemLabel],
)}
>
{option.label}
</Text>
</ListBoxItem>
))}
</ListBox>
<SelectContent
searchable={searchable}
searchPlaceholder={searchPlaceholder}
options={options}
/>
</Popover>
</AriaSelect>
);
@@ -0,0 +1,74 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Input,
SearchField,
Autocomplete,
Button,
} from 'react-aria-components';
import { useFilter } from 'react-aria';
import { RiCloseCircleLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { SelectListBox } from './SelectListBox';
import styles from './Select.module.css';
import type { Option } from './types';
interface SelectContentProps {
searchable?: boolean;
searchPlaceholder?: string;
options?: Array<Option>;
}
export function SelectContent({
searchable,
searchPlaceholder = 'Search...',
options,
}: SelectContentProps) {
const { contains } = useFilter({ sensitivity: 'base' });
const { classNames } = useStyles('Select');
if (!searchable) {
return <SelectListBox options={options} />;
}
return (
<Autocomplete filter={contains}>
<SearchField
autoFocus
className={clsx(
classNames.searchWrapper,
styles[classNames.searchWrapper],
)}
>
<Input
placeholder={searchPlaceholder}
className={clsx(classNames.search, styles[classNames.search])}
/>
<Button
className={clsx(
classNames.searchClear,
styles[classNames.searchClear],
)}
>
<RiCloseCircleLine />
</Button>
</SearchField>
<SelectListBox options={options} />
</Autocomplete>
);
}
@@ -0,0 +1,71 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ListBox, ListBoxItem, Text } from 'react-aria-components';
import { RiCheckLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Select.module.css';
import type { Option } from './types';
interface SelectListBoxProps {
options?: Array<Option>;
}
const NoResults = () => {
const { classNames } = useStyles('Select');
return (
<div className={clsx(classNames.noResults, styles[classNames.noResults])}>
No results found.
</div>
);
};
export function SelectListBox({ options, ...props }: SelectListBoxProps) {
const { classNames } = useStyles('Select', props);
return (
<ListBox
className={clsx(classNames.list, styles[classNames.list])}
renderEmptyState={() => <NoResults />}
>
{options?.map(option => (
<ListBoxItem
key={option.value}
id={option.value}
textValue={option.label}
className={clsx(classNames.item, styles[classNames.item])}
isDisabled={option.disabled}
>
<div
className={clsx(
classNames.itemIndicator,
styles[classNames.itemIndicator],
)}
>
<RiCheckLine />
</div>
<Text
slot="label"
className={clsx(classNames.itemLabel, styles[classNames.itemLabel])}
>
{option.label}
</Text>
</ListBoxItem>
))}
</ListBox>
);
}
@@ -0,0 +1,42 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
import { Button, SelectValue } from 'react-aria-components';
import { RiArrowDownSLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import styles from './Select.module.css';
interface SelectTriggerProps {
icon?: ReactNode;
}
export function SelectTrigger({ icon }: SelectTriggerProps) {
const { classNames } = useStyles('Select');
return (
<Button className={clsx(classNames.trigger, styles[classNames.trigger])}>
{icon}
<SelectValue
className={clsx(classNames.value, styles[classNames.value])}
/>
<div className={clsx(classNames.chevron, styles[classNames.chevron])}>
<RiArrowDownSLine aria-hidden="true" />
</div>
</Button>
);
}
+26 -7
View File
@@ -20,11 +20,11 @@ import type { SelectProps as AriaSelectProps } from 'react-aria-components';
import type { FieldLabelProps } from '../FieldLabel/types';
/** @public */
export interface SelectProps
extends AriaSelectProps<{
name: string;
value: string;
}>,
export type Option = { value: string; label: string; disabled?: boolean };
/** @public */
export interface SelectProps<T extends 'single' | 'multiple'>
extends AriaSelectProps<Option, T>,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
/**
* An icon to render before the input
@@ -33,12 +33,31 @@ export interface SelectProps
/**
* The size of the select field
* @defaultValue 'medium'
* @defaultValue 'small'
*/
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
/**
* The options of the select field
*/
options?: Array<{ value: string; label: string; disabled?: boolean }>;
options?: Array<Option>;
/**
* Enable search/filter functionality in the dropdown
* @defaultValue false
*/
searchable?: boolean;
/**
* placeholder text for the search input
* only used when searchable is true
* @defaultvalue 'search...'
*/
searchPlaceholder?: string;
/**
* Selection mode, single or multiple
* @defaultvalue 'single'
*/
selectionMode?: T;
}
@@ -303,12 +303,16 @@ export const componentDefinitions = {
root: 'bui-Select',
popover: 'bui-SelectPopover',
trigger: 'bui-SelectTrigger',
chevron: 'bui-SelectTriggerChevron',
value: 'bui-SelectValue',
icon: 'bui-SelectIcon',
list: 'bui-SelectList',
item: 'bui-SelectItem',
itemIndicator: 'bui-SelectItemIndicator',
itemLabel: 'bui-SelectItemLabel',
searchWrapper: 'bui-SelectSearchWrapper',
search: 'bui-SelectSearch',
searchClear: 'bui-SelectSearchClear',
noResults: 'bui-SelectNoResults',
},
dataAttributes: {
size: ['small', 'medium'] as const,