svelte-5-select 1.0.2 โ†’ 2.1.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.
Files changed (39) hide show
  1. package/README.md +255 -115
  2. package/dist/ChevronIcon.svelte +14 -13
  3. package/dist/ChevronIcon.svelte.d.ts +6 -14
  4. package/dist/ClearIcon.svelte +9 -11
  5. package/dist/ClearIcon.svelte.d.ts +6 -14
  6. package/dist/LoadingIcon.svelte +15 -2
  7. package/dist/LoadingIcon.svelte.d.ts +6 -14
  8. package/dist/Select.svelte +1048 -442
  9. package/dist/Select.svelte.d.ts +37 -5
  10. package/dist/aria-handlers.svelte.d.ts +5 -2
  11. package/dist/aria-handlers.svelte.js +30 -8
  12. package/dist/filter.d.ts +10 -2
  13. package/dist/filter.js +15 -8
  14. package/dist/index.d.ts +3 -4
  15. package/dist/index.js +2 -3
  16. package/dist/keyboard-navigation.svelte.d.ts +7 -2
  17. package/dist/keyboard-navigation.svelte.js +293 -47
  18. package/dist/no-styles/ChevronIcon.svelte +17 -0
  19. package/dist/no-styles/ChevronIcon.svelte.d.ts +18 -0
  20. package/dist/no-styles/ClearIcon.svelte +14 -0
  21. package/dist/no-styles/ClearIcon.svelte.d.ts +18 -0
  22. package/dist/no-styles/LoadingIcon.svelte +19 -0
  23. package/dist/no-styles/LoadingIcon.svelte.d.ts +18 -0
  24. package/dist/no-styles/Select.svelte +1452 -0
  25. package/dist/no-styles/Select.svelte.d.ts +38 -0
  26. package/dist/select-state.svelte.d.ts +15 -0
  27. package/dist/select-state.svelte.js +161 -0
  28. package/dist/styles/default.css +131 -71
  29. package/dist/tailwind.css +139 -17
  30. package/dist/types.d.ts +488 -129
  31. package/dist/use-hover.svelte.d.ts +3 -7
  32. package/dist/use-hover.svelte.js +110 -36
  33. package/dist/use-load-options.svelte.d.ts +18 -3
  34. package/dist/use-load-options.svelte.js +362 -42
  35. package/dist/use-value.svelte.d.ts +2 -11
  36. package/dist/use-value.svelte.js +334 -111
  37. package/dist/utils.d.ts +19 -8
  38. package/dist/utils.js +52 -8
  39. package/package.json +121 -94
package/README.md CHANGED
@@ -1,24 +1,20 @@
1
- <div style="text-align: center;">
1
+ <div align="center">
2
2
  <img src="https://raw.githubusercontent.com/IDontKnowMyUsername/svelte-5-select/master/svelte-select.png" alt="Svelte 5 Select" width="150" />
3
3
  <h1>Svelte 5 Select</h1>
4
4
  </div>
5
- <div style="text-align: center;">
5
+ <div align="center">
6
6
  <a href="https://npmjs.org/package/svelte-5-select">
7
- <img src="https://badgen.now.sh/npm/v/svelte-5-select" alt="version" />
7
+ <img src="https://badgen.net/npm/v/svelte-5-select" alt="version" />
8
8
  </a>
9
9
  <a href="https://npmjs.org/package/svelte-5-select">
10
- <img src="https://badgen.now.sh/npm/dm/svelte-5-select" alt="downloads" />
10
+ <img src="https://badgen.net/npm/dm/svelte-5-select" alt="downloads" />
11
11
  </a>
12
12
  </div>
13
- <div style="text-align: center;">A select/autocomplete/typeahead Svelte 5 component.</div>
13
+ <div align="center">A select/autocomplete/typeahead Svelte 5 component.</div>
14
14
 
15
15
  ## Demos
16
16
 
