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
+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
]}
/>`;