svelte-5-select 1.0.2 β 2.0.0
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 +239 -109
- package/dist/ChevronIcon.svelte +8 -13
- package/dist/ClearIcon.svelte +3 -11
- package/dist/LoadingIcon.svelte +9 -2
- package/dist/Select.svelte +954 -436
- package/dist/Select.svelte.d.ts +37 -5
- package/dist/aria-handlers.svelte.d.ts +4 -1
- package/dist/aria-handlers.svelte.js +19 -8
- package/dist/filter.d.ts +10 -2
- package/dist/filter.js +14 -7
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/keyboard-navigation.svelte.d.ts +7 -2
- package/dist/keyboard-navigation.svelte.js +285 -47
- package/dist/no-styles/ChevronIcon.svelte +11 -0
- package/dist/no-styles/ChevronIcon.svelte.d.ts +26 -0
- package/dist/no-styles/ClearIcon.svelte +8 -0
- package/dist/no-styles/ClearIcon.svelte.d.ts +26 -0
- package/dist/no-styles/LoadingIcon.svelte +13 -0
- package/dist/no-styles/LoadingIcon.svelte.d.ts +26 -0
- package/dist/no-styles/Select.svelte +1383 -0
- package/dist/no-styles/Select.svelte.d.ts +38 -0
- package/dist/select-state.svelte.d.ts +15 -0
- package/dist/select-state.svelte.js +161 -0
- package/dist/styles/default.css +112 -71
- package/dist/tailwind.css +120 -17
- package/dist/types.d.ts +459 -129
- package/dist/use-hover.svelte.d.ts +3 -7
- package/dist/use-hover.svelte.js +91 -36
- package/dist/use-load-options.svelte.d.ts +18 -3
- package/dist/use-load-options.svelte.js +333 -42
- package/dist/use-value.svelte.d.ts +2 -11
- package/dist/use-value.svelte.js +322 -111
- package/dist/utils.d.ts +19 -8
- package/dist/utils.js +52 -8
- package/package.json +117 -94
package/README.md
CHANGED
|
@@ -14,11 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## Demos
|
|
16
16
|
|
|
17
|
-
[π₯ Examples of every
|
|
18
|
-
|
|
19
|
-
[β¨ REPL: Simple β¨] Doesn't exist yet
|
|
20
|
-
|
|
21
|
-
[π REPL: Show me everything πΊ]Doesn't exist yet
|
|
17
|
+
[π₯ Examples of every event and snippet, plus the major props π₯](https://github.com/IDontKnowMyUsername/svelte-5-select/tree/master/src/routes/examples)
|
|
22
18
|
|
|
23
19
|
## Installation
|
|
24
20
|
|
|
@@ -38,9 +34,9 @@ List position and floating is powered by `floating-ui`, see their [package-entry
|
|
|
38
34
|
|
|
39
35
|
| Prop | Type | Default | Description |
|
|
40
36
|
| ---------------------- | --------- | --------------- | -------------------------------------------------------------- |
|
|
41
|
-
| items | `any[]` | `
|
|
42
|
-
| value | `any` | `
|
|
43
|
-
| justValue | `any` | `
|
|
37
|
+
| items | `any[]` | `null` | Array of items available to display / filter |
|
|
38
|
+
| value | `any` | `undefined` | Selected value(s); an emptied selection is always `undefined` |
|
|
39
|
+
| justValue | `any` | `undefined` | Raw `itemId` value(s) of the selection (pairs with `useJustValue`); bindable β writing it while no selection exists hydrates `value`; while one exists it stays derived from `value` |
|
|
44
40
|
| itemId | `string` | `value` | Override default identifier |
|
|
45
41
|
| label | `string` | `label` | Override default label |
|
|
46
42
|
| id | `string` | `null` | id attr for input field |
|
|
@@ -51,14 +47,14 @@ List position and floating is powered by `floating-ui`, see their [package-entry
|
|
|
51
47
|
| class | `string` | `''` | container classes |
|
|
52
48
|
| containerStyles | `string` | `''` | Add inline styles to container |
|
|
53
49
|
| clearable | `boolean` | `true` | Enable clearing of value(s) |
|
|
54
|
-
| disabled | `boolean` | `false` | Disable select
|
|
50
|
+
| disabled | `boolean` | `false` | Disable select: closes the list and releases focus; keeps the selection, except with `loadOptions` (value and loaded items are cleared) |
|
|
55
51
|
| multiple | `boolean` | `false` | Enable multi-select |
|
|
56
|
-
| searchable | `boolean` | `true` | If `false` search/filtering is disabled
|
|
52
|
+
| searchable | `boolean` | `true` | If `false` search/filtering is disabled; typing moves to the next matching option (type-ahead) |
|
|
57
53
|
| groupHeaderSelectable | `boolean` | `false` | Enable selectable group headers |
|
|
58
|
-
| focused | `boolean` | `false` |
|
|
54
|
+
| focused | `boolean` | `false` | Input focus; set `true` to focus the input, `false` to blur it and close the list |
|
|
59
55
|
| listAutoWidth | `boolean` | `true` | If `false` will ignore width of select |
|
|
60
56
|
| showChevron | `boolean` | `false` | Show chevron |
|
|
61
|
-
| inputAttributes | `object` | `{}` | Pass in HTML attributes to Select's input
|
|
57
|
+
| inputAttributes | `object` | `{}` | Pass in HTML attributes to Select's input; `on*` handlers run after the component's own, not instead of them |
|
|
62
58
|
| placeholderAlwaysShow | `boolean` | `false` | When `multiple` placeholder text will always show |
|
|
63
59
|
| loading | `boolean` | `false` | Shows `loading-icon`. `loadOptions` will override this |
|
|
64
60
|
| listOffset | `number` | `5` | `px` space between select and list |
|
|
@@ -68,45 +64,97 @@ List position and floating is powered by `floating-ui`, see their [package-entry
|
|
|
68
64
|
| name | `string` | `null` | Name attribute of hidden input, helpful for form actions |
|
|
69
65
|
| required | `boolean` | `false` | If `Select` is within a `<form>` will restrict form submission |
|
|
70
66
|
| multiFullItemClearable | `boolean` | `false` | When `multiple` selected items will clear on click |
|
|
71
|
-
| closeListOnChange | `boolean` | `true` | After
|
|
72
|
-
| clearFilterTextOnBlur | `boolean` | `true` | If `false`, `filterText` value is preserved on
|
|
67
|
+
| closeListOnChange | `boolean` | `true` | After selection the list will close |
|
|
68
|
+
| clearFilterTextOnBlur | `boolean` | `true` | If `false`, `filterText` value is preserved on blur |
|
|
69
|
+
| useJustValue | `boolean` | `false` | Hidden form input uses `justValue` (raw id) instead of JSON |
|
|
70
|
+
| filterSelectedItems | `boolean` | `true` | When `multiple`, hide selected items from the list |
|
|
71
|
+
| inputStyles | `string` | `''` | Add inline styles to the input |
|
|
72
|
+
| listStyles | `string` | `''` | Add inline styles to the list |
|
|
73
|
+
| hoverItemIndex | `number` | `0` | Index of the currently hovered item (bindable) |
|
|
74
|
+
| loadOptionsDeps | `any[]` | `[]` | When these values change, `loadOptions` re-fires. Compared by `===` β pass primitives or stable references, not inline literals |
|
|
75
|
+
| ariaLabel | `string` | `undefined` | Explicit `aria-label` for the input (and the listbox); when omitted, a `<label for={id}>` (or the placeholder, as a last resort) names it |
|
|
76
|
+
| ariaErrorMessage | `string` | `undefined` | id of your error element; wired to the input's `aria-errormessage` while `hasError` is `true` |
|
|
77
|
+
| ariaClearSelectLabel | `string` | `'Clear selection'` | `aria-label` for the clear-all button |
|
|
78
|
+
| ariaRemoveItemLabel | `(label: string) => string` | ``(label) => `Remove ${label}` `` | `aria-label` for each multi-select tag's remove button |
|
|
79
|
+
| ariaCleared | `() => string` | see below | Announcement after the selection is cleared |
|
|
80
|
+
| ariaEmpty | `() => string` | see below | Announcement when the open list has no options |
|
|
81
|
+
| ariaLoading | `() => string` | see below | Announcement while the open list is loading |
|
|
82
|
+
| ariaFocused | `() => string` | see below | Announcement when the input receives focus |
|
|
83
|
+
| ariaListOpen | `(label: string, count: number) => string` | see below | Announcement when the list opens on a focused option |
|
|
84
|
+
| ariaValues | `(values: string) => string` | see below | Announcement naming the current selection |
|
|
85
|
+
|
|
86
|
+
The `aria*` text-builder defaults are shown in the [A11y](#a11y-accessibility) section. See [Function props](#function-props) for the overridable behavior functions (`loadOptions`, `filter`, `groupBy`, and friends).
|
|
87
|
+
|
|
88
|
+
### Bindable props
|
|
89
|
+
|
|
90
|
+
`value`, `filterText`, `items`, `listOpen`, `loading`, `focused`, `justValue`, and `hoverItemIndex` support `bind:`. The DOM references `container` and `input` are also bindable.
|
|
91
|
+
|
|
92
|
+
```svelte
|
|
93
|
+
<Select {items} bind:value bind:listOpen />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> **The DOM input's value is the filter text, not the selection.** A selection is rendered in an element beside the input (so it can be rich markup via `selectionSnippet`, or multiple tags) and announced to screen readers via the live regions β it is never written into the textbox, so `input.value` is `''` unless the user is filtering. Read the selection from `bind:value`/`bind:justValue`, the `onValueChange`/`onSelectionChange` callbacks, or the submitted form field (`name` prop); in tests, assert on `.selected-item` (or `.multi-item`) rather than the input's value.
|
|
97
|
+
|
|
73
98
|
|
|
99
|
+
## Snippets
|
|
74
100
|
|
|
75
|
-
|
|
101
|
+
Rendering is customized with [snippets](https://svelte.dev/docs/svelte/snippet). Declare them inside `<Select>` and they are passed as props automatically.
|
|
76
102
|
|
|
77
103
|
```svelte
|
|
78
|
-
<Select>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<div slot="required" let:value />
|
|
91
|
-
<!-- Remember you can also use `svelte:fragment` to avoid a container DOM element. -->
|
|
92
|
-
<svelte:fragment slot="empty" />
|
|
104
|
+
<Select {items}>
|
|
105
|
+
{#snippet prependSnippet()}
|
|
106
|
+
<span>π</span>
|
|
107
|
+
{/snippet}
|
|
108
|
+
|
|
109
|
+
{#snippet itemSnippet(item, index)}
|
|
110
|
+
<div>{index + 1}. {item.label}</div>
|
|
111
|
+
{/snippet}
|
|
112
|
+
|
|
113
|
+
{#snippet selectionSnippet(selection)}
|
|
114
|
+
<strong>{selection.label}</strong>
|
|
115
|
+
{/snippet}
|
|
93
116
|
</Select>
|
|
94
117
|
```
|
|
95
118
|
|
|
119
|
+
| Snippet prop | Arguments | Description |
|
|
120
|
+
| --------------------- | -------------------- | ------------------------------------------------------ |
|
|
121
|
+
| chevronIconSnippet | `listOpen` | Chevron indicator icon |
|
|
122
|
+
| clearIconSnippet | β | Clear indicator icon |
|
|
123
|
+
| emptySnippet | β | Shown when there are no items to display |
|
|
124
|
+
| inputHiddenSnippet | `value` | Override the hidden form input(s) |
|
|
125
|
+
| itemSnippet | `item, index` | A list item |
|
|
126
|
+
| listAppendSnippet | β | Rendered after the list |
|
|
127
|
+
| listPrependSnippet | β | Rendered before the list |
|
|
128
|
+
| listSnippet | `filteredItems` | Replace the entire list |
|
|
129
|
+
| loadingIconSnippet | β | Loading indicator icon |
|
|
130
|
+
| multiClearIconSnippet | β | Remove icon on multi-select items |
|
|
131
|
+
| prependSnippet | β | Rendered before the value container |
|
|
132
|
+
| requiredSnippet | `value` | Override the hidden native `required` input |
|
|
133
|
+
| selectionSnippet | `selection, index?` | A selected value (`index` only in `multiple` mode) |
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## Events (callback props)
|
|
137
|
+
|
|
138
|
+
Events are plain callback props. Handlers receive the value directly β there is no `CustomEvent` and no `event.detail`.
|
|
139
|
+
|
|
140
|
+
```svelte
|
|
141
|
+
<Select {items} onSelectionChange={(value) => console.log('selected', value)} />
|
|
142
|
+
```
|
|
96
143
|
|
|
97
|
-
|
|
144
|
+
| Prop | Arguments | Description |
|
|
145
|
+
| ----------------- | ------------------- | --------------------------------------------------------------------------------------- |
|
|
146
|
+
| onblur | `event: FocusEvent` | fires when the input loses focus |
|
|
147
|
+
| onclear | `value` | fires when the value is cleared or a multi-select item is removed |
|
|
148
|
+
| onerror | `{ type, details }` | fires when an error is caught (e.g. a `loadOptions` rejection) |
|
|
149
|
+
| onfilter | `items` | fires when `listOpen: true` and items are filtered |
|
|
150
|
+
| onfocus | `event: FocusEvent` | fires when the input gains focus |
|
|
151
|
+
| onhoveritem | `index` | fires when `hoverItemIndex` changes |
|
|
152
|
+
| onloaded | `options` | fires when `loadOptions` resolves |
|
|
153
|
+
| onselect | `selection` | fires with just the picked item when the user selects an option |
|
|
154
|
+
| onSelectionChange | `value` | fires with the whole value when the user selects an option (never on clears or writes) |
|
|
155
|
+
| onValueChange | `value` | fires on _every_ value change β selection, clear, programmatic write, deps invalidation |
|
|
98
156
|
|
|
99
|
-
|
|
100
|
-
| ---------- | ----------------- | -------------------------------------------------------------------------- |
|
|
101
|
-
| change | { detail } | fires when the user selects an option |
|
|
102
|
-
| input | { detail } | fires when the value has been changed |
|
|
103
|
-
| focus | { detail } | fires when select > input on:focus |
|
|
104
|
-
| blur | { detail } | fires when select > input on:blur |
|
|
105
|
-
| clear | { detail } | fires when clear is invoked or item is removed (by user) from multi select |
|
|
106
|
-
| loaded | { options } | fires when `loadOptions` resolves |
|
|
107
|
-
| error | { type, details } | fires when error is caught |
|
|
108
|
-
| filter | { detail } | fires when `listOpen: true` and items are filtered |
|
|
109
|
-
| hoverItem | { detail } | fires when hoverItemIndex changes |
|
|
157
|
+
`onSelectionChange` and `onValueChange` are component state-change callbacks (headless-library naming), not DOM events β typing lives in `bind:filterText`, and only `onblur`/`onfocus` are literal DOM event passthroughs.
|
|
110
158
|
|
|
111
159
|
|
|
112
160
|
### Items
|
|
@@ -115,7 +163,7 @@ List position and floating is powered by `floating-ui`, see their [package-entry
|
|
|
115
163
|
|
|
116
164
|
```html
|
|
117
165
|
<script>
|
|
118
|
-
import Select from 'svelte-5-select';
|
|
166
|
+
import { Select } from 'svelte-5-select';
|
|
119
167
|
|
|
120
168
|
let simple = ['one', 'two', 'three'];
|
|
121
169
|
|
|
@@ -135,7 +183,7 @@ They can also be grouped and include non-selectable items.
|
|
|
135
183
|
|
|
136
184
|
```html
|
|
137
185
|
<script>
|
|
138
|
-
import Select from 'svelte-5-select';
|
|
186
|
+
import { Select } from 'svelte-5-select';
|
|
139
187
|
|
|
140
188
|
const items = [
|
|
141
189
|
{value: 'chocolate', label: 'Chocolate', group: 'Sweet'},
|
|
@@ -155,7 +203,7 @@ You can also use custom collections.
|
|
|
155
203
|
|
|
156
204
|
```html
|
|
157
205
|
<script>
|
|
158
|
-
import Select from 'svelte-5-select';
|
|
206
|
+
import { Select } from 'svelte-5-select';
|
|
159
207
|
|
|
160
208
|
const itemId = 'id';
|
|
161
209
|
const label = 'title';
|
|
@@ -171,11 +219,11 @@ You can also use custom collections.
|
|
|
171
219
|
|
|
172
220
|
### Async Items
|
|
173
221
|
|
|
174
|
-
To load items asynchronously then `loadOptions` is the simplest solution. Supply a function that returns a `Promise` that resolves with a list of items. `loadOptions`
|
|
222
|
+
To load items asynchronously then `loadOptions` is the simplest solution. Supply a function that returns a `Promise` that resolves with a list of items. `loadOptions` fires once on mount, on typing non-empty `filterText` (debounced), and whenever `loadOptionsDeps` or `disabled` change. Emptying the filter text or closing the list cancels a pending typing-driven load instead of re-fetching. One open/close-related exception: reopening the list with retained filter text whose load was cancelled on close (e.g. with `clearFilterTextOnBlur={false}`) refetches immediately, so the list never shows results that are stale for the visible text.
|
|
175
223
|
|
|
176
224
|
```html
|
|
177
225
|
<script>
|
|
178
|
-
import Select from 'svelte-5-select';
|
|
226
|
+
import { Select } from 'svelte-5-select';
|
|
179
227
|
|
|
180
228
|
import { someApiCall } from './services';
|
|
181
229
|
|
|
@@ -198,7 +246,7 @@ To load items asynchronously then `loadOptions` is the simplest solution. Supply
|
|
|
198
246
|
|
|
199
247
|
```html
|
|
200
248
|
<script>
|
|
201
|
-
import Select from 'svelte-5-select';
|
|
249
|
+
import { Select } from 'svelte-5-select';
|
|
202
250
|
|
|
203
251
|
let floatingConfig = {
|
|
204
252
|
strategy: 'fixed'
|
|
@@ -208,92 +256,174 @@ To load items asynchronously then `loadOptions` is the simplest solution. Supply
|
|
|
208
256
|
<Select {floatingConfig} />
|
|
209
257
|
```
|
|
210
258
|
|
|
211
|
-
###
|
|
212
|
-
These internal functions are exposed to override if needed. Look through the test file (test/src/index.js) for examples.
|
|
259
|
+
### Function props
|
|
213
260
|
|
|
214
|
-
|
|
215
|
-
export let itemFilter = (label, filterText, option) => label.toLowerCase().includes(filterText.toLowerCase());
|
|
216
|
-
```
|
|
261
|
+
Core behavior can be overridden by passing your own functions as props. Look through the test suite ([tests/src/select.test.ts](/tests/src/select.test.ts)) and [src/lib/filter.ts](/src/lib/filter.ts) for examples.
|
|
217
262
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
```svelte
|
|
264
|
+
<script>
|
|
265
|
+
import { Select } from 'svelte-5-select';
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
// How a single item is matched against filterText
|
|
268
|
+
const itemFilter = (label, filterText, option) => label.toLowerCase().includes(filterText.toLowerCase());
|
|
269
|
+
|
|
270
|
+
// Grouping
|
|
271
|
+
const groupBy = (item) => item.group;
|
|
272
|
+
const groupFilter = (groups) => groups; // sort/filter group order
|
|
273
|
+
const createGroupHeaderItem = (groupValue, item) => ({ value: groupValue, label: groupValue });
|
|
225
274
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
// Async loading β must return a Promise that resolves with items
|
|
276
|
+
const loadOptions = async (filterText) => fetchMyItems(filterText);
|
|
277
|
+
|
|
278
|
+
// Debounce used by loadOptions
|
|
279
|
+
let timeout;
|
|
280
|
+
const debounce = (fn, wait = 1) => {
|
|
281
|
+
clearTimeout(timeout);
|
|
282
|
+
timeout = setTimeout(fn, wait);
|
|
231
283
|
};
|
|
232
|
-
};
|
|
233
|
-
```
|
|
234
284
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
listOpen = false;
|
|
239
|
-
onClear(value);
|
|
240
|
-
handleFocus();
|
|
241
|
-
}
|
|
242
|
-
```
|
|
285
|
+
// Runs when the clear indicator is clicked
|
|
286
|
+
const handleClear = () => { /* your own clearing logic */ };
|
|
287
|
+
</script>
|
|
243
288
|
|
|
244
|
-
|
|
245
|
-
export let loadOptions = undefined; // if used must return a Promise that updates 'items'
|
|
246
|
-
/* Return an object with { cancelled: true } to keep the loading state as active. */
|
|
289
|
+
<Select {items} {itemFilter} {groupBy} {groupFilter} {createGroupHeaderItem} {loadOptions} {handleClear} />
|
|
247
290
|
```
|
|
248
291
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
The `filter` prop replaces the entire filtering pipeline β override it at your own risk.
|
|
293
|
+
|
|
294
|
+
| Prop | Type | Description |
|
|
295
|
+
| --------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
296
|
+
| loadOptions | `(filterText: string) => Promise<Item[] \| string[]>` | Load items asynchronously; see [Async Items](#async-items). Overrides the `loading` prop. |
|
|
297
|
+
| itemFilter | `(label: string, filterText: string, option) => boolean` | Whether one item matches `filterText`. Defaults to a case-insensitive substring match. |
|
|
298
|
+
| groupBy | `(item) => string` | Group items under headers by the returned key. Unset by default. |
|
|
299
|
+
| groupFilter | `(groups: string[]) => string[]` | Sort or filter the group order. Defaults to identity (`(groups) => groups`). |
|
|
300
|
+
| createGroupHeaderItem | `(groupValue: string, item) => SelectItem` | Build the header item for a group. Defaults to `{ value: groupValue, [label]: groupValue }` β the header's text is keyed by the `label` prop. |
|
|
301
|
+
| debounce | `(fn: () => void, wait: number) => void` | Debounce strategy for `loadOptions`. Defaults to a `setTimeout` of `debounceWait` ms. |
|
|
302
|
+
| handleClear | `(e?: MouseEvent) => void` | Runs when the clear indicator is clicked. Defaults to clearing `value` and refocusing. |
|
|
303
|
+
| filter | `(config: FilterConfig<Item>) => (Item \| SelectItem)[]` | Replaces the entire filtering pipeline. Defaults to the built-in `filter`. Override at your own risk. |
|
|
254
304
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
305
|
+
### Instance methods
|
|
306
|
+
|
|
307
|
+
A component reference exposes a couple of methods:
|
|
308
|
+
|
|
309
|
+
```svelte
|
|
310
|
+
<script>
|
|
311
|
+
import { Select } from 'svelte-5-select';
|
|
312
|
+
|
|
313
|
+
let select;
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
<Select bind:this={select} {items} />
|
|
317
|
+
|
|
318
|
+
<button onclick={() => console.log(select.getFilteredItems())}>Log filtered items</button>
|
|
319
|
+
<button onclick={() => select.reset()}>Reset</button>
|
|
260
320
|
```
|
|
261
321
|
|
|
262
|
-
|
|
322
|
+
## Exports
|
|
323
|
+
|
|
324
|
+
Everything `svelte-5-select` exports:
|
|
325
|
+
|
|
326
|
+
| Export | Kind | Description |
|
|
327
|
+
| ------ | ---- | ----------- |
|
|
328
|
+
| `Select` | component | The select/combobox component, default styles included. |
|
|
329
|
+
| `ChevronIcon`, `ClearIcon`, `LoadingIcon` | components | The built-in icons, exported for reuse inside custom snippets. |
|
|
330
|
+
| `filter` | function | The filtering pipeline `Select` uses internally β the basis for a custom `filter` prop (see [Function props](#function-props)). |
|
|
331
|
+
| `areItemsEqual` | function | Compares two items by `itemId`. |
|
|
332
|
+
| `isGroupHeader` | function | Type guard narrowing a `SelectRow` to a group header synthesized by `groupBy`. |
|
|
333
|
+
| `normalizeItem` | function | Resolves a raw string to a `{ value, label }` item; passes items through. |
|
|
334
|
+
|
|
335
|
+
Types: `ItemLike`, `SelectItem`, `SelectGroupHeader`, `SelectRow`, `SelectProps`, `SelectValue`, `SelectValueProp`, `SelectClearValue`, `JustValue`, `FloatingConfig`, `FilterConfig`, and `SelectErrorEvent`.
|
|
336
|
+
|
|
337
|
+
Subpath exports: `svelte-5-select/styles/default.css` (the default stylesheet on its own), `svelte-5-select/tailwind.css`, and `svelte-5-select/no-styles/Select.svelte` (the component with its style block stripped β see the [experimental section](#-experimental-replace-styles-tailwind-bootstrap-bulma-etc)).
|
|
338
|
+
|
|
339
|
+
Internal wiring (the shared state store, the composables, and their types) is deliberately not exported and cannot be deep-imported.
|
|
263
340
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
341
|
+
## TypeScript
|
|
342
|
+
|
|
343
|
+
The component is generic over your item type: values, items, snippets, and callbacks are typed from the `items` you pass in β plain interfaces work, no index signature needed (TypeScript >= 5.4). Only `items`, `value`, and `loadOptions` drive that inference: configuration callbacks like `groupBy`/`itemFilter` receive the inferred `Item` but never widen it, so a loosely-typed callback const can't silently change what `Item` means. The `multiple` prop narrows types too: with `multiple`, `bind:value` and the `onValueChange`/`onSelectionChange` payloads are `Item[]`; without it they are `Item | null`. The `null` appears only in the dispatch payloads (`onValueChange(null)` on clear) β an emptied `bind:value` is always `undefined`, so test it with falsiness, not `=== null`.
|
|
344
|
+
|
|
345
|
+
```svelte
|
|
346
|
+
<script lang="ts">
|
|
347
|
+
interface Country {
|
|
348
|
+
code: string;
|
|
349
|
+
name: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let countries: Country[] = [
|
|
353
|
+
{ code: 'de', name: 'Germany' },
|
|
354
|
+
{ code: 'fr', name: 'France' },
|
|
355
|
+
];
|
|
356
|
+
let value: Country[] | undefined = $state();
|
|
357
|
+
</script>
|
|
358
|
+
|
|
359
|
+
<Select items={countries} itemId="code" label="name" multiple bind:value onValueChange={(v) => v.length} />
|
|
270
360
|
```
|
|
271
361
|
|
|
362
|
+
`SelectProps`, `SelectItem`, `SelectValue`, and `SelectValueProp` (the bindable `value` shape, which also accepts raw string ids) are exported for annotating your own wrappers.
|
|
363
|
+
|
|
272
364
|
## A11y (Accessibility)
|
|
273
365
|
|
|
274
|
-
|
|
366
|
+
The input renders as a WAI-ARIA combobox with a listbox popup, including `aria-expanded`, `aria-activedescendant`, `aria-required`, `aria-invalid`, `aria-busy` (while loading), and `aria-multiselectable` where applicable. When `groupBy` is set, each group's options are wrapped in a `role="group"` region named by its header (via `aria-labelledby`). Set `hasError` with `ariaErrorMessage` to wire the input to an external error element via `aria-errormessage`. A `disabled` Select marks the input `aria-disabled` + `readonly` (rather than natively `disabled`) so the combobox and its value stay in the accessibility tree and remain announceable, while staying non-interactive and out of the tab order. Keyboard support covers ArrowUp/ArrowDown, PageUp/PageDown, Home/End, Enter, Tab (commits the highlighted option β but only after you've navigated or typed, so tabbing straight past an opened list never selects), Escape, `Alt`+ArrowDown/ArrowUp (open/close), Space (select in select-only mode), and Backspace/ArrowLeft/ArrowRight for multi-select items; with `multiFullItemClearable`, each tag is a focusable button that Enter/Space removes. The open listbox is named by `ariaLabel`, an `aria-labelledby` supplied via `inputAttributes`, or β on the `id` + `<label for>` path β by that label (a wrapping `<label>` contributes only its own text). The focused input shows a ring (`--focused-box-shadow`) in addition to the border colour; the option under the keyboard cursor shows a >=3:1 outline (`--item-hover-outline`) on top of the hover background; the spinner and item transitions respect `prefers-reduced-motion`; and focus/selection stay visible under Windows High Contrast Mode (`forced-colors`).
|
|
275
367
|
|
|
276
|
-
|
|
277
|
-
export let ariaValues = (values) => {
|
|
278
|
-
return `Option ${values}, selected.`;
|
|
279
|
-
}
|
|
368
|
+
The textbox itself only ever contains the typed filter text β the current selection is rendered beside it and conveyed to assistive tech through two polite `role="status"` live regions (customizable via the `ariaValues`, `ariaFocused`, `ariaListOpen`, `ariaEmpty`, `ariaLoading`, and `ariaCleared` builders): focusing a valued Select announces the selection, and selecting/clearing announces the change.
|
|
280
369
|
|
|
281
|
-
|
|
282
|
-
return `You are currently focused on option ${label}. There are ${count} results available.`;
|
|
283
|
-
}
|
|
370
|
+
Give the input an accessible name with either `ariaLabel` or an external `<label for={id}>` (set the `id` prop). In development the component logs a `console.warn` if it finds neither `ariaLabel`, an `aria-labelledby`, nor an associated `<label>` β the placeholder is only a last-resort fallback that some screen readers ignore. The warning is stripped from production builds.
|
|
284
371
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
372
|
+
Selection and list state (including the empty/loading state) are announced through two polite `role="status"` live regions; the visible "No options"/"Loading Data" text is `aria-hidden` so it is not read twice. Override these props to change the screen-reader announcement text:
|
|
373
|
+
|
|
374
|
+
```svelte
|
|
375
|
+
<Select
|
|
376
|
+
{items}
|
|
377
|
+
ariaLabel="Choose a flavour"
|
|
378
|
+
ariaValues={(values) => `Option ${values}, selected.`}
|
|
379
|
+
ariaListOpen={(label, count) => `You are currently focused on option ${label}. There are ${count} results available.`}
|
|
380
|
+
ariaFocused={() => `Select is focused, type to refine list, press down to open the menu.`}
|
|
381
|
+
ariaEmpty={() => `No options`}
|
|
382
|
+
ariaLoading={() => `Loading Data`}
|
|
383
|
+
ariaCleared={() => `Selection cleared.`}
|
|
384
|
+
ariaClearSelectLabel="Clear selection"
|
|
385
|
+
ariaRemoveItemLabel={(label) => `Remove ${label}`}
|
|
386
|
+
/>
|
|
288
387
|
```
|
|
289
388
|
|
|
389
|
+
## Migrating from svelte-select (Svelte 4)
|
|
390
|
+
|
|
391
|
+
The public API moved to idiomatic Svelte 5:
|
|
392
|
+
|
|
393
|
+
- **Slots β snippets.** `<div slot="item" let:item />` becomes `{#snippet itemSnippet(item, index)}...{/snippet}` declared inside `<Select>`. See the [Snippets](#snippets) table for the full mapping (`slot="chevron-icon"` β `chevronIconSnippet`, etc.).
|
|
394
|
+
- **Events β callback props.** `on:change={(e) => e.detail}` becomes `onSelectionChange={(value) => ...}` and `on:input` becomes `onValueChange={(value) => ...}` β handlers receive the value directly, with no `event.detail`.
|
|
395
|
+
- **`export let` overrides β regular props.** Functions like `itemFilter`, `groupBy`, and the aria text builders are passed as props.
|
|
396
|
+
- **CSS variables are kebab-case.** `--borderRadius` is now `--border-radius`; see [the full list](/docs/theming_variables.md).
|
|
397
|
+
|
|
398
|
+
## Migrating from 1.x to 2.0
|
|
399
|
+
|
|
400
|
+
Every change below is covered in detail in the [changelog](CHANGELOG.md); this is the upgrade checklist:
|
|
401
|
+
|
|
402
|
+
- **`oninput` and `onchange` are renamed.** `oninput` is now `onValueChange` and `onchange` is now `onSelectionChange` β same payloads, same firing rules. The old names collided with DOM-event expectations on a component wrapping a text input (`oninput` never fired per keystroke; typing is `bind:filterText`).
|
|
403
|
+
- **An emptied `value` is always `undefined`.** Every clear path (clear button, last tag removed, `loadOptionsDeps` invalidation, disabling a `loadOptions` select, multipleβsingle switch) writes `undefined` β never `null` or `[]` β so test emptiness with falsiness, not `=== null`. `justValue` follows the same rule. Clearing a single select dispatches `onValueChange(null)` instead of `onValueChange([])`.
|
|
404
|
+
- **`onloaded` receives `(Item | SelectItem)[]`, not `Item[]`.** A loader that resolves raw strings delivers the synthesized `{ value, label, index }` items built from them, so handlers explicitly annotated `(options: Item[]) => void` need the widened element type (narrow rows before reading item fields).
|
|
405
|
+
- **Removed exports.** `useKeyboardNavigation` (with its `KeyboardNavigationContext`/`isCancelled` surface) and `isStringArray` are gone; the composables are internal.
|
|
406
|
+
- **`listStyle` is renamed `listStyles`**, matching the `containerStyles`/`inputStyles` pluralization.
|
|
407
|
+
- **`ErrorEvent` is renamed `SelectErrorEvent`** β the old name shadowed the DOM's global `ErrorEvent` and has been removed; update imports.
|
|
408
|
+
- **Rendered-list surfaces are typed `SelectRow<Item>`.** `getFilteredItems()`, `onfilter`, `listSnippet`, and `itemSnippet` see the group headers `groupBy` synthesizes; narrow rows with the exported `isGroupHeader` guard.
|
|
409
|
+
- **`SelectValue` takes a `Multiple` type parameter** (inferred from the `multiple` prop), and `onclear` receives the `Multiple`-discriminated `SelectClearValue` instead of a flat union.
|
|
410
|
+
- **`loadOptions` triggers changed.** It fires on mount, on typing, on `loadOptionsDeps` changes, and on disabled toggles β never on list open/close, except that reopening onto results that don't reflect the current filter text re-fetches. Pending fetches that become moot are cancelled (including on a programmatic `bind:listOpen = false`), results are no longer re-filtered by `itemFilter`, and only deps-driven reloads clear a stale value.
|
|
411
|
+
- **Enter and Escape pass through when the list is closed.** Keys are only claimed when the component acts on them, so Enter on a closed select now submits the surrounding form and Escape now closes the surrounding dialog β re-test forms and dialogs that relied on the old swallow-everything behavior. Exception: in select-only mode (`searchable={false}`) Enter on a closed list opens it instead of passing through, per the APG select-only combobox pattern.
|
|
412
|
+
- **Tab only commits after real navigation.** Tab still selects the highlighted option in a single press, but only once you've moved the cursor (keys or pointer movement over the list) or typed filter text during that open β merely opening the list and tabbing away now closes it without selecting, instead of silently committing the first option. A seeded or retained `filterText` doesn't count as typing.
|
|
413
|
+
- **`selectable: undefined` is keyboard-reachable.** Arrow navigation uses the same rule as click/Enter (`selectable !== false`), so an item carrying an explicit `selectable: undefined` is no longer skipped by the keyboard.
|
|
414
|
+
- **`selectionSnippet` is `Snippet<[Item, number?]>`** β always a single item, also in multiple mode.
|
|
415
|
+
- **Custom `filter` implementations:** `FilterConfig.filterGroupedItems` is renamed `applyGrouping`, and `FilterConfig.value` is now honestly typed `Item | SelectItem | (Item | SelectItem)[] | null | undefined` (it was `SelectItem`-only) β implementations annotating that field may need the widened type.
|
|
416
|
+
- **Markup and a11y changes.** The multi-select remove control is a real `<button>` in the tab order; grouped options are wrapped in `role="group"` regions named by their headers; the input no longer defaults its `aria-label` to the placeholder (name it with `ariaLabel` or an external `<label for>`).
|
|
417
|
+
- **Behavior fixes worth re-testing:** `bind:focused` writes now move real DOM focus; disabling releases focus and keyboard control; and an initial `filterText` is kept on mount (it used to be silently cleared) β it filters and drives the mount `loadOptions` fetch, without opening the list or moving focus.
|
|
418
|
+
- **Node >= 18 at runtime** (Svelte 5's own floor); **TypeScript >= 5.4** for the published types (they use `NoInfer`). Developing this repository needs Node >= 22.12.
|
|
419
|
+
|
|
290
420
|
## CSS custom properties (variables)
|
|
291
421
|
|
|
292
422
|
You can style a component by overriding [the available CSS custom properties](/docs/theming_variables.md).
|
|
293
423
|
|
|
294
424
|
```html
|
|
295
425
|
<script>
|
|
296
|
-
import Select from 'svelte-5-select';
|
|
426
|
+
import { Select } from 'svelte-5-select';
|
|
297
427
|
</script>
|
|
298
428
|
|
|
299
429
|
<Select --border-radius= "10px" --placeholder-color="blue" />
|
|
@@ -303,7 +433,7 @@ You can also use the `inputStyles` prop to write in any override styles needed f
|
|
|
303
433
|
|
|
304
434
|
```html
|
|
305
435
|
<script>
|
|
306
|
-
import Select from 'svelte-5-select';
|
|
436
|
+
import { Select } from 'svelte-5-select';
|
|
307
437
|
|
|
308
438
|
const items = ['One', 'Two', 'Three'];
|
|
309
439
|
</script>
|
|
@@ -312,9 +442,9 @@ You can also use the `inputStyles` prop to write in any override styles needed f
|
|
|
312
442
|
```
|
|
313
443
|
|
|
314
444
|
### π§ͺ Experimental: Replace styles (Tailwind, Bootstrap, Bulma etc)
|
|
315
|
-
If you'd like to supply your own styles use: `import Select from 'svelte-5-select/no-styles/Select.svelte'`. Then somewhere in your code or build pipeline add your own. There is a tailwind stylesheet via `import 'svelte-5-select/tailwind.css'`. It uses `@
|
|
445
|
+
If you'd like to supply your own styles use: `import Select from 'svelte-5-select/no-styles/Select.svelte'`. Then somewhere in your code or build pipeline add your own. There is a tailwind stylesheet via `import 'svelte-5-select/tailwind.css'`. It uses `@import 'tailwindcss'` and `@apply`, so your app must have Tailwind CSS v4 set up to process it.
|
|
316
446
|
|
|
317
447
|
|
|
318
448
|
## License
|
|
319
449
|
|
|
320
|
-
[
|
|
450
|
+
[License](LICENSE)
|
package/dist/ChevronIcon.svelte
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
<svg
|
|
2
|
-
width="100%"
|
|
3
|
-
height="100%"
|
|
4
|
-
viewBox="0 0 20 20"
|
|
5
|
-
focusable="false"
|
|
6
|
-
aria-hidden="true">
|
|
1
|
+
<svg width="100%" height="100%" viewBox="0 0 20 20" focusable="false" aria-hidden="true">
|
|
7
2
|
<path
|
|
8
|
-
|
|
3
|
+
fill="currentColor"
|
|
9
4
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747
|
|
10
5
|
3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0
|
|
11
6
|
1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502
|
|
@@ -14,9 +9,9 @@
|
|
|
14
9
|
</svg>
|
|
15
10
|
|
|
16
11
|
<style>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</style>
|
|
12
|
+
svg {
|
|
13
|
+
width: var(--chevron-icon-width, 20px);
|
|
14
|
+
height: var(--chevron-icon-width, 20px);
|
|
15
|
+
color: var(--chevron-icon-colour, currentColor);
|
|
16
|
+
}
|
|
17
|
+
</style>
|
package/dist/ClearIcon.svelte
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
<svg
|
|
2
|
-
width="100%"
|
|
3
|
-
height="100%"
|
|
4
|
-
viewBox="-2 -2 50 50"
|
|
5
|
-
focusable="false"
|
|
6
|
-
aria-hidden="true"
|
|
7
|
-
role="presentation"
|
|
8
|
-
>
|
|
1
|
+
<svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation">
|
|
9
2
|
<path
|
|
10
3
|
fill="currentColor"
|
|
11
4
|
d="M34.923,37.251L24,26.328L13.077,37.251L9.436,33.61l10.923-10.923L9.436,11.765l3.641-3.641L24,19.047L34.923,8.124
|
|
12
|
-
l3.641,3.641L27.641,22.688L38.564,33.61L34.923,37.251z"
|
|
13
|
-
/>
|
|
5
|
+
l3.641,3.641L27.641,22.688L38.564,33.61L34.923,37.251z" />
|
|
14
6
|
</svg>
|
|
15
7
|
|
|
16
8
|
<style>
|
|
@@ -19,4 +11,4 @@
|
|
|
19
11
|
height: var(--clear-icon-width, 20px);
|
|
20
12
|
color: var(--clear-icon-color, currentColor);
|
|
21
13
|
}
|
|
22
|
-
</style>
|
|
14
|
+
</style>
|
package/dist/LoadingIcon.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg class="loading" viewBox="25 25 50 50">
|
|
1
|
+
<svg class="loading" viewBox="25 25 50 50" aria-hidden="true" focusable="false">
|
|
2
2
|
<circle
|
|
3
3
|
class="circle_path"
|
|
4
4
|
cx="50"
|
|
@@ -30,4 +30,11 @@
|
|
|
30
30
|
transform: rotate(360deg);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
/* Respect users who ask for reduced motion: stop the continuous spin. */
|
|
35
|
+
@media (prefers-reduced-motion: reduce) {
|
|
36
|
+
.loading {
|
|
37
|
+
animation: none;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</style>
|