17
- [๐Ÿ’ฅ Examples of every prop, event, slot and more ๐Ÿ’ฅ](https://svelte-5-select-examples.vercel.app)
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[]` | `[]` | Array of items available to display / filter |
42
- | value | `any` | `null` | Selected value(s) |
43
- | justValue | `any` | `null` | **READ-ONLY** Selected value(s) excluding container object |
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` | Controls input focus |
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. `value`, `placeholder`, and `style` are ignored โ€” use `bind:filterText`, `placeholder`, and `inputStyles` |
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,98 @@ 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 `on:change` list will close |
72
- | clearFilterTextOnBlur | `boolean` | `true` | If `false`, `filterText` value is preserved on:blur |
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
+ | ariaActiveTag | `(label: string) => string` | see below | Announcement when the arrow-key tag cursor lands on a multi-select tag |
86
+
87
+ 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).
88
+
89
+ ### Bindable props
90
+
91
+ `value`, `filterText`, `items`, `listOpen`, `loading`, `focused`, `justValue`, and `hoverItemIndex` support `bind:`. The DOM references `container` and `input` are also bindable.
92
+
93
+ ```svelte
94
+ <Select {items} bind:value bind:listOpen />
95
+ ```
96
+
97
+ > **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.
98
+
73
99
 
100
+ ## Snippets
74
101
 
