solid-tom-ui 1.0.11 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -246
- package/dist/README.md +246 -246
- package/dist/components/avatar/avatar.js.map +1 -1
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/carousel/carousel.js.map +1 -1
- package/dist/components/chat-bubble/chatBubble.js.map +1 -1
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/collapse/collapse.js.map +1 -1
- package/dist/components/context-menu/context-menu.js.map +1 -1
- package/dist/components/context-menu/context-menu.store.js.map +1 -1
- package/dist/components/divider/divider.js.map +1 -1
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/dropdown/dropdown.store.js.map +1 -1
- package/dist/components/float-button/float-button.js.map +1 -1
- package/dist/components/hover-3d-image/hover-3d-image.js.map +1 -1
- package/dist/components/image-preview/image-preview.js.map +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/input/input.utils.js.map +1 -1
- package/dist/components/input/variants/input-color.js.map +1 -1
- package/dist/components/input/variants/input-date.js.map +1 -1
- package/dist/components/input/variants/input-number.d.ts.map +1 -1
- package/dist/components/input/variants/input-number.js +1 -1
- package/dist/components/input/variants/input-number.js.map +1 -1
- package/dist/components/input/variants/input-otp.js.map +1 -1
- package/dist/components/input/variants/input-password.js.map +1 -1
- package/dist/components/input/variants/input-radio.js.map +1 -1
- package/dist/components/input/variants/input-range.js.map +1 -1
- package/dist/components/input/variants/input-text.d.ts.map +1 -1
- package/dist/components/input/variants/input-text.js +1 -1
- package/dist/components/input/variants/input-text.js.map +1 -1
- package/dist/components/input/variants/input-textarea.js.map +1 -1
- package/dist/components/loading/loading.js.map +1 -1
- package/dist/components/mansory/mansory.js.map +1 -1
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/modal/modal.js.map +1 -1
- package/dist/components/modal/modalContext.js.map +1 -1
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/progress-bar/progress-bar.js.map +1 -1
- package/dist/components/qr-code/qr-code.js.map +1 -1
- package/dist/components/select/select.js +1 -1
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select-zone/select-zone.js.map +1 -1
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/slider/slider.js.map +1 -1
- package/dist/components/splitter/splitter.js.map +1 -1
- package/dist/components/steps/steps.js.map +1 -1
- package/dist/components/swap/swap.js.map +1 -1
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/tab/tab.js.map +1 -1
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/timeline/timeline.js.map +1 -1
- package/dist/components/toast/icons/ErrorIcon.js.map +1 -1
- package/dist/components/toast/icons/IconCircle.js.map +1 -1
- package/dist/components/toast/icons/InfoIcon.js.map +1 -1
- package/dist/components/toast/icons/LoaderIcon.js.map +1 -1
- package/dist/components/toast/icons/SuccessIcon.js.map +1 -1
- package/dist/components/toast/icons/WarningIcon.js.map +1 -1
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/toast/toast.store.js.map +1 -1
- package/dist/components/tooltip/tooltip.js.map +1 -1
- package/dist/components/tour/tour.js.map +1 -1
- package/dist/components/upload/upload.js.map +1 -1
- package/dist/components/z-index/z-index.context.js.map +1 -1
- package/dist/components/z-index/z-index.js.map +1 -1
- package/dist/components/z-index/z-index.store.js.map +1 -1
- package/dist/components/z-index/z-index.types.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/skill/avatar.skill.md.txt +255 -255
- package/dist/skill/badge.skill.md.txt +223 -223
- package/dist/skill/breadcrumb.skill.md.txt +177 -177
- package/dist/skill/button.skill.md.txt +198 -198
- package/dist/skill/carousel.skill.md.txt +406 -406
- package/dist/skill/chat-bubble.skill.md.txt +342 -342
- package/dist/skill/checkbox.skill.md.txt +326 -326
- package/dist/skill/code-preview.skill.md.txt +240 -240
- package/dist/skill/collapse.skill.md.txt +329 -329
- package/dist/skill/context-menu.skill.md.txt +233 -233
- package/dist/skill/diff.skill.md.txt +244 -244
- package/dist/skill/divider.skill.md.txt +151 -151
- package/dist/skill/doc.skill.md.txt +191 -191
- package/dist/skill/drawer.skill.md.txt +157 -157
- package/dist/skill/dropdown.skill.md.txt +198 -198
- package/dist/skill/float-button.skill.md.txt +315 -315
- package/dist/skill/hover-3d-image.skill.md.txt +120 -120
- package/dist/skill/iframe.skill.md.txt +114 -114
- package/dist/skill/image-preview.skill.md.txt +162 -162
- package/dist/skill/indicator.skill.md.txt +60 -60
- package/dist/skill/input.skill.md.txt +489 -489
- package/dist/skill/loading.skill.md.txt +127 -127
- package/dist/skill/menu.skill.md.txt +476 -476
- package/dist/skill/modal.skill.md.txt +359 -359
- package/dist/skill/pagination.skill.md.txt +405 -405
- package/dist/skill/progress-bar.skill.md.txt +207 -207
- package/dist/skill/qr-code.skill.md.txt +136 -136
- package/dist/skill/rating.skill.md.txt +167 -167
- package/dist/skill/select-zone.skill.md.txt +93 -93
- package/dist/skill/select.skill.md.txt +663 -663
- package/dist/skill/skeleton.skill.md.txt +192 -192
- package/dist/skill/slider.skill.md.txt +404 -404
- package/dist/skill/splitter.skill.md.txt +411 -411
- package/dist/skill/steps.skill.md.txt +264 -264
- package/dist/skill/swap.skill.md.txt +139 -139
- package/dist/skill/switch.skill.md.txt +191 -191
- package/dist/skill/tab.skill.md.txt +484 -484
- package/dist/skill/table.example.header.md.txt +666 -666
- package/dist/skill/table.skill.md.txt +1407 -1407
- package/dist/skill/text-rotate.skill.md.txt +186 -186
- package/dist/skill/timeline.skill.md.txt +247 -247
- package/dist/skill/toast.skill.md.txt +531 -531
- package/dist/skill/tooltip.skill.md.txt +222 -222
- package/dist/skill/tour.skill.md.txt +156 -156
- package/dist/skill/upload.skill.md.txt +358 -358
- package/dist/utils/cn.js.map +1 -1
- package/dist/utils/element-tracker.js.map +1 -1
- package/dist/utils/helper.js.map +1 -1
- package/dist/utils/hoc.js.map +1 -1
- package/package.json +132 -133
|
@@ -1,663 +1,663 @@
|
|
|
1
|
-
## COMPONENT IDENTITY
|
|
2
|
-
- **Import**: `import { Select } from 'solid-tom-ui';`
|
|
3
|
-
- **Exports**: `Select` (named export), `SelectProps`, `SelectOptionType`, `OptGroupType`, `SelectValue`, `SelectFieldNames`, `SelectMethods`, `ShowSearchConfig`, `LabeledValue`, `LabelInValueType`, `FlattenOptionData`, `TagRenderProps` (type exports)
|
|
4
|
-
- **Framework**: SolidJS
|
|
5
|
-
- **Purpose**: Feature-rich select/dropdown input — supports single/multi select, search, grouped options, tags, and controlled/uncontrolled modes
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## PROP REFERENCE
|
|
10
|
-
|
|
11
|
-
### Core props (SelectBaseProps)
|
|
12
|
-
|
|
13
|
-
| Prop | Type | Default | Description |
|
|
14
|
-
| -------------------------- | -------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
-
| `options` | `(SelectOptionType \| OptGroupType)[]` | — | List of options or option groups |
|
|
16
|
-
| `value` | `SelectValue` | — | Controlled value. Must update via `onChange`. |
|
|
17
|
-
| `defaultValue` | `SelectValue` | — | Initial value for uncontrolled mode. |
|
|
18
|
-
| `mode` | `'single' \| 'multiple'` | `'single'` | Select mode. `'multiple'` enables multi-select. |
|
|
19
|
-
| `placeholder` | `string` | `'Select...'` | Placeholder text when no value is selected. |
|
|
20
|
-
| `disabled` | `boolean` | `false` | Disable all interactions. |
|
|
21
|
-
| `loading` | `boolean` | `false` | Show loading spinner instead of arrow icon. |
|
|
22
|
-
| `open` | `boolean` | — | Controlled dropdown open state. |
|
|
23
|
-
| `defaultOpen` | `boolean` | `false` | Initial dropdown open state (uncontrolled). |
|
|
24
|
-
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Visual size variant. |
|
|
25
|
-
| `variant` | `'outline' \| 'filled' \| 'borderless' \| 'underlined'` | `'outline'` | Border/fill style variant. |
|
|
26
|
-
| `color` | `BaseColorProps` | `'error'` | Theme color token (affects input border, dropdown highlights). |
|
|
27
|
-
| `placement` | `'top' \| 'bottom'` | `'bottom'` | Dropdown placement direction. |
|
|
28
|
-
| `allowClear` | `boolean \| { clearIcon?: SolidComponent }` | — | Show clear button when value is selected. |
|
|
29
|
-
| `showSearch` | `boolean \| ShowSearchConfig` | auto | Enable search input in dropdown. Auto-enabled when `mode='multiple'`. |
|
|
30
|
-
| `virtual` | `boolean` | `false` | Enable virtual scrolling for large option lists. |
|
|
31
|
-
| `listHeight` | `number` | `256` | Max height of dropdown list in pixels. |
|
|
32
|
-
| `fieldNames` | `SelectFieldNames` | — | Custom field name mapping for option objects (see below). |
|
|
33
|
-
| `labelInValue` | `boolean` | `false` | When `true`, `onChange` receives `LabeledValue` objects instead of raw values. |
|
|
34
|
-
| `notFoundContent` | `SolidComponent` | `'Not Found'` | Content shown when no options match search. |
|
|
35
|
-
| `defaultActiveFirstOption` | `boolean` | `false` | Auto-activate first option when dropdown opens. |
|
|
36
|
-
| `prefix` | `SolidComponent` | — | Icon/element rendered before the selector. |
|
|
37
|
-
| `suffixIcon` | `SolidComponent` | — | Custom dropdown arrow icon. |
|
|
38
|
-
| `removeIcon` | `SolidComponent` | — | Custom remove icon for tags (multiple mode). |
|
|
39
|
-
| `popupRender` | `(originNode: JSXElement) => JSXElement` | — | Wrap/augment dropdown content. |
|
|
40
|
-
| `optionRender` | `(option: FlattenOptionData, info: { index: number }) => JSXElement` | — | Custom render for each option item. |
|
|
41
|
-
| `labelRender` | `(props: LabelInValueType) => JSXElement` | — | Custom render for the selected label in trigger. |
|
|
42
|
-
| `class` | `Partial<Record<'root' \| 'popup', string>>` | — | Custom class for root element and popup. |
|
|
43
|
-
### Single-mode-only props (mode='single' or omitted)
|
|
44
|
-
|
|
45
|
-
| Prop | Type | Description |
|
|
46
|
-
| -------------- | ---------------------------------- | ------------------------ |
|
|
47
|
-
| `value` | `string \| number \| LabeledValue` | Controlled single value. |
|
|
48
|
-
| `defaultValue` | `string \| number \| LabeledValue` | Initial single value. |
|
|
49
|
-
|
|
50
|
-
### Multiple-mode-only props (mode='multiple')
|
|
51
|
-
|
|
52
|
-
| Prop | Type | Default | Description |
|
|
53
|
-
| ---------------------- | ----------------------------------------------------------------- | -------------- | -------------------------------------------------------- |
|
|
54
|
-
| `value` | `string[] \| number[] \| LabeledValue[]` | — | Controlled array of selected values. |
|
|
55
|
-
| `defaultValue` | `string[] \| number[] \| LabeledValue[]` | — | Initial array of selected values. |
|
|
56
|
-
| `allowCustomValue` | `boolean` | `false` | Allow typing and selecting values not in `options` list. |
|
|
57
|
-
| `maxCount` | `number` | — | Maximum number of selectable items. |
|
|
58
|
-
| `maxTagCount` | `number` | — | Max visible tags; excess shown in `+N more...` tooltip. |
|
|
59
|
-
| `maxTagPlaceholder` | `SolidComponent \| ((omitted: LabeledValue[]) => SolidComponent)` | `'+N more...'` | Custom content for omitted tags badge. |
|
|
60
|
-
| `maxTagTextLength` | `number` | — | Truncate tag labels to this character length. |
|
|
61
|
-
| `tagRender` | `(props: TagRenderProps) => JSXElement` | — | Custom render for each selected tag in trigger. |
|
|
62
|
-
| `menuItemSelectedIcon` | `SolidComponent` | checkmark icon | Custom icon shown on selected options in dropdown. |
|
|
63
|
-
|
|
64
|
-
### Event props
|
|
65
|
-
|
|
66
|
-
| Prop | Type | Description |
|
|
67
|
-
| ---------------- | ------------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
68
|
-
| `onChange` | `(value: SelectValue, option: SelectOptionType \| SelectOptionType[]) => void` | Fires on selection change. |
|
|
69
|
-
| `onSelect` | `(value: string \| number \| LabeledValue, option: SelectOptionType) => void` | Fires when an item is selected (not deselected). |
|
|
70
|
-
| `onDeselect` | `(value: string \| number \| LabeledValue) => void` | Fires when an item is deselected (multiple mode). |
|
|
71
|
-
| `onClear` | `() => void` | Fires when clear button is clicked. |
|
|
72
|
-
| `onOpenChange` | `(open: boolean) => void` | Fires when dropdown opens or closes. |
|
|
73
|
-
| `onFocus` | `(event: FocusEvent) => void` | Fires on focus. |
|
|
74
|
-
| `onBlur` | `(event: FocusEvent) => void` | Fires on blur. |
|
|
75
|
-
| `onActive` | `(value: string \| number \| LabeledValue) => void` | Fires when keyboard navigation activates an option. |
|
|
76
|
-
| `onInputKeyDown` | `(event: KeyboardEvent) => void` | Fires on keydown in the trigger/search input. |
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## TYPE REFERENCE
|
|
81
|
-
|
|
82
|
-
### `SelectOptionType`
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
interface SelectOptionType {
|
|
86
|
-
label?: SolidComponent; // display text or JSX
|
|
87
|
-
value?: string | number; // option value (used internally)
|
|
88
|
-
disabled?: boolean; // disable individual option
|
|
89
|
-
class?: string; // extra CSS class on option element
|
|
90
|
-
title?: string; // HTML title attribute on option element
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### `OptGroupType` (grouped options)
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
interface OptGroupType {
|
|
98
|
-
key?: string;
|
|
99
|
-
label?: SolidComponent; // group header label
|
|
100
|
-
options?: SelectOptionType[]; // child options in the group
|
|
101
|
-
class?: string;
|
|
102
|
-
title?: string;
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### `SelectFieldNames` (custom field mapping)
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
interface SelectFieldNames {
|
|
110
|
-
label?: string; // default: 'label'
|
|
111
|
-
value?: string; // default: 'value'
|
|
112
|
-
options?: string; // default: 'options' (for group children)
|
|
113
|
-
groupLabel?: string; // default: 'label' (for group header)
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### `ShowSearchConfig`
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
interface ShowSearchConfig {
|
|
121
|
-
autoClearSearchValue?: boolean; // clear search on select (default: true)
|
|
122
|
-
filterOption?: boolean | ((inputValue: string, option: SelectOptionType) => boolean);
|
|
123
|
-
filterSort?: (
|
|
124
|
-
optA: SelectOptionType,
|
|
125
|
-
optB: SelectOptionType,
|
|
126
|
-
info: { searchValue: string },
|
|
127
|
-
) => number;
|
|
128
|
-
optionFilterProp?: string | string[]; // field(s) to filter on (default: 'value')
|
|
129
|
-
searchValue?: string; // controlled search input value
|
|
130
|
-
onSearch?: (value: string) => void; // fires on search input change
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### `TagRenderProps`
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
interface TagRenderProps {
|
|
138
|
-
label: SolidComponent;
|
|
139
|
-
value: string | number;
|
|
140
|
-
closable: boolean;
|
|
141
|
-
onClose: () => void;
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### `LabeledValue`
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
interface LabeledValue {
|
|
149
|
-
value: string | number;
|
|
150
|
-
label: SolidComponent;
|
|
151
|
-
key?: string;
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## BEHAVIORAL RULES
|
|
158
|
-
|
|
159
|
-
### Controlled vs Uncontrolled
|
|
160
|
-
|
|
161
|
-
- **Uncontrolled**: omit `value`. Internal signal manages state. Use `defaultValue` for initial state.
|
|
162
|
-
- **Controlled**: supply `value`. Component reads it reactively. **You must update the signal yourself in `onChange`**; the component will not auto-sync.
|
|
163
|
-
|
|
164
|
-
### Search behavior
|
|
165
|
-
|
|
166
|
-
- `showSearch={true}` → enables search input with default `filterOption` (matches against `value` field, case-insensitive).
|
|
167
|
-
- `showSearch={object}` → passes `ShowSearchConfig` for full control.
|
|
168
|
-
- In `mode='multiple'`, search is auto-enabled (no need to explicitly set `showSearch`).
|
|
169
|
-
- `autoClearSearchValue` (default `true`) clears search text after each selection.
|
|
170
|
-
|
|
171
|
-
### Filtering logic
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
if filterOption === false → show all options (server-side filtering)
|
|
175
|
-
if filterOption is function → use custom filter function
|
|
176
|
-
else → filter by optionFilterProp fields, lowercase includes match
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Option flattening & groups
|
|
180
|
-
|
|
181
|
-
- `OptGroupType` entries (those with an `options` array) are flattened internally.
|
|
182
|
-
- Groups are rendered with a group label header followed by their options.
|
|
183
|
-
- During search, grouping is collapsed and all matching items are shown in a flat list.
|
|
184
|
-
|
|
185
|
-
### Virtual scroll
|
|
186
|
-
|
|
187
|
-
- `virtual={true}` activates `@tanstack/solid-virtual` for efficient rendering of large lists.
|
|
188
|
-
- Recommended for option lists > 500 items.
|
|
189
|
-
- Item height defaults: options = 32px, group labels = 28px. Heights are **fixed** — variable-height options will misalign.
|
|
190
|
-
|
|
191
|
-
### Multiple mode — tag overflow
|
|
192
|
-
|
|
193
|
-
- `maxTagCount={N}` → shows first N tags; remaining are hidden behind a `+N more...` badge.
|
|
194
|
-
- Hovering the badge reveals a tooltip showing all hidden tags with close buttons.
|
|
195
|
-
- `maxTagPlaceholder` overrides the badge content.
|
|
196
|
-
|
|
197
|
-
### Keyboard navigation
|
|
198
|
-
|
|
199
|
-
| Key | Behavior |
|
|
200
|
-
| ----------- | --------------------------------------------------- |
|
|
201
|
-
| `ArrowDown` | Open dropdown / move active option down |
|
|
202
|
-
| `ArrowUp` | Open dropdown / move active option up |
|
|
203
|
-
| `Enter` | Select active option (or open dropdown) |
|
|
204
|
-
| `Space` | Open dropdown (ignored if search input is focused) |
|
|
205
|
-
| `Escape` | Close dropdown, return focus to trigger |
|
|
206
|
-
| `Backspace` | In multiple mode with empty search: remove last tag |
|
|
207
|
-
| `Tab` | Close dropdown |
|
|
208
|
-
|
|
209
|
-
### `allowCustomValue` (multiple mode only)
|
|
210
|
-
|
|
211
|
-
- Allows user to type arbitrary values not in `options`.
|
|
212
|
-
- Press `Enter` when no option is active to add the current search text as a tag.
|
|
213
|
-
- Custom values are stored internally and appear as synthetic `FlatOption` entries.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## USAGE PATTERNS
|
|
218
|
-
|
|
219
|
-
### 1. Basic single select (uncontrolled)
|
|
220
|
-
|
|
221
|
-
```tsx
|
|
222
|
-
import { Select } from 'solid-tom-ui';
|
|
223
|
-
|
|
224
|
-
const options = [
|
|
225
|
-
{ label: 'Option 1', value: '1' },
|
|
226
|
-
{ label: 'Option 2', value: '2' },
|
|
227
|
-
{ label: 'Option 3', value: '3', disabled: true },
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
<Select options={options} placeholder="Select one..." />;
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### 2. Controlled single select
|
|
234
|
-
|
|
235
|
-
```tsx
|
|
236
|
-
const [value, setValue] = createSignal<string>('1');
|
|
237
|
-
|
|
238
|
-
<Select options={options} value={value()} onChange={val => setValue(val as string)} />;
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### 3. Multiple select (uncontrolled)
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
<Select
|
|
245
|
-
mode="multiple"
|
|
246
|
-
options={options}
|
|
247
|
-
placeholder="Select multiple..."
|
|
248
|
-
allowClear
|
|
249
|
-
onChange={val => console.log(val)}
|
|
250
|
-
/>
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### 4. Controlled multiple select
|
|
254
|
-
|
|
255
|
-
```tsx
|
|
256
|
-
const [values, setValues] = createSignal<string[]>(['1', '2']);
|
|
257
|
-
|
|
258
|
-
<Select
|
|
259
|
-
mode="multiple"
|
|
260
|
-
options={options}
|
|
261
|
-
value={values()}
|
|
262
|
-
onChange={val => setValues(val as string[])}
|
|
263
|
-
allowClear
|
|
264
|
-
/>;
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### 5. Single select with search
|
|
268
|
-
|
|
269
|
-
```tsx
|
|
270
|
-
<Select
|
|
271
|
-
options={largeOptions}
|
|
272
|
-
showSearch={{
|
|
273
|
-
filterOption: (input, option) =>
|
|
274
|
-
String(option?.label).toLowerCase().includes(input.toLowerCase()),
|
|
275
|
-
optionFilterProp: 'label',
|
|
276
|
-
}}
|
|
277
|
-
placeholder="Search to select..."
|
|
278
|
-
/>
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### 6. Option groups
|
|
282
|
-
|
|
283
|
-
```tsx
|
|
284
|
-
const groupedOptions = [
|
|
285
|
-
{
|
|
286
|
-
label: 'Managers',
|
|
287
|
-
options: [
|
|
288
|
-
{ label: 'Alice', value: 'alice' },
|
|
289
|
-
{ label: 'Bob', value: 'bob' },
|
|
290
|
-
],
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
label: 'Engineers',
|
|
294
|
-
options: [
|
|
295
|
-
{ label: 'Carol', value: 'carol' },
|
|
296
|
-
{ label: 'Dave', value: 'dave' },
|
|
297
|
-
],
|
|
298
|
-
},
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
<Select options={groupedOptions} placeholder="Select a person..." />;
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### 7. Custom field names (non-standard option shape)
|
|
305
|
-
|
|
306
|
-
```tsx
|
|
307
|
-
const countryOptions = [
|
|
308
|
-
{ name: 'Vietnam', id: 'vn' },
|
|
309
|
-
{ name: 'Japan', id: 'jp' },
|
|
310
|
-
];
|
|
311
|
-
|
|
312
|
-
<Select
|
|
313
|
-
options={countryOptions as any}
|
|
314
|
-
fieldNames={{ label: 'name', value: 'id' }}
|
|
315
|
-
placeholder="Select country..."
|
|
316
|
-
/>;
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### 8. Label in value
|
|
320
|
-
|
|
321
|
-
```tsx
|
|
322
|
-
<Select
|
|
323
|
-
labelInValue
|
|
324
|
-
defaultValue={{ value: '1', label: 'Option 1' }}
|
|
325
|
-
options={options}
|
|
326
|
-
onChange={val => {
|
|
327
|
-
const { value, label } = val as LabeledValue;
|
|
328
|
-
console.log(value, label);
|
|
329
|
-
}}
|
|
330
|
-
/>
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### 9. Virtual scroll (large option lists)
|
|
334
|
-
|
|
335
|
-
```tsx
|
|
336
|
-
const manyOptions = Array.from({ length: 10000 }, (_, i) => ({
|
|
337
|
-
label: `Option ${i + 1}`,
|
|
338
|
-
value: `opt-${i + 1}`,
|
|
339
|
-
}));
|
|
340
|
-
|
|
341
|
-
<Select
|
|
342
|
-
options={manyOptions}
|
|
343
|
-
showSearch
|
|
344
|
-
virtual
|
|
345
|
-
listHeight={256}
|
|
346
|
-
placeholder="Search 10,000 items..."
|
|
347
|
-
/>;
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### 10. Max selections (multiple mode)
|
|
351
|
-
|
|
352
|
-
```tsx
|
|
353
|
-
<Select mode="multiple" options={options} maxCount={3} placeholder="Select up to 3..." />
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### 11. Max tag count with custom placeholder
|
|
357
|
-
|
|
358
|
-
```tsx
|
|
359
|
-
<Select
|
|
360
|
-
mode="multiple"
|
|
361
|
-
options={colorOptions}
|
|
362
|
-
defaultValue={['red', 'green', 'blue', 'yellow']}
|
|
363
|
-
maxTagCount={2}
|
|
364
|
-
maxTagPlaceholder={omitted => `and ${omitted.length} more...`}
|
|
365
|
-
/>
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
### 12. Allow custom values (free tagging)
|
|
369
|
-
|
|
370
|
-
```tsx
|
|
371
|
-
<Select
|
|
372
|
-
mode="multiple"
|
|
373
|
-
allowCustomValue
|
|
374
|
-
options={langOptions}
|
|
375
|
-
placeholder="Type or select tags..."
|
|
376
|
-
/>
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### 13. Custom option render
|
|
380
|
-
|
|
381
|
-
```tsx
|
|
382
|
-
<Select
|
|
383
|
-
options={options}
|
|
384
|
-
optionRender={(option, { index }) => (
|
|
385
|
-
<div class="flex items-center gap-2">
|
|
386
|
-
<span class="text-xs font-bold">{index + 1}.</span>
|
|
387
|
-
<span>{option.label as string}</span>
|
|
388
|
-
</div>
|
|
389
|
-
)}
|
|
390
|
-
/>
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### 14. Custom tag render (multiple mode)
|
|
394
|
-
|
|
395
|
-
```tsx
|
|
396
|
-
<Select
|
|
397
|
-
mode="multiple"
|
|
398
|
-
options={options}
|
|
399
|
-
tagRender={props => (
|
|
400
|
-
<span class="badge badge-primary gap-1">
|
|
401
|
-
{props.label as string}
|
|
402
|
-
{props.closable && (
|
|
403
|
-
<span onClick={props.onClose} class="cursor-pointer">
|
|
404
|
-
×
|
|
405
|
-
</span>
|
|
406
|
-
)}
|
|
407
|
-
</span>
|
|
408
|
-
)}
|
|
409
|
-
/>
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### 15. Custom label render (selected value in trigger)
|
|
413
|
-
|
|
414
|
-
```tsx
|
|
415
|
-
<Select
|
|
416
|
-
options={langOptions}
|
|
417
|
-
labelRender={props => (
|
|
418
|
-
<span class="flex items-center gap-1">
|
|
419
|
-
<span class="text-primary font-mono text-xs">[{props.value}]</span>
|
|
420
|
-
<span>{props.label as string}</span>
|
|
421
|
-
</span>
|
|
422
|
-
)}
|
|
423
|
-
/>
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### 16. Popup render (add item footer)
|
|
427
|
-
|
|
428
|
-
```tsx
|
|
429
|
-
const [opts, setOpts] = createSignal([...baseOptions]);
|
|
430
|
-
const [newName, setNewName] = createSignal('');
|
|
431
|
-
|
|
432
|
-
<Select
|
|
433
|
-
options={opts()}
|
|
434
|
-
popupRender={menu => (
|
|
435
|
-
<div>
|
|
436
|
-
{menu}
|
|
437
|
-
<div class="flex gap-2 border-t p-2" onMouseDown={e => e.stopPropagation()}>
|
|
438
|
-
<input
|
|
439
|
-
class="input input-xs flex-1"
|
|
440
|
-
value={newName()}
|
|
441
|
-
onInput={e => setNewName(e.currentTarget.value)}
|
|
442
|
-
onKeyDown={e => {
|
|
443
|
-
if (e.key === 'Enter' && newName().trim()) {
|
|
444
|
-
const n = newName().trim();
|
|
445
|
-
setOpts(prev => [...prev, { label: n, value: n }]);
|
|
446
|
-
setNewName('');
|
|
447
|
-
}
|
|
448
|
-
}}
|
|
449
|
-
placeholder="New item..."
|
|
450
|
-
/>
|
|
451
|
-
<button
|
|
452
|
-
class="btn btn-xs btn-primary"
|
|
453
|
-
onClick={() => {
|
|
454
|
-
if (newName().trim()) {
|
|
455
|
-
const n = newName().trim();
|
|
456
|
-
setOpts(prev => [...prev, { label: n, value: n }]);
|
|
457
|
-
setNewName('');
|
|
458
|
-
}
|
|
459
|
-
}}
|
|
460
|
-
>
|
|
461
|
-
Add
|
|
462
|
-
</button>
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
465
|
-
)}
|
|
466
|
-
/>;
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### 17. Sizes
|
|
470
|
-
|
|
471
|
-
```tsx
|
|
472
|
-
<Select size="sm" options={options} placeholder="Small" />
|
|
473
|
-
<Select size="md" options={options} placeholder="Medium (default)" />
|
|
474
|
-
<Select size="lg" options={options} placeholder="Large" />
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
### 18. Variants
|
|
478
|
-
|
|
479
|
-
```tsx
|
|
480
|
-
<Select variant="outline" options={options} placeholder="Outline (default)" />
|
|
481
|
-
<Select variant="filled" options={options} placeholder="Filled" />
|
|
482
|
-
<Select variant="borderless" options={options} placeholder="Borderless" />
|
|
483
|
-
<Select variant="underlined" options={options} placeholder="Underlined" />
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### 19. Placement (dropdown direction)
|
|
487
|
-
|
|
488
|
-
```tsx
|
|
489
|
-
<Select options={options} placement="top" placeholder="Opens upward" />
|
|
490
|
-
<Select options={options} placement="bottom" placeholder="Opens downward (default)" />
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
### 20. Disabled state
|
|
494
|
-
|
|
495
|
-
```tsx
|
|
496
|
-
<Select disabled defaultValue="1" options={options} />
|
|
497
|
-
<Select disabled mode="multiple" defaultValue={['1', '2']} options={options} />
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### 21. Loading state
|
|
501
|
-
|
|
502
|
-
```tsx
|
|
503
|
-
<Select loading placeholder="Loading options..." options={[]} />
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### 22. Allow clear
|
|
507
|
-
|
|
508
|
-
```tsx
|
|
509
|
-
<Select allowClear defaultValue="1" options={options} />
|
|
510
|
-
// With custom clear icon:
|
|
511
|
-
<Select allowClear={{ clearIcon: <span>✕</span> }} options={options} />
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### 23. Prefix icon
|
|
515
|
-
|
|
516
|
-
```tsx
|
|
517
|
-
<Select
|
|
518
|
-
prefix={<DynamicIcon name="search" size={14} />}
|
|
519
|
-
showSearch
|
|
520
|
-
options={options}
|
|
521
|
-
placeholder="Search..."
|
|
522
|
-
/>
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### 24. Controlled open state
|
|
526
|
-
|
|
527
|
-
```tsx
|
|
528
|
-
const [open, setOpen] = createSignal(false);
|
|
529
|
-
|
|
530
|
-
<Select open={open()} onOpenChange={setOpen} options={options} />;
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### 25. Server-side search (disable client filter)
|
|
534
|
-
|
|
535
|
-
```tsx
|
|
536
|
-
const [searchResults, setSearchResults] = createSignal<SelectOptionType[]>([]);
|
|
537
|
-
|
|
538
|
-
<Select
|
|
539
|
-
options={searchResults()}
|
|
540
|
-
showSearch={{
|
|
541
|
-
filterOption: false, // disable client-side filtering
|
|
542
|
-
onSearch: async q => {
|
|
543
|
-
const results = await fetchOptions(q);
|
|
544
|
-
setSearchResults(results);
|
|
545
|
-
},
|
|
546
|
-
}}
|
|
547
|
-
placeholder="Search server..."
|
|
548
|
-
/>;
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### 26. Max tag text length
|
|
552
|
-
|
|
553
|
-
```tsx
|
|
554
|
-
<Select
|
|
555
|
-
mode="multiple"
|
|
556
|
-
options={options}
|
|
557
|
-
maxTagTextLength={8}
|
|
558
|
-
defaultValue={['very-long-label-value']}
|
|
559
|
-
/>
|
|
560
|
-
// Labels longer than 8 chars are truncated: "very-lon..."
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## CSS CLASSES (public API for customization)
|
|
566
|
-
|
|
567
|
-
| Class | Applied to |
|
|
568
|
-
| ---------------------------------- | ------------------------------------- |
|
|
569
|
-
| `sui-select` | Root trigger `<div>` |
|
|
570
|
-
| `sui-select-outline` | Variant: outline |
|
|
571
|
-
| `sui-select-filled` | Variant: filled |
|
|
572
|
-
| `sui-select-borderless` | Variant: borderless |
|
|
573
|
-
| `sui-select-underlined` | Variant: underlined |
|
|
574
|
-
| `sui-select-sm/md/lg` | Size modifier |
|
|
575
|
-
| `sui-select-focused` | Root when dropdown is open |
|
|
576
|
-
| `sui-select-open` | Root when dropdown is open |
|
|
577
|
-
| `sui-select-multiple` | Root in multiple mode |
|
|
578
|
-
| `sui-select-disabled` | Root when `disabled=true` |
|
|
579
|
-
| `sui-select-prefix` | Prefix icon wrapper `<span>` |
|
|
580
|
-
| `sui-select-selector` | Inner selector container |
|
|
581
|
-
| `sui-select-selection-single` | Single-mode value display |
|
|
582
|
-
| `sui-select-selection-item` | Selected label in single mode |
|
|
583
|
-
| `sui-select-placeholder` | Placeholder `<span>` |
|
|
584
|
-
| `sui-select-selection-multiple` | Multiple-mode tags container |
|
|
585
|
-
| `sui-select-tag` | Each tag in multiple mode |
|
|
586
|
-
| `sui-select-tag-content` | Tag label text |
|
|
587
|
-
| `sui-select-tag-remove` | Tag remove button |
|
|
588
|
-
| `sui-select-tag-omitted` | The `+N more...` badge |
|
|
589
|
-
| `sui-select-suffix` | Suffix area (clear + arrow) |
|
|
590
|
-
| `sui-select-clear` | Clear button `<span>` |
|
|
591
|
-
| `sui-select-arrow` | Dropdown arrow icon |
|
|
592
|
-
| `sui-select-arrow-open` | Arrow when dropdown is open (rotated) |
|
|
593
|
-
| `sui-select-option-list` | Dropdown options container |
|
|
594
|
-
| `sui-select-option` | Each option item |
|
|
595
|
-
| `sui-select-option-selected` | Selected option |
|
|
596
|
-
| `sui-select-option-active` | Keyboard-active option |
|
|
597
|
-
| `sui-select-option-disabled` | Disabled option |
|
|
598
|
-
| `sui-select-option-grouped` | Option inside a group |
|
|
599
|
-
| `sui-select-option-content` | Option label wrapper |
|
|
600
|
-
| `sui-select-option-state` | Checkmark icon wrapper |
|
|
601
|
-
| `sui-select-option-group-label` | Group header `<div>` |
|
|
602
|
-
| `sui-select-dropdown-search-input` | Search `<input>` in dropdown |
|
|
603
|
-
| `sui-select-empty` | Empty/not-found state `<div>` |
|
|
604
|
-
|
|
605
|
-
To add custom class to root or popup: `class={{ root: 'my-root', popup: 'my-popup' }}`.
|
|
606
|
-
|
|
607
|
-
---
|
|
608
|
-
|
|
609
|
-
## ANIMATION
|
|
610
|
-
|
|
611
|
-
- **Tags (multiple mode)**:
|
|
612
|
-
- Enter: slide from right (`translateX(100% → 0)`, opacity 0→1, 300ms ease-out).
|
|
613
|
-
- Exit: scale-out from left origin (`scale(1→0)`, 300ms ease-in).
|
|
614
|
-
- Implemented via `solid-transition-group` `<TransitionGroup>` using Web Animations API.
|
|
615
|
-
|
|
616
|
-
---
|
|
617
|
-
|
|
618
|
-
## CONSTRAINTS & EDGE CASES
|
|
619
|
-
|
|
620
|
-
- In **controlled mode**, `value` must always be updated via `onChange`. The component does not force-sync controlled values into internal state.
|
|
621
|
-
- `mode='multiple'` enables search automatically — no need to set `showSearch` explicitly.
|
|
622
|
-
- `allowCustomValue` only works with `mode='multiple'`. It is `never` typed in single mode.
|
|
623
|
-
- `maxCount`, `maxTagCount`, `tagRender`, `menuItemSelectedIcon` are `never` in single mode — TypeScript will error.
|
|
624
|
-
- `fieldNames.options` must point to the array field for grouped options. Defaults to `'options'`.
|
|
625
|
-
- When `virtual={true}` is active, option heights are fixed (option: 32px, group label: 28px). Variable-height options will overflow or appear misaligned.
|
|
626
|
-
- `showSearch` as `object` enables search regardless of mode. Only `showSearch={false}` explicitly disables it.
|
|
627
|
-
- `autoClearSearchValue` defaults to `true` — search clears after each selection in multiple mode. Set to `false` in `showSearch` config to retain search text.
|
|
628
|
-
- `popupRender` wraps the entire dropdown content. Use `onMouseDown={e => e.stopPropagation()}` on custom footer elements to prevent dropdown from closing.
|
|
629
|
-
- The dropdown closes on outside click via blur event on `selectRootRef`. Elements outside will trigger close.
|
|
630
|
-
- `placement='top'` requires sufficient space above the trigger; no auto-flip logic is built in.
|
|
631
|
-
- `defaultActiveFirstOption` auto-focuses the first non-disabled option when dropdown opens or search text changes.
|
|
632
|
-
- `usePortal=true` renders the dropdown via a portal. Combine with `blockScroll=false` if the trigger is inside a scrollable container and the dropdown must stay aligned while scrolling.
|
|
633
|
-
|
|
634
|
-
---
|
|
635
|
-
|
|
636
|
-
## DO / DON'T
|
|
637
|
-
|
|
638
|
-
**DO**
|
|
639
|
-
|
|
640
|
-
- Import from barrel: `import { Select } from 'solid-tom-ui'`.
|
|
641
|
-
- Use `value` + `onChange` together for controlled usage.
|
|
642
|
-
- Use `class={{ root: '...', popup: '...' }}` (object shape) to extend styles.
|
|
643
|
-
- Set `virtual={true}` for lists with 500+ options.
|
|
644
|
-
- Set `onMouseDown={e => e.stopPropagation()}` on interactive elements inside `popupRender` footer to prevent premature close.
|
|
645
|
-
- Use `fieldNames` when options come from an API with non-standard field names.
|
|
646
|
-
- Use `showSearch={{ filterOption: false, onSearch }}` for server-side search.
|
|
647
|
-
|
|
648
|
-
**DON'T**
|
|
649
|
-
|
|
650
|
-
- Don't pass `maxCount`, `tagRender`, `allowCustomValue` without `mode='multiple'` — TypeScript will error.
|
|
651
|
-
- Don't use `virtual={true}` if you have variable-height options or complex JSX option renders requiring dynamic heights.
|
|
652
|
-
- Don't rely on `autoClearSearchValue` behavior being stable with controlled `showSearch.searchValue` — manage search state yourself when using `searchValue`.
|
|
653
|
-
- Don't mix `labelInValue={true}` with raw string values — `onChange` will receive `LabeledValue` objects.
|
|
654
|
-
- Don't import `SelectExample` in production — it's a demo component only.
|
|
655
|
-
- Don't set `placement` expecting auto-flip — choose the direction that always has available space.
|
|
656
|
-
- Don't write `variant="outlined"` (with "d") — the correct value is `"outline"`.
|
|
657
|
-
---
|
|
658
|
-
|
|
659
|
-
## Component Conventions
|
|
660
|
-
|
|
661
|
-
> **CSS encoding**: internal CSS classes use short encoded names (e.g. `sl01`, `sl02`) per project convention.
|
|
662
|
-
|
|
663
|
-
> **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.
|
|
1
|
+
## COMPONENT IDENTITY
|
|
2
|
+
- **Import**: `import { Select } from 'solid-tom-ui';`
|
|
3
|
+
- **Exports**: `Select` (named export), `SelectProps`, `SelectOptionType`, `OptGroupType`, `SelectValue`, `SelectFieldNames`, `SelectMethods`, `ShowSearchConfig`, `LabeledValue`, `LabelInValueType`, `FlattenOptionData`, `TagRenderProps` (type exports)
|
|
4
|
+
- **Framework**: SolidJS
|
|
5
|
+
- **Purpose**: Feature-rich select/dropdown input — supports single/multi select, search, grouped options, tags, and controlled/uncontrolled modes
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## PROP REFERENCE
|
|
10
|
+
|
|
11
|
+
### Core props (SelectBaseProps)
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
| -------------------------- | -------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| `options` | `(SelectOptionType \| OptGroupType)[]` | — | List of options or option groups |
|
|
16
|
+
| `value` | `SelectValue` | — | Controlled value. Must update via `onChange`. |
|
|
17
|
+
| `defaultValue` | `SelectValue` | — | Initial value for uncontrolled mode. |
|
|
18
|
+
| `mode` | `'single' \| 'multiple'` | `'single'` | Select mode. `'multiple'` enables multi-select. |
|
|
19
|
+
| `placeholder` | `string` | `'Select...'` | Placeholder text when no value is selected. |
|
|
20
|
+
| `disabled` | `boolean` | `false` | Disable all interactions. |
|
|
21
|
+
| `loading` | `boolean` | `false` | Show loading spinner instead of arrow icon. |
|
|
22
|
+
| `open` | `boolean` | — | Controlled dropdown open state. |
|
|
23
|
+
| `defaultOpen` | `boolean` | `false` | Initial dropdown open state (uncontrolled). |
|
|
24
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Visual size variant. |
|
|
25
|
+
| `variant` | `'outline' \| 'filled' \| 'borderless' \| 'underlined'` | `'outline'` | Border/fill style variant. |
|
|
26
|
+
| `color` | `BaseColorProps` | `'error'` | Theme color token (affects input border, dropdown highlights). |
|
|
27
|
+
| `placement` | `'top' \| 'bottom'` | `'bottom'` | Dropdown placement direction. |
|
|
28
|
+
| `allowClear` | `boolean \| { clearIcon?: SolidComponent }` | — | Show clear button when value is selected. |
|
|
29
|
+
| `showSearch` | `boolean \| ShowSearchConfig` | auto | Enable search input in dropdown. Auto-enabled when `mode='multiple'`. |
|
|
30
|
+
| `virtual` | `boolean` | `false` | Enable virtual scrolling for large option lists. |
|
|
31
|
+
| `listHeight` | `number` | `256` | Max height of dropdown list in pixels. |
|
|
32
|
+
| `fieldNames` | `SelectFieldNames` | — | Custom field name mapping for option objects (see below). |
|
|
33
|
+
| `labelInValue` | `boolean` | `false` | When `true`, `onChange` receives `LabeledValue` objects instead of raw values. |
|
|
34
|
+
| `notFoundContent` | `SolidComponent` | `'Not Found'` | Content shown when no options match search. |
|
|
35
|
+
| `defaultActiveFirstOption` | `boolean` | `false` | Auto-activate first option when dropdown opens. |
|
|
36
|
+
| `prefix` | `SolidComponent` | — | Icon/element rendered before the selector. |
|
|
37
|
+
| `suffixIcon` | `SolidComponent` | — | Custom dropdown arrow icon. |
|
|
38
|
+
| `removeIcon` | `SolidComponent` | — | Custom remove icon for tags (multiple mode). |
|
|
39
|
+
| `popupRender` | `(originNode: JSXElement) => JSXElement` | — | Wrap/augment dropdown content. |
|
|
40
|
+
| `optionRender` | `(option: FlattenOptionData, info: { index: number }) => JSXElement` | — | Custom render for each option item. |
|
|
41
|
+
| `labelRender` | `(props: LabelInValueType) => JSXElement` | — | Custom render for the selected label in trigger. |
|
|
42
|
+
| `class` | `Partial<Record<'root' \| 'popup', string>>` | — | Custom class for root element and popup. |
|
|
43
|
+
### Single-mode-only props (mode='single' or omitted)
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Description |
|
|
46
|
+
| -------------- | ---------------------------------- | ------------------------ |
|
|
47
|
+
| `value` | `string \| number \| LabeledValue` | Controlled single value. |
|
|
48
|
+
| `defaultValue` | `string \| number \| LabeledValue` | Initial single value. |
|
|
49
|
+
|
|
50
|
+
### Multiple-mode-only props (mode='multiple')
|
|
51
|
+
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
| ---------------------- | ----------------------------------------------------------------- | -------------- | -------------------------------------------------------- |
|
|
54
|
+
| `value` | `string[] \| number[] \| LabeledValue[]` | — | Controlled array of selected values. |
|
|
55
|
+
| `defaultValue` | `string[] \| number[] \| LabeledValue[]` | — | Initial array of selected values. |
|
|
56
|
+
| `allowCustomValue` | `boolean` | `false` | Allow typing and selecting values not in `options` list. |
|
|
57
|
+
| `maxCount` | `number` | — | Maximum number of selectable items. |
|
|
58
|
+
| `maxTagCount` | `number` | — | Max visible tags; excess shown in `+N more...` tooltip. |
|
|
59
|
+
| `maxTagPlaceholder` | `SolidComponent \| ((omitted: LabeledValue[]) => SolidComponent)` | `'+N more...'` | Custom content for omitted tags badge. |
|
|
60
|
+
| `maxTagTextLength` | `number` | — | Truncate tag labels to this character length. |
|
|
61
|
+
| `tagRender` | `(props: TagRenderProps) => JSXElement` | — | Custom render for each selected tag in trigger. |
|
|
62
|
+
| `menuItemSelectedIcon` | `SolidComponent` | checkmark icon | Custom icon shown on selected options in dropdown. |
|
|
63
|
+
|
|
64
|
+
### Event props
|
|
65
|
+
|
|
66
|
+
| Prop | Type | Description |
|
|
67
|
+
| ---------------- | ------------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
68
|
+
| `onChange` | `(value: SelectValue, option: SelectOptionType \| SelectOptionType[]) => void` | Fires on selection change. |
|
|
69
|
+
| `onSelect` | `(value: string \| number \| LabeledValue, option: SelectOptionType) => void` | Fires when an item is selected (not deselected). |
|
|
70
|
+
| `onDeselect` | `(value: string \| number \| LabeledValue) => void` | Fires when an item is deselected (multiple mode). |
|
|
71
|
+
| `onClear` | `() => void` | Fires when clear button is clicked. |
|
|
72
|
+
| `onOpenChange` | `(open: boolean) => void` | Fires when dropdown opens or closes. |
|
|
73
|
+
| `onFocus` | `(event: FocusEvent) => void` | Fires on focus. |
|
|
74
|
+
| `onBlur` | `(event: FocusEvent) => void` | Fires on blur. |
|
|
75
|
+
| `onActive` | `(value: string \| number \| LabeledValue) => void` | Fires when keyboard navigation activates an option. |
|
|
76
|
+
| `onInputKeyDown` | `(event: KeyboardEvent) => void` | Fires on keydown in the trigger/search input. |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## TYPE REFERENCE
|
|
81
|
+
|
|
82
|
+
### `SelectOptionType`
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
interface SelectOptionType {
|
|
86
|
+
label?: SolidComponent; // display text or JSX
|
|
87
|
+
value?: string | number; // option value (used internally)
|
|
88
|
+
disabled?: boolean; // disable individual option
|
|
89
|
+
class?: string; // extra CSS class on option element
|
|
90
|
+
title?: string; // HTML title attribute on option element
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `OptGroupType` (grouped options)
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface OptGroupType {
|
|
98
|
+
key?: string;
|
|
99
|
+
label?: SolidComponent; // group header label
|
|
100
|
+
options?: SelectOptionType[]; // child options in the group
|
|
101
|
+
class?: string;
|
|
102
|
+
title?: string;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `SelectFieldNames` (custom field mapping)
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
interface SelectFieldNames {
|
|
110
|
+
label?: string; // default: 'label'
|
|
111
|
+
value?: string; // default: 'value'
|
|
112
|
+
options?: string; // default: 'options' (for group children)
|
|
113
|
+
groupLabel?: string; // default: 'label' (for group header)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `ShowSearchConfig`
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
interface ShowSearchConfig {
|
|
121
|
+
autoClearSearchValue?: boolean; // clear search on select (default: true)
|
|
122
|
+
filterOption?: boolean | ((inputValue: string, option: SelectOptionType) => boolean);
|
|
123
|
+
filterSort?: (
|
|
124
|
+
optA: SelectOptionType,
|
|
125
|
+
optB: SelectOptionType,
|
|
126
|
+
info: { searchValue: string },
|
|
127
|
+
) => number;
|
|
128
|
+
optionFilterProp?: string | string[]; // field(s) to filter on (default: 'value')
|
|
129
|
+
searchValue?: string; // controlled search input value
|
|
130
|
+
onSearch?: (value: string) => void; // fires on search input change
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `TagRenderProps`
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
interface TagRenderProps {
|
|
138
|
+
label: SolidComponent;
|
|
139
|
+
value: string | number;
|
|
140
|
+
closable: boolean;
|
|
141
|
+
onClose: () => void;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `LabeledValue`
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
interface LabeledValue {
|
|
149
|
+
value: string | number;
|
|
150
|
+
label: SolidComponent;
|
|
151
|
+
key?: string;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## BEHAVIORAL RULES
|
|
158
|
+
|
|
159
|
+
### Controlled vs Uncontrolled
|
|
160
|
+
|
|
161
|
+
- **Uncontrolled**: omit `value`. Internal signal manages state. Use `defaultValue` for initial state.
|
|
162
|
+
- **Controlled**: supply `value`. Component reads it reactively. **You must update the signal yourself in `onChange`**; the component will not auto-sync.
|
|
163
|
+
|
|
164
|
+
### Search behavior
|
|
165
|
+
|
|
166
|
+
- `showSearch={true}` → enables search input with default `filterOption` (matches against `value` field, case-insensitive).
|
|
167
|
+
- `showSearch={object}` → passes `ShowSearchConfig` for full control.
|
|
168
|
+
- In `mode='multiple'`, search is auto-enabled (no need to explicitly set `showSearch`).
|
|
169
|
+
- `autoClearSearchValue` (default `true`) clears search text after each selection.
|
|
170
|
+
|
|
171
|
+
### Filtering logic
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
if filterOption === false → show all options (server-side filtering)
|
|
175
|
+
if filterOption is function → use custom filter function
|
|
176
|
+
else → filter by optionFilterProp fields, lowercase includes match
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Option flattening & groups
|
|
180
|
+
|
|
181
|
+
- `OptGroupType` entries (those with an `options` array) are flattened internally.
|
|
182
|
+
- Groups are rendered with a group label header followed by their options.
|
|
183
|
+
- During search, grouping is collapsed and all matching items are shown in a flat list.
|
|
184
|
+
|
|
185
|
+
### Virtual scroll
|
|
186
|
+
|
|
187
|
+
- `virtual={true}` activates `@tanstack/solid-virtual` for efficient rendering of large lists.
|
|
188
|
+
- Recommended for option lists > 500 items.
|
|
189
|
+
- Item height defaults: options = 32px, group labels = 28px. Heights are **fixed** — variable-height options will misalign.
|
|
190
|
+
|
|
191
|
+
### Multiple mode — tag overflow
|
|
192
|
+
|
|
193
|
+
- `maxTagCount={N}` → shows first N tags; remaining are hidden behind a `+N more...` badge.
|
|
194
|
+
- Hovering the badge reveals a tooltip showing all hidden tags with close buttons.
|
|
195
|
+
- `maxTagPlaceholder` overrides the badge content.
|
|
196
|
+
|
|
197
|
+
### Keyboard navigation
|
|
198
|
+
|
|
199
|
+
| Key | Behavior |
|
|
200
|
+
| ----------- | --------------------------------------------------- |
|
|
201
|
+
| `ArrowDown` | Open dropdown / move active option down |
|
|
202
|
+
| `ArrowUp` | Open dropdown / move active option up |
|
|
203
|
+
| `Enter` | Select active option (or open dropdown) |
|
|
204
|
+
| `Space` | Open dropdown (ignored if search input is focused) |
|
|
205
|
+
| `Escape` | Close dropdown, return focus to trigger |
|
|
206
|
+
| `Backspace` | In multiple mode with empty search: remove last tag |
|
|
207
|
+
| `Tab` | Close dropdown |
|
|
208
|
+
|
|
209
|
+
### `allowCustomValue` (multiple mode only)
|
|
210
|
+
|
|
211
|
+
- Allows user to type arbitrary values not in `options`.
|
|
212
|
+
- Press `Enter` when no option is active to add the current search text as a tag.
|
|
213
|
+
- Custom values are stored internally and appear as synthetic `FlatOption` entries.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## USAGE PATTERNS
|
|
218
|
+
|
|
219
|
+
### 1. Basic single select (uncontrolled)
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { Select } from 'solid-tom-ui';
|
|
223
|
+
|
|
224
|
+
const options = [
|
|
225
|
+
{ label: 'Option 1', value: '1' },
|
|
226
|
+
{ label: 'Option 2', value: '2' },
|
|
227
|
+
{ label: 'Option 3', value: '3', disabled: true },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
<Select options={options} placeholder="Select one..." />;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 2. Controlled single select
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
const [value, setValue] = createSignal<string>('1');
|
|
237
|
+
|
|
238
|
+
<Select options={options} value={value()} onChange={val => setValue(val as string)} />;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 3. Multiple select (uncontrolled)
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
<Select
|
|
245
|
+
mode="multiple"
|
|
246
|
+
options={options}
|
|
247
|
+
placeholder="Select multiple..."
|
|
248
|
+
allowClear
|
|
249
|
+
onChange={val => console.log(val)}
|
|
250
|
+
/>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 4. Controlled multiple select
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
const [values, setValues] = createSignal<string[]>(['1', '2']);
|
|
257
|
+
|
|
258
|
+
<Select
|
|
259
|
+
mode="multiple"
|
|
260
|
+
options={options}
|
|
261
|
+
value={values()}
|
|
262
|
+
onChange={val => setValues(val as string[])}
|
|
263
|
+
allowClear
|
|
264
|
+
/>;
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 5. Single select with search
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<Select
|
|
271
|
+
options={largeOptions}
|
|
272
|
+
showSearch={{
|
|
273
|
+
filterOption: (input, option) =>
|
|
274
|
+
String(option?.label).toLowerCase().includes(input.toLowerCase()),
|
|
275
|
+
optionFilterProp: 'label',
|
|
276
|
+
}}
|
|
277
|
+
placeholder="Search to select..."
|
|
278
|
+
/>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 6. Option groups
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
const groupedOptions = [
|
|
285
|
+
{
|
|
286
|
+
label: 'Managers',
|
|
287
|
+
options: [
|
|
288
|
+
{ label: 'Alice', value: 'alice' },
|
|
289
|
+
{ label: 'Bob', value: 'bob' },
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
label: 'Engineers',
|
|
294
|
+
options: [
|
|
295
|
+
{ label: 'Carol', value: 'carol' },
|
|
296
|
+
{ label: 'Dave', value: 'dave' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
<Select options={groupedOptions} placeholder="Select a person..." />;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 7. Custom field names (non-standard option shape)
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
const countryOptions = [
|
|
308
|
+
{ name: 'Vietnam', id: 'vn' },
|
|
309
|
+
{ name: 'Japan', id: 'jp' },
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
<Select
|
|
313
|
+
options={countryOptions as any}
|
|
314
|
+
fieldNames={{ label: 'name', value: 'id' }}
|
|
315
|
+
placeholder="Select country..."
|
|
316
|
+
/>;
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 8. Label in value
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
<Select
|
|
323
|
+
labelInValue
|
|
324
|
+
defaultValue={{ value: '1', label: 'Option 1' }}
|
|
325
|
+
options={options}
|
|
326
|
+
onChange={val => {
|
|
327
|
+
const { value, label } = val as LabeledValue;
|
|
328
|
+
console.log(value, label);
|
|
329
|
+
}}
|
|
330
|
+
/>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### 9. Virtual scroll (large option lists)
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
const manyOptions = Array.from({ length: 10000 }, (_, i) => ({
|
|
337
|
+
label: `Option ${i + 1}`,
|
|
338
|
+
value: `opt-${i + 1}`,
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
<Select
|
|
342
|
+
options={manyOptions}
|
|
343
|
+
showSearch
|
|
344
|
+
virtual
|
|
345
|
+
listHeight={256}
|
|
346
|
+
placeholder="Search 10,000 items..."
|
|
347
|
+
/>;
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 10. Max selections (multiple mode)
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
<Select mode="multiple" options={options} maxCount={3} placeholder="Select up to 3..." />
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### 11. Max tag count with custom placeholder
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
<Select
|
|
360
|
+
mode="multiple"
|
|
361
|
+
options={colorOptions}
|
|
362
|
+
defaultValue={['red', 'green', 'blue', 'yellow']}
|
|
363
|
+
maxTagCount={2}
|
|
364
|
+
maxTagPlaceholder={omitted => `and ${omitted.length} more...`}
|
|
365
|
+
/>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### 12. Allow custom values (free tagging)
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
<Select
|
|
372
|
+
mode="multiple"
|
|
373
|
+
allowCustomValue
|
|
374
|
+
options={langOptions}
|
|
375
|
+
placeholder="Type or select tags..."
|
|
376
|
+
/>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 13. Custom option render
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
<Select
|
|
383
|
+
options={options}
|
|
384
|
+
optionRender={(option, { index }) => (
|
|
385
|
+
<div class="flex items-center gap-2">
|
|
386
|
+
<span class="text-xs font-bold">{index + 1}.</span>
|
|
387
|
+
<span>{option.label as string}</span>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
/>
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### 14. Custom tag render (multiple mode)
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
<Select
|
|
397
|
+
mode="multiple"
|
|
398
|
+
options={options}
|
|
399
|
+
tagRender={props => (
|
|
400
|
+
<span class="badge badge-primary gap-1">
|
|
401
|
+
{props.label as string}
|
|
402
|
+
{props.closable && (
|
|
403
|
+
<span onClick={props.onClose} class="cursor-pointer">
|
|
404
|
+
×
|
|
405
|
+
</span>
|
|
406
|
+
)}
|
|
407
|
+
</span>
|
|
408
|
+
)}
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 15. Custom label render (selected value in trigger)
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
<Select
|
|
416
|
+
options={langOptions}
|
|
417
|
+
labelRender={props => (
|
|
418
|
+
<span class="flex items-center gap-1">
|
|
419
|
+
<span class="text-primary font-mono text-xs">[{props.value}]</span>
|
|
420
|
+
<span>{props.label as string}</span>
|
|
421
|
+
</span>
|
|
422
|
+
)}
|
|
423
|
+
/>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 16. Popup render (add item footer)
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
const [opts, setOpts] = createSignal([...baseOptions]);
|
|
430
|
+
const [newName, setNewName] = createSignal('');
|
|
431
|
+
|
|
432
|
+
<Select
|
|
433
|
+
options={opts()}
|
|
434
|
+
popupRender={menu => (
|
|
435
|
+
<div>
|
|
436
|
+
{menu}
|
|
437
|
+
<div class="flex gap-2 border-t p-2" onMouseDown={e => e.stopPropagation()}>
|
|
438
|
+
<input
|
|
439
|
+
class="input input-xs flex-1"
|
|
440
|
+
value={newName()}
|
|
441
|
+
onInput={e => setNewName(e.currentTarget.value)}
|
|
442
|
+
onKeyDown={e => {
|
|
443
|
+
if (e.key === 'Enter' && newName().trim()) {
|
|
444
|
+
const n = newName().trim();
|
|
445
|
+
setOpts(prev => [...prev, { label: n, value: n }]);
|
|
446
|
+
setNewName('');
|
|
447
|
+
}
|
|
448
|
+
}}
|
|
449
|
+
placeholder="New item..."
|
|
450
|
+
/>
|
|
451
|
+
<button
|
|
452
|
+
class="btn btn-xs btn-primary"
|
|
453
|
+
onClick={() => {
|
|
454
|
+
if (newName().trim()) {
|
|
455
|
+
const n = newName().trim();
|
|
456
|
+
setOpts(prev => [...prev, { label: n, value: n }]);
|
|
457
|
+
setNewName('');
|
|
458
|
+
}
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
Add
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
/>;
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 17. Sizes
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
<Select size="sm" options={options} placeholder="Small" />
|
|
473
|
+
<Select size="md" options={options} placeholder="Medium (default)" />
|
|
474
|
+
<Select size="lg" options={options} placeholder="Large" />
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 18. Variants
|
|
478
|
+
|
|
479
|
+
```tsx
|
|
480
|
+
<Select variant="outline" options={options} placeholder="Outline (default)" />
|
|
481
|
+
<Select variant="filled" options={options} placeholder="Filled" />
|
|
482
|
+
<Select variant="borderless" options={options} placeholder="Borderless" />
|
|
483
|
+
<Select variant="underlined" options={options} placeholder="Underlined" />
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 19. Placement (dropdown direction)
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<Select options={options} placement="top" placeholder="Opens upward" />
|
|
490
|
+
<Select options={options} placement="bottom" placeholder="Opens downward (default)" />
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### 20. Disabled state
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
<Select disabled defaultValue="1" options={options} />
|
|
497
|
+
<Select disabled mode="multiple" defaultValue={['1', '2']} options={options} />
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### 21. Loading state
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
<Select loading placeholder="Loading options..." options={[]} />
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 22. Allow clear
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
<Select allowClear defaultValue="1" options={options} />
|
|
510
|
+
// With custom clear icon:
|
|
511
|
+
<Select allowClear={{ clearIcon: <span>✕</span> }} options={options} />
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### 23. Prefix icon
|
|
515
|
+
|
|
516
|
+
```tsx
|
|
517
|
+
<Select
|
|
518
|
+
prefix={<DynamicIcon name="search" size={14} />}
|
|
519
|
+
showSearch
|
|
520
|
+
options={options}
|
|
521
|
+
placeholder="Search..."
|
|
522
|
+
/>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### 24. Controlled open state
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
const [open, setOpen] = createSignal(false);
|
|
529
|
+
|
|
530
|
+
<Select open={open()} onOpenChange={setOpen} options={options} />;
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### 25. Server-side search (disable client filter)
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
const [searchResults, setSearchResults] = createSignal<SelectOptionType[]>([]);
|
|
537
|
+
|
|
538
|
+
<Select
|
|
539
|
+
options={searchResults()}
|
|
540
|
+
showSearch={{
|
|
541
|
+
filterOption: false, // disable client-side filtering
|
|
542
|
+
onSearch: async q => {
|
|
543
|
+
const results = await fetchOptions(q);
|
|
544
|
+
setSearchResults(results);
|
|
545
|
+
},
|
|
546
|
+
}}
|
|
547
|
+
placeholder="Search server..."
|
|
548
|
+
/>;
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### 26. Max tag text length
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
<Select
|
|
555
|
+
mode="multiple"
|
|
556
|
+
options={options}
|
|
557
|
+
maxTagTextLength={8}
|
|
558
|
+
defaultValue={['very-long-label-value']}
|
|
559
|
+
/>
|
|
560
|
+
// Labels longer than 8 chars are truncated: "very-lon..."
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## CSS CLASSES (public API for customization)
|
|
566
|
+
|
|
567
|
+
| Class | Applied to |
|
|
568
|
+
| ---------------------------------- | ------------------------------------- |
|
|
569
|
+
| `sui-select` | Root trigger `<div>` |
|
|
570
|
+
| `sui-select-outline` | Variant: outline |
|
|
571
|
+
| `sui-select-filled` | Variant: filled |
|
|
572
|
+
| `sui-select-borderless` | Variant: borderless |
|
|
573
|
+
| `sui-select-underlined` | Variant: underlined |
|
|
574
|
+
| `sui-select-sm/md/lg` | Size modifier |
|
|
575
|
+
| `sui-select-focused` | Root when dropdown is open |
|
|
576
|
+
| `sui-select-open` | Root when dropdown is open |
|
|
577
|
+
| `sui-select-multiple` | Root in multiple mode |
|
|
578
|
+
| `sui-select-disabled` | Root when `disabled=true` |
|
|
579
|
+
| `sui-select-prefix` | Prefix icon wrapper `<span>` |
|
|
580
|
+
| `sui-select-selector` | Inner selector container |
|
|
581
|
+
| `sui-select-selection-single` | Single-mode value display |
|
|
582
|
+
| `sui-select-selection-item` | Selected label in single mode |
|
|
583
|
+
| `sui-select-placeholder` | Placeholder `<span>` |
|
|
584
|
+
| `sui-select-selection-multiple` | Multiple-mode tags container |
|
|
585
|
+
| `sui-select-tag` | Each tag in multiple mode |
|
|
586
|
+
| `sui-select-tag-content` | Tag label text |
|
|
587
|
+
| `sui-select-tag-remove` | Tag remove button |
|
|
588
|
+
| `sui-select-tag-omitted` | The `+N more...` badge |
|
|
589
|
+
| `sui-select-suffix` | Suffix area (clear + arrow) |
|
|
590
|
+
| `sui-select-clear` | Clear button `<span>` |
|
|
591
|
+
| `sui-select-arrow` | Dropdown arrow icon |
|
|
592
|
+
| `sui-select-arrow-open` | Arrow when dropdown is open (rotated) |
|
|
593
|
+
| `sui-select-option-list` | Dropdown options container |
|
|
594
|
+
| `sui-select-option` | Each option item |
|
|
595
|
+
| `sui-select-option-selected` | Selected option |
|
|
596
|
+
| `sui-select-option-active` | Keyboard-active option |
|
|
597
|
+
| `sui-select-option-disabled` | Disabled option |
|
|
598
|
+
| `sui-select-option-grouped` | Option inside a group |
|
|
599
|
+
| `sui-select-option-content` | Option label wrapper |
|
|
600
|
+
| `sui-select-option-state` | Checkmark icon wrapper |
|
|
601
|
+
| `sui-select-option-group-label` | Group header `<div>` |
|
|
602
|
+
| `sui-select-dropdown-search-input` | Search `<input>` in dropdown |
|
|
603
|
+
| `sui-select-empty` | Empty/not-found state `<div>` |
|
|
604
|
+
|
|
605
|
+
To add custom class to root or popup: `class={{ root: 'my-root', popup: 'my-popup' }}`.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## ANIMATION
|
|
610
|
+
|
|
611
|
+
- **Tags (multiple mode)**:
|
|
612
|
+
- Enter: slide from right (`translateX(100% → 0)`, opacity 0→1, 300ms ease-out).
|
|
613
|
+
- Exit: scale-out from left origin (`scale(1→0)`, 300ms ease-in).
|
|
614
|
+
- Implemented via `solid-transition-group` `<TransitionGroup>` using Web Animations API.
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## CONSTRAINTS & EDGE CASES
|
|
619
|
+
|
|
620
|
+
- In **controlled mode**, `value` must always be updated via `onChange`. The component does not force-sync controlled values into internal state.
|
|
621
|
+
- `mode='multiple'` enables search automatically — no need to set `showSearch` explicitly.
|
|
622
|
+
- `allowCustomValue` only works with `mode='multiple'`. It is `never` typed in single mode.
|
|
623
|
+
- `maxCount`, `maxTagCount`, `tagRender`, `menuItemSelectedIcon` are `never` in single mode — TypeScript will error.
|
|
624
|
+
- `fieldNames.options` must point to the array field for grouped options. Defaults to `'options'`.
|
|
625
|
+
- When `virtual={true}` is active, option heights are fixed (option: 32px, group label: 28px). Variable-height options will overflow or appear misaligned.
|
|
626
|
+
- `showSearch` as `object` enables search regardless of mode. Only `showSearch={false}` explicitly disables it.
|
|
627
|
+
- `autoClearSearchValue` defaults to `true` — search clears after each selection in multiple mode. Set to `false` in `showSearch` config to retain search text.
|
|
628
|
+
- `popupRender` wraps the entire dropdown content. Use `onMouseDown={e => e.stopPropagation()}` on custom footer elements to prevent dropdown from closing.
|
|
629
|
+
- The dropdown closes on outside click via blur event on `selectRootRef`. Elements outside will trigger close.
|
|
630
|
+
- `placement='top'` requires sufficient space above the trigger; no auto-flip logic is built in.
|
|
631
|
+
- `defaultActiveFirstOption` auto-focuses the first non-disabled option when dropdown opens or search text changes.
|
|
632
|
+
- `usePortal=true` renders the dropdown via a portal. Combine with `blockScroll=false` if the trigger is inside a scrollable container and the dropdown must stay aligned while scrolling.
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## DO / DON'T
|
|
637
|
+
|
|
638
|
+
**DO**
|
|
639
|
+
|
|
640
|
+
- Import from barrel: `import { Select } from 'solid-tom-ui'`.
|
|
641
|
+
- Use `value` + `onChange` together for controlled usage.
|
|
642
|
+
- Use `class={{ root: '...', popup: '...' }}` (object shape) to extend styles.
|
|
643
|
+
- Set `virtual={true}` for lists with 500+ options.
|
|
644
|
+
- Set `onMouseDown={e => e.stopPropagation()}` on interactive elements inside `popupRender` footer to prevent premature close.
|
|
645
|
+
- Use `fieldNames` when options come from an API with non-standard field names.
|
|
646
|
+
- Use `showSearch={{ filterOption: false, onSearch }}` for server-side search.
|
|
647
|
+
|
|
648
|
+
**DON'T**
|
|
649
|
+
|
|
650
|
+
- Don't pass `maxCount`, `tagRender`, `allowCustomValue` without `mode='multiple'` — TypeScript will error.
|
|
651
|
+
- Don't use `virtual={true}` if you have variable-height options or complex JSX option renders requiring dynamic heights.
|
|
652
|
+
- Don't rely on `autoClearSearchValue` behavior being stable with controlled `showSearch.searchValue` — manage search state yourself when using `searchValue`.
|
|
653
|
+
- Don't mix `labelInValue={true}` with raw string values — `onChange` will receive `LabeledValue` objects.
|
|
654
|
+
- Don't import `SelectExample` in production — it's a demo component only.
|
|
655
|
+
- Don't set `placement` expecting auto-flip — choose the direction that always has available space.
|
|
656
|
+
- Don't write `variant="outlined"` (with "d") — the correct value is `"outline"`.
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Component Conventions
|
|
660
|
+
|
|
661
|
+
> **CSS encoding**: internal CSS classes use short encoded names (e.g. `sl01`, `sl02`) per project convention.
|
|
662
|
+
|
|
663
|
+
> **Unique IDs**: if this component needs to generate HTML `id` attributes, always use `createUniqueId()` from `solid-js` — never `Math.random()` or `Date.now()`.
|