75
- ## Named slots
102
+ Rendering is customized with [snippets](https://svelte.dev/docs/svelte/snippet). Declare them inside `<Select>` and they are passed as props automatically.
76
103
 
77
104
  ```svelte
78
- <Select>
79
- <div slot="prepend" />
80
- <div slot="selection" let:selection let:index /> <!-- index only available when multiple -->
81
- <div slot="clear-icon" />
82
- <div slot="multi-clear-icon" />
83
- <div slot="loading-icon" />
84
- <div slot="chevron-icon" />
85
- <div slot="list-prepend" />
86
- <div slot="list" let:filteredItems />
87
- <div slot="list-append" />
88
- <div slot="item" let:item let:index />
89
- <div slot="input-hidden" let:value />
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" />
105
+ <Select {items}>
106
+ {#snippet prependSnippet()}
107
+ <span>๐Ÿ”</span>
108
+ {/snippet}
109
+
110
+ {#snippet itemSnippet(item, index)}
111
+ <div>{index + 1}. {item.label}</div>
112
+ {/snippet}
113
+
114
+ {#snippet selectionSnippet(selection)}
115
+ <strong>{selection.label}</strong>
116
+ {/snippet}
93
117
  </Select>
94
118
  ```
95
119
 
120
+ | Snippet prop | Arguments | Description |
121
+ | --------------------- | -------------------- | ------------------------------------------------------ |
122
+ | chevronIconSnippet | `listOpen` | Chevron indicator icon |
123
+ | clearIconSnippet | โ€” | Clear indicator icon |
124
+ | emptySnippet | โ€” | Shown when there are no items to display |
125
+ | inputHiddenSnippet | `value` | Override the hidden form input(s) |
126
+ | itemSnippet | `item, index` | A list item |
127
+ | listAppendSnippet | โ€” | Rendered after the list |
128
+ | listPrependSnippet | โ€” | Rendered before the list |
129
+ | listSnippet | `filteredItems` | Replace the entire list |
130
+ | loadingIconSnippet | โ€” | Loading indicator icon |
131
+ | multiClearIconSnippet | โ€” | Remove icon on multi-select items |
132
+ | prependSnippet | โ€” | Rendered before the value container |
133
+ | requiredSnippet | `value` | Override the hidden native `required` input |
134
+ | selectionSnippet | `selection, index?` | A selected value (`index` only in `multiple` mode) |
96
135
 
97
- ## Events
98
136
 
99
- | Event Name | Callback | Description |
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 |
137
+ ## Events (callback props)
138
+
139
+ Events are plain callback props. Handlers receive the value directly โ€” there is no `CustomEvent` and no `event.detail`.
140
+
141
+ ```svelte
142
+ <Select {items} onSelectionChange={(value) => console.log('selected', value)} />
143
+ ```
144
+
145
+ | Prop | Arguments | Description |
146
+ | ----------------- | ------------------- | --------------------------------------------------------------------------------------- |
147
+ | onblur | `event: FocusEvent` | fires when the input loses focus |
148
+ | onclear | `value` | fires when the value is cleared or a multi-select item is removed |
149
+ | onerror | `{ type, details }` | fires when an error is caught (e.g. a `loadOptions` rejection) |
150
+ | onfilter | `items` | fires when `listOpen: true` and items are filtered |
151
+ | onfocus | `event: FocusEvent` | fires when the input gains focus |
152
+ | onhoveritem | `index` | fires when `hoverItemIndex` changes |
153
+ | onloaded | `options` | fires when `loadOptions` resolves |
154
+ | onselect | `selection` | fires with just the picked item when the user selects an option |
155
+ | onSelectionChange | `value` | fires with the whole value when the user selects an option (never on clears or writes) |
156
+ | onValueChange | `value` | fires on _every_ value change โ€” selection, clear, programmatic write, deps invalidation |
157
+
158
+ `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
159
 
111
160
 
112
161
  ### Items
@@ -115,7 +164,7 @@ List position and floating is powered by `floating-ui`, see their [package-entry
115
164
 
116
165
  ```html
117
166
  <script>
118
- import Select from 'svelte-5-select';
167
+ import { Select } from 'svelte-5-select';
119
168
 
120
169
  let simple = ['one', 'two', 'three'];
121
170
 
@@ -135,7 +184,7 @@ They can also be grouped and include non-selectable items.
135
184
 
136
185
  ```html
137
186
  <script>
138
- import Select from 'svelte-5-select';
187
+ import { Select } from 'svelte-5-select';
139
188
 
140
189
  const items = [
141
190
  {value: 'chocolate', label: 'Chocolate', group: 'Sweet'},
@@ -155,7 +204,7 @@ You can also use custom collections.
155
204
 
156
205
  ```html
157
206
  <script>
158
- import Select from 'svelte-5-select';
207
+ import { Select } from 'svelte-5-select';
159
208
 
160
209
  const itemId = 'id';
161
210
  const label = 'title';
@@ -171,11 +220,11 @@ You can also use custom collections.
171
220
 
172
221
  ### Async Items
173
222
 
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` has debounce baked in and fires each time `filterText` is updated.
223
+ 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. Closing the list cancels a pending typing-driven load instead of re-fetching. Beyond those triggers, one rule: the open list never shows results that are stale for the visible text. Reopening it refetches when the shown results don't match the retained filter text (`clearFilterTextOnBlur={false}`), when Escape wiped the text over query-narrowed results, or after a failed load; and emptying the text while the list stays open โ€” deleting the query, or a selection wiping it with `closeListOnChange={false}` โ€” refetches the baseline (empty-query) set in place.
175
224
 
176
225
  ```html
177
226
  <script>
178
- import Select from 'svelte-5-select';
227
+ import { Select } from 'svelte-5-select';
179
228
 
180
229
  import { someApiCall } from './services';
181
230
 
@@ -198,7 +247,7 @@ To load items asynchronously then `loadOptions` is the simplest solution. Supply
198
247
 
199
248
  ```html
200
249
  <script>
201
- import Select from 'svelte-5-select';
250
+ import { Select } from 'svelte-5-select';
202
251
 
203
252
  let floatingConfig = {
204
253
  strategy: 'fixed'
@@ -208,102 +257,193 @@ To load items asynchronously then `loadOptions` is the simplest solution. Supply
208
257
  <Select {floatingConfig} />
209
258
  ```
210
259
 
211
- ### Exposed methods
212
- These internal functions are exposed to override if needed. Look through the test file (test/src/index.js) for examples.
260
+ ### Function props
213
261
 
214
- ```js
215
- export let itemFilter = (label, filterText, option) => label.toLowerCase().includes(filterText.toLowerCase());
216
- ```
262
+ 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
263
 
218
- ```js
219
- export let groupBy = undefined;
220
- ```
264
+ ```svelte
265
+ <script>
266
+ import { Select } from 'svelte-5-select';
221
267
 
222
- ```js
223
- export let groupFilter = groups => groups;
224
- ```
268
+ // How a single item is matched against filterText
269
+ const itemFilter = (label, filterText, option) => label.toLowerCase().includes(filterText.toLowerCase());
270
+
271
+ // Grouping
272
+ const groupBy = (item) => item.group;
273
+ const groupFilter = (groups) => groups; // sort/filter group order
274
+ const createGroupHeaderItem = (groupValue, item) => ({ value: groupValue, label: groupValue });
275
+
276
+ // Async loading โ€” must return a Promise that resolves with items
277
+ const loadOptions = async (filterText) => fetchMyItems(filterText);
225
278
 
226
- ```js
227
- export let createGroupHeaderItem = groupValue => {
228
- return {
229
- value: groupValue,
230
- label: groupValue
279
+ // Debounce used by loadOptions
280
+ let timeout;
281
+ const debounce = (fn, wait = 1) => {
282
+ clearTimeout(timeout);
283
+ timeout = setTimeout(fn, wait);
231
284
  };
232
- };
233
- ```
234
285
 
235
- ```js
236
- export function handleClear() {
237
- value = undefined;
238
- listOpen = false;
239
- onClear(value);
240
- handleFocus();
241
- }
242
- ```
286
+ // Runs when the clear indicator is clicked
287
+ const handleClear = () => { /* your own clearing logic */ };
288
+ </script>
243
289
 
244
- ```js
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. */
290
+ <Select {items} {itemFilter} {groupBy} {groupFilter} {createGroupHeaderItem} {loadOptions} {handleClear} />
247
291
  ```
248
292
 
249
- ```js
250
- export const getFilteredItems = () => {
251
- return filteredItems;
252
- };
253
- ```
293
+ The `filter` prop replaces the entire filtering pipeline โ€” override it at your own risk.
294
+
295
+ | Prop | Type | Description |
296
+ | --------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
297
+ | loadOptions | `(filterText: string) => Promise<Item[] \| string[]>` | Load items asynchronously; see [Async Items](#async-items). Overrides the `loading` prop. |
298
+ | itemFilter | `(label: string, filterText: string, option) => boolean` | Whether one item matches `filterText`. Defaults to a case-insensitive substring match. |
299
+ | groupBy | `(item) => string` | Group items under headers by the returned key. Unset by default. |
300
+ | groupFilter | `(groups: string[]) => string[]` | Sort or filter the group order. Defaults to identity (`(groups) => groups`). |
301
+ | 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. |
302
+ | debounce | `(fn: () => void, wait: number) => void` | Debounce strategy for `loadOptions`. Defaults to a `setTimeout` of `debounceWait` ms. |
303
+ | handleClear | `(e?: MouseEvent) => void` | Runs when the clear indicator is clicked. Defaults to clearing `value` and refocusing. |
304
+ | filter | `(config: FilterConfig<Item>) => (Item \| SelectItem)[]` | Replaces the entire filtering pipeline. Defaults to the built-in `filter`. Override at your own risk. |
305
+
306
+ ### Instance methods
307
+
308
+ A component reference exposes a couple of methods:
254
309
 
255
- ```js
256
- export let debounce = (fn, wait = 1) => {
257
- clearTimeout(timeout);
258
- timeout = setTimeout(fn, wait);
259
- };
310
+ ```svelte
311
+ <script>
312
+ import { Select } from 'svelte-5-select';
313
+
314
+ let select;
315
+ </script>
316
+
317
+ <Select bind:this={select} {items} />
318
+
319
+ <button onclick={() => console.log(select.getFilteredItems())}>Log filtered items</button>
320
+ <button onclick={() => select.reset()}>Reset</button>
260
321
  ```
261
322
 
262
- Override core functionality at your own risk! See ([get-items.js](/src/lib/get-items.js) & [filter.js](/src/lib/filter.js))
323
+ ## Exports
324
+
325
+ Everything `svelte-5-select` exports:
263
326
 
264
- ```js
265
- // core replaceable methods...
266
- <Select
267
- filter={...}
268
- getItems={...}
269
- />
327
+ | Export | Kind | Description |
328
+ | ------ | ---- | ----------- |
329
+ | `Select` | component | The select/combobox component, default styles included. |
330
+ | `ChevronIcon`, `ClearIcon`, `LoadingIcon` | components | The built-in icons, exported for reuse inside custom snippets. |
331
+ | `filter` | function | The filtering pipeline `Select` uses internally โ€” the basis for a custom `filter` prop (see [Function props](#function-props)). |
332
+ | `areItemsEqual` | function | Compares two items by `itemId`. |
333
+ | `isGroupHeader` | function | Type guard narrowing a `SelectRow` to a group header synthesized by `groupBy`. |
334
+ | `normalizeItem` | function | Resolves a raw string to a `{ value, label }` item; passes items through. |
335
+
336
+ Types: `ItemLike`, `SelectItem`, `SelectGroupHeader`, `SelectRow`, `SelectProps`, `SelectValue`, `SelectValueProp`, `SelectClearValue`, `JustValue`, `FloatingConfig`, `FilterConfig`, and `SelectErrorEvent`.
337
+
338
+ 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)).
339
+
340
+ Internal wiring (the shared state store, the composables, and their types) is deliberately not exported and cannot be deep-imported.
341
+
342
+ ## TypeScript
343
+
344
+ 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 and event handlers (`groupBy`, `itemFilter`, `onselect`, โ€ฆ) 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`, the `onValueChange`/`onSelectionChange` payloads are `Item[]`; without it they are `Item | null`. `bind:value` is looser than the payloads โ€” it also accepts raw string ids on input, and an emptied `bind:value` is always `undefined` โ€” so test it with falsiness, not `=== null` (the `null` appears only in the dispatch payloads, e.g. `onValueChange(null)` on clear).
345
+
346
+ ```svelte
347
+ <script lang="ts">
348
+ import { Select } from 'svelte-5-select';
349
+
350
+ interface Country {
351
+ code: string;
352
+ name: string;
353
+ }
354
+
355
+ let countries: Country[] = [
356
+ { code: 'de', name: 'Germany' },
357
+ { code: 'fr', name: 'France' },
358
+ ];
359
+ let value: Country[] | undefined = $state();
360
+ </script>
361
+
362
+ <Select items={countries} itemId="code" label="name" multiple bind:value onValueChange={(v) => v.length} />
270
363
  ```
271
364
 
365
+ `SelectProps`, `SelectItem`, `SelectValue`, and `SelectValueProp` (the bindable `value` shape, which also accepts raw string ids) are exported for annotating your own wrappers.
366
+
367
+ One resolution caveat: the full generic typing requires Svelte-aware tooling (svelte-check, the Svelte VS Code extension) or `"moduleResolution": "bundler"` โ€” which SvelteKit and Vite templates use. Plain `tsc` under `"moduleResolution": "nodenext"` cannot resolve `.svelte` type declarations, so the `Select` component import silently falls back to Svelte's untyped ambient component type there (the exported types like `SelectProps` still resolve fully).
368
+
369
+ ## SSR (SvelteKit)
370
+
371
+ The component server-renders: no browser globals are touched at module scope or during init, and the selection โ€” including raw string `value` entries, which are resolved against `items` before the first render โ€” the multi-select tags, the hidden form inputs, and an open list are all in the server HTML. Floating list positioning is applied client-side after hydration. Server rendering is exercised by its own test suite (`tests/src/ssr.test.ts`).
372
+
272
373
  ## A11y (Accessibility)
273
374
 
274
- Override these methods to change the `aria-context` and `aria-selection` text.
375
+ 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
376
 
276
- ```js
277
- export let ariaValues = (values) => {
278
- return `Option ${values}, selected.`;
279
- }
377
+ 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`, `ariaCleared`, and `ariaActiveTag` builders): focusing a valued Select announces the selection, and selecting/clearing announces the change.
280
378
 
281
- export let ariaListOpen = (label, count) => {
282
- return `You are currently focused on option ${label}. There are ${count} results available.`;
283
- }
379
+ 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
380
 
285
- export let ariaFocused = () => {
286
- return `Select is focused, type to refine list, press down to open the menu.`;
287
- }
381
+ 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:
382
+
383
+ ```svelte
384
+ <Select
385
+ {items}
386
+ ariaLabel="Choose a flavour"
387
+ ariaValues={(values) => `Option ${values}, selected.`}
388
+ ariaListOpen={(label, count) => `You are currently focused on option ${label}. There are ${count} results available.`}
389
+ ariaFocused={() => `Select is focused, type to refine list, press down to open the menu.`}
390
+ ariaEmpty={() => `No options`}
391
+ ariaLoading={() => `Loading Data`}
392
+ ariaCleared={() => `Selection cleared.`}
393
+ ariaClearSelectLabel="Clear selection"
394
+ ariaRemoveItemLabel={(label) => `Remove ${label}`}
395
+ ariaActiveTag={(label) => `${label} is active. Press Backspace to remove, or left and right arrow keys to move between selected options.`}
396
+ />
288
397
  ```
289
398
 
399
+ ## Migrating from svelte-select (Svelte 4)
400
+
401
+ The public API moved to idiomatic Svelte 5:
402
+
403
+ - **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.).
404
+ - **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`.
405
+ - **`export let` overrides โ†’ regular props.** Functions like `itemFilter`, `groupBy`, and the aria text builders are passed as props.
406
+ - **CSS variables are kebab-case.** As in svelte-select v5 โ€” but if you are coming from a pre-v5 version, camelCase names like `--borderRadius` are now `--border-radius`; see [the full list](/docs/theming_variables.md).
407
+
408
+ ## Migrating from 1.x to 2.0
409
+
410
+ Every change below is covered in detail in the [changelog](CHANGELOG.md); this is the upgrade checklist:
411
+
412
+ - **`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`).
413
+ - **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([])`.
414
+ - **`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).
415
+ - **Removed exports.** `useKeyboardNavigation` (with its `KeyboardNavigationContext`/`isCancelled` surface) and `isStringArray` are gone; the composables are internal.
416
+ - **`listStyle` is renamed `listStyles`**, matching the `containerStyles`/`inputStyles` pluralization.
417
+ - **`ErrorEvent` is renamed `SelectErrorEvent`** โ€” the old name shadowed the DOM's global `ErrorEvent` and has been removed; update imports.
418
+ - **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.
419
+ - **`SelectValue` takes a `Multiple` type parameter** (inferred from the `multiple` prop), and `onclear` receives the `Multiple`-discriminated `SelectClearValue` instead of a flat union.
420
+ - **`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.
421
+ - **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.
422
+ - **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.
423
+ - **`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.
424
+ - **`selectionSnippet` is `Snippet<[Item, number?]>`** โ€” always a single item, also in multiple mode.
425
+ - **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.
426
+ - **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>`).
427
+ - **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.
428
+ - **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.
429
+
290
430
  ## CSS custom properties (variables)
291
431
 
292
432
  You can style a component by overriding [the available CSS custom properties](/docs/theming_variables.md).
293
433
 
294
434
  ```html
295
435
  <script>
296
- import Select from 'svelte-5-select';
436
+ import { Select } from 'svelte-5-select';
297
437
  </script>
298
438
 
299
- <Select --border-radius= "10px" --placeholder-color="blue" />
439
+ <Select --border-radius="10px" --placeholder-color="blue" />
300
440
  ```
301
441
 
302
442
  You can also use the `inputStyles` prop to write in any override styles needed for the input.
303
443
 
304
444
  ```html
305
445
  <script>
306
- import Select from 'svelte-5-select';
446
+ import { Select } from 'svelte-5-select';
307
447
 
308
448
  const items = ['One', 'Two', 'Three'];
309
449
  </script>
@@ -312,9 +452,9 @@ You can also use the `inputStyles` prop to write in any override styles needed f
312
452
  ```
313
453
 
314
454
  ### ๐Ÿงช 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 `@extend` so PostCSS is required.
455
+ 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
456
 
317
457
 
318
458
  ## License
319
459
 
320
- [LIL](LICENSE)
460
+ [License](LICENSE)
@@ -1,11 +1,12 @@
1
- <svg
2
- width="100%"
3
- height="100%"
4
- viewBox="0 0 20 20"
5
- focusable="false"
6
- aria-hidden="true">
1
+ <script lang="ts">
2
+ // Required even though empty: script-less components make svelte2tsx emit a
3
+ // legacy d.ts referencing `SvelteComponent` without importing it, which breaks
4
+ // consumers that type-check with skipLibCheck: false.
5
+ </script>
6
+
7
+ <svg width="100%" height="100%" viewBox="0 0 20 20" focusable="false" aria-hidden="true">
7
8
  <path
8
- fill="currentColor"
9
+ fill="currentColor"
9
10
  d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747
10
11
  3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0
11
12
  1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502
@@ -14,9 +15,9 @@
14
15
  </svg>
15
16
 
16
17
  <style>
17
- svg {
18
- width: var(--chevron-icon-width, 20px);
19
- height: var(--chevron-icon-width, 20px);
20
- color: var(--chevron-icon-colour, currentColor);
21
- }
22
- </style>
18
+ svg {
19
+ width: var(--chevron-icon-width, 20px);
20
+ height: var(--chevron-icon-width, 20px);
21
+ color: var(--chevron-icon-colour, currentColor);
22
+ }
23
+ </style>
@@ -1,18 +1,5 @@
1
- export default ChevronIcon;
2
- type ChevronIcon = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const ChevronIcon: $$__sveltets_2_IsomorphicComponent<{
10
- [x: string]: never;
11
- }, {
12
- [evt: string]: CustomEvent<any>;
13
- }, {}, {}, string>;
14
1
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
- new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
16
3
  $$bindings?: Bindings;
17
4
  } & Exports;
18
5
  (internal: unknown, props: {
@@ -24,3 +11,8 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
24
11
  };
25
12
  z_$$bindings?: Bindings;
26
13
  }
14
+ declare const ChevronIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type ChevronIcon = InstanceType<typeof ChevronIcon>;
18
+ export default ChevronIcon;
@@ -1,16 +1,14 @@
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
+ <script lang="ts">
2
+ // Required even though empty: script-less components make svelte2tsx emit a
3
+ // legacy d.ts referencing `SvelteComponent` without importing it, which breaks
4
+ // consumers that type-check with skipLibCheck: false.
5
+ </script>
6
+
7
+ <svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation">
9
8
  <path
10
9
  fill="currentColor"
11
10
  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
- />
11
+ l3.641,3.641L27.641,22.688L38.564,33.61L34.923,37.251z" />
14
12
  </svg>
15
13
 
16
14
  <style>
@@ -19,4 +17,4 @@
19
17
  height: var(--clear-icon-width, 20px);
20
18
  color: var(--clear-icon-color, currentColor);
21
19
  }
22
- </style>
20
+ </style>