svelte-5-select 1.0.0 → 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.
Files changed (36) hide show
  1. package/README.md +239 -109
  2. package/dist/ChevronIcon.svelte +8 -13
  3. package/dist/ClearIcon.svelte +3 -11
  4. package/dist/LoadingIcon.svelte +9 -2
  5. package/dist/Select.svelte +955 -437
  6. package/dist/Select.svelte.d.ts +37 -5
  7. package/dist/aria-handlers.svelte.d.ts +4 -1
  8. package/dist/aria-handlers.svelte.js +19 -8
  9. package/dist/filter.d.ts +10 -2
  10. package/dist/filter.js +14 -7
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.js +1 -2
  13. package/dist/keyboard-navigation.svelte.d.ts +7 -2
  14. package/dist/keyboard-navigation.svelte.js +285 -47
  15. package/dist/no-styles/ChevronIcon.svelte +11 -0
  16. package/dist/no-styles/ChevronIcon.svelte.d.ts +26 -0
  17. package/dist/no-styles/ClearIcon.svelte +8 -0
  18. package/dist/no-styles/ClearIcon.svelte.d.ts +26 -0
  19. package/dist/no-styles/LoadingIcon.svelte +13 -0
  20. package/dist/no-styles/LoadingIcon.svelte.d.ts +26 -0
  21. package/dist/no-styles/Select.svelte +1383 -0
  22. package/dist/no-styles/Select.svelte.d.ts +38 -0
  23. package/dist/select-state.svelte.d.ts +15 -0
  24. package/dist/select-state.svelte.js +161 -0
  25. package/dist/styles/default.css +112 -71
  26. package/dist/tailwind.css +120 -17
  27. package/dist/types.d.ts +459 -129
  28. package/dist/use-hover.svelte.d.ts +3 -7
  29. package/dist/use-hover.svelte.js +91 -36
  30. package/dist/use-load-options.svelte.d.ts +18 -3
  31. package/dist/use-load-options.svelte.js +333 -42
  32. package/dist/use-value.svelte.d.ts +2 -11
  33. package/dist/use-value.svelte.js +322 -111
  34. package/dist/utils.d.ts +19 -8
  35. package/dist/utils.js +52 -8
  36. package/package.json +117 -95
package/dist/types.d.ts CHANGED
@@ -1,205 +1,535 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
3
  import type { ComputePositionConfig } from 'svelte-floating-ui/dom';
3
- export type SelectValue = SelectItem | SelectItem[] | null;
4
- export interface ErrorEvent {
5
- type: string;
4
+ /**
5
+ * The bound for user item types. Deliberately `Record<string, any>` rather than
6
+ * {@link SelectItem}: interface-declared item types have no implicit index
7
+ * signature, so they satisfy this bound but not `SelectItem` itself.
8
+ */
9
+ export type ItemLike = Record<string, any>;
10
+ /**
11
+ * The payload shape of `onValueChange`/`onSelectionChange`: an item array in
12
+ * multiple mode, a single item or null otherwise. `Multiple` defaults to
13
+ * `boolean`, which distributes to the loose `Item[] | Item | null` union.
14
+ */
15
+ export type SelectValue<Item extends ItemLike = SelectItem, Multiple extends boolean = boolean> = Multiple extends true ? Item[] : Item | null;
16
+ /**
17
+ * The bindable `value` prop shape: raw string ids are also accepted and are
18
+ * normalized into items against `items`.
19
+ *
20
+ * The component always *writes* `undefined` for an empty selection — in both single
21
+ * and multiple mode, and on every clear path (the clear button, removing the last
22
+ * tag, a `loadOptionsDeps` reload invalidating the value, a multiple→single switch).
23
+ * `null` is accepted on the way in, so an existing `bind:value` initialized to `null`
24
+ * keeps working; read an emptied value as falsy rather than testing `=== null`.
25
+ * (The `onValueChange`/`onSelectionChange` payload is a separate contract — see
26
+ * {@link SelectValue} — and reports `null` in single mode and `[]` in multiple mode.)
27
+ */
28
+ export type SelectValueProp<Item extends ItemLike = SelectItem, Multiple extends boolean = boolean> = Multiple extends true ? Item[] | string[] | null | undefined : Item | string | null | undefined;
29
+ /**
30
+ * The payload of `onclear`. Clearing the whole selection passes the full removed
31
+ * value (an `Item[]` in multiple mode); removing a single tag in multiple mode
32
+ * passes just that removed entry. Discriminated by `Multiple` so single mode never
33
+ * has to account for an array, and one-tag removal is only in the multiple branch.
34
+ * Raw string ids are included because `value` accepts them.
35
+ */
36
+ export type SelectClearValue<Item extends ItemLike = SelectItem, Multiple extends boolean = boolean> = Multiple extends true ? Item[] | string[] | Item | string | null : Item | string | null;
37
+ /** Payload of the `onerror` callback. `type` discriminates the failure source; `loadOptions` (a rejected loader promise) is currently the only emitter, with the rejection reason in `details`. */
38
+ export interface SelectErrorEvent {
39
+ type: 'loadOptions';
6
40
  details: unknown;
7
41
  }
8
- export interface FilterConfig {
9
- loadOptions?: (filterText: string) => Promise<SelectItem[] | string[]>;
42
+ /** The configuration object a custom `filter` prop receives — a snapshot of everything the built-in pipeline uses. See the exported `filter` for the default implementation to delegate to or replace. */
43
+ export interface FilterConfig<Item extends ItemLike = SelectItem> {
44
+ /** The `loadOptions` prop, if set. When present, results are already filtered remotely — the default pipeline skips `itemFilter` entirely. */
45
+ loadOptions?: (filterText: string) => Promise<Item[] | string[]>;
46
+ /** The current filter text. */
10
47
  filterText: string;
11
- items: SelectItem[] | string[] | null;
48
+ /** The `items` prop as supplied entries may still be raw strings; pass them through `convertStringItemsToObjects` before reading item fields. */
49
+ items: Item[] | string[] | null;
50
+ /** The `multiple` prop. */
12
51
  multiple: boolean;
13
- value: SelectItem | SelectItem[] | null | undefined;
52
+ /** The current (normalized) selection; raw string entries have already been synthesized into `SelectItem`s. Used with `filterSelectedItems` to hide picked options. */
53
+ value: Item | SelectItem | (Item | SelectItem)[] | null | undefined;
54
+ /** The `itemId` prop — the field name selections are compared by. */
14
55
  itemId: string;
15
- groupBy?: (item: SelectItem) => string | undefined;
56
+ /** The `groupBy` prop, if set. */
57
+ groupBy?: (item: Item) => string;
58
+ /** The `label` prop — the field name option text is read from. */
16
59
  label: string;
60
+ /** The `filterSelectedItems` prop: hide already-selected options in multiple mode. */
17
61
  filterSelectedItems: boolean;
18
- itemFilter: (label: string, filterText: string, option: SelectItem) => boolean;
62
+ /** The per-item match predicate (the `itemFilter` prop). The default pipeline skips it when `loadOptions` is set. */
63
+ itemFilter: (label: string, filterText: string, option: Item) => boolean;
64
+ /** Synthesizes `{ value, label, index }` items from raw string entries. */
19
65
  convertStringItemsToObjects: (items: string[]) => SelectItem[];
20
- filterGroupedItems: (items: SelectItem[]) => SelectItem[];
66
+ /**
67
+ * Transforms the flat filtered list into a grouped one (inserting headers and
68
+ * applying the `groupBy`/`groupFilter` props). Distinct from the `groupFilter`
69
+ * prop, which only reorders/filters the group keys this transform consumes.
70
+ */
71
+ applyGrouping: (items: SelectItem[]) => SelectItem[];
21
72
  }
73
+ /** The `floatingConfig` prop shape: floating-ui's `computePosition` options plus `autoUpdate` to reposition on scroll/resize. */
22
74
  export interface FloatingConfig extends Partial<ComputePositionConfig> {
23
75
  autoUpdate?: boolean;
24
76
  }
25
- export interface KeyboardNavigationContext {
26
- getState: () => {
27
- listOpen: boolean;
28
- filteredItems: SelectItem[];
29
- hoverItemIndex: number;
30
- multiple: boolean;
31
- value: SelectProps['value'];
32
- filterText: string;
33
- activeValue: number | undefined;
34
- itemId: string;
35
- focused: boolean;
36
- };
37
- setListOpen: (value: boolean) => void;
38
- setHoverItemIndex: (value: number) => void;
39
- setActiveValue: (value: number | undefined) => void;
77
+ /**
78
+ * The `justValue` shape: bare `itemId` values instead of whole items. Includes
79
+ * `undefined` because that is what the component writes for an emptied selection —
80
+ * on every clear path, mirroring `value` (see {@link SelectValueProp}). `null` and
81
+ * `[]` are accepted on the way in but never written back.
82
+ */
83
+ export type JustValue = string | number | string[] | number[] | null | undefined;
84
+ /** The default item shape (and the shape synthesized for raw string items). Your own item type does not need to extend it — any {@link ItemLike} works; these are just the fields the component reads when present. */
85
+ export interface SelectItem {
86
+ value?: unknown;
87
+ label?: string;
88
+ index?: number;
89
+ group?: string;
90
+ groupHeader?: boolean;
91
+ groupItem?: boolean;
92
+ selectable?: boolean;
93
+ id?: string | number;
94
+ [key: string]: unknown;
95
+ }
96
+ /**
97
+ * A group header row synthesized by `groupBy` and injected into the rendered list.
98
+ * It is not one of your items — it carries the group's own value and label plus the
99
+ * marker fields, and nothing else — so it cannot be read as an `Item`.
100
+ */
101
+ export interface SelectGroupHeader extends SelectItem {
102
+ groupHeader: true;
103
+ selectable: boolean;
104
+ }
105
+ /**
106
+ * A row of the rendered list. With `groupBy` set the list interleaves your items with
107
+ * synthesized {@link SelectGroupHeader} rows, so everything that reads the rendered
108
+ * list — `getFilteredItems()`, `onfilter`, `listSnippet`, `itemSnippet` — sees this
109
+ * union rather than a bare `Item`. Narrow it with {@link isGroupHeader}:
110
+ *
111
+ * ```svelte
112
+ * {#snippet itemSnippet(row)}
113
+ * {#if isGroupHeader(row)}
114
+ * <strong>{row.label}</strong>
115
+ * {:else}
116
+ * {row.name} <!-- row is your Item here -->
117
+ * {/if}
118
+ * {/snippet}
119
+ * ```
120
+ *
121
+ * Without `groupBy` no headers are ever produced, but the type cannot know that
122
+ * statically — the guard is a one-line narrow in that case.
123
+ */
124
+ export type SelectRow<Item extends ItemLike = SelectItem> = Item | SelectGroupHeader;
125
+ /**
126
+ * The single reactive state object shared by Select.svelte and its composables.
127
+ * Prop-backed fields are live accessors over the component's `$props()` bindings;
128
+ * the rest is internal shared state owned by `createSelectState`. Reading a field
129
+ * tracks only that field's signal.
130
+ */
131
+ export interface SelectState<Item extends ItemLike = SelectItem> {
132
+ value: Item | Item[] | string | string[] | null | undefined;
133
+ items: Item[] | string[] | null;
134
+ filterText: string;
135
+ justValue: JustValue;
136
+ listOpen: boolean;
137
+ loading: boolean;
138
+ focused: boolean;
139
+ hoverItemIndex: number;
140
+ readonly multiple: boolean;
141
+ readonly itemId: string;
142
+ readonly label: string;
143
+ readonly searchable: boolean;
144
+ readonly disabled: boolean;
145
+ readonly useJustValue: boolean;
146
+ readonly closeListOnChange: boolean;
147
+ readonly debounceWait: number;
148
+ readonly groupBy: ((item: Item) => string) | undefined;
149
+ readonly loadOptions: ((filterText: string) => Promise<Item[] | string[]>) | undefined;
150
+ readonly loadOptionsDeps: unknown[];
151
+ readonly filteredItems: SelectItem[];
152
+ readonly normalizedValue: SelectItem | SelectItem[] | null;
153
+ readonly hasValue: boolean;
154
+ activeValue: number | undefined;
155
+ isScrolling: boolean;
156
+ clearState: boolean;
157
+ prevValue: Item | Item[] | string | string[] | null | undefined;
158
+ prevFilterText: string | undefined;
159
+ prevMultiple: boolean | undefined;
160
+ /**
161
+ * One-shot handshake between type-ahead and the hover effects: set when a
162
+ * type-ahead keypress opens the list *and* parks hover on its match, so the
163
+ * open-with-value effect must not snap hover back to the selected value.
164
+ * Consumed (cleared) on the next run of that effect. Deliberately
165
+ * non-reactive scratch, like the prev* fields.
166
+ */
167
+ suppressValueHoverSnap: boolean;
168
+ /**
169
+ * Whether the user has expressed commit-intent since the list last opened:
170
+ * moved the option cursor (arrow/Home/End/Page keys, type-ahead), typed in
171
+ * the input, or moved the pointer over the open list (mousemove, not
172
+ * mouseover — browsers synthesize mouseover for content appearing under a
173
+ * stationary cursor). Tab only commits the hovered option when this is set —
174
+ * the cursor auto-parks on an option the moment the list opens, and a bare
175
+ * open-then-Tab must close without selecting. Reset whenever the list
176
+ * closes. Deliberately non-reactive scratch, like the prev* fields.
177
+ */
178
+ userNavigatedSinceOpen: boolean;
179
+ }
180
+ /** The subset of {@link SelectState} that keyboard navigation needs; any object with these fields works. */
181
+ export interface KeyboardNavigationState<Item extends ItemLike = SelectItem> {
182
+ listOpen: boolean;
183
+ readonly filteredItems: SelectItem[];
184
+ hoverItemIndex: number;
185
+ readonly multiple: boolean;
186
+ readonly value: Item | Item[] | string | string[] | null | undefined;
187
+ readonly filterText: string;
188
+ activeValue: number | undefined;
189
+ readonly itemId: string;
190
+ readonly label: string;
191
+ readonly searchable: boolean;
192
+ readonly focused: boolean;
193
+ readonly disabled: boolean;
194
+ /** See {@link SelectState.suppressValueHoverSnap} — written by type-ahead when it opens the list. */
195
+ suppressValueHoverSnap: boolean;
196
+ /** See {@link SelectState.userNavigatedSinceOpen} — gates Tab's commit-on-close. */
197
+ userNavigatedSinceOpen: boolean;
198
+ }
199
+ export interface KeyboardNavigationActions {
40
200
  closeList: () => void;
41
201
  setHoverIndex: (increment: number) => void;
42
202
  handleSelect: (item: SelectItem) => void;
43
- handleMultiItemClear: (index: number) => Promise<void>;
203
+ handleMultiItemClear: (index: number) => void | Promise<void>;
44
204
  }
45
- export interface HoverContext {
46
- getState: () => {
47
- listOpen: boolean;
48
- filteredItems: SelectItem[];
49
- hoverItemIndex: number;
50
- multiple: boolean;
51
- value: SelectProps['value'];
52
- isScrolling: boolean;
53
- groupBy: ((item: SelectItem) => string) | undefined;
54
- itemId: string;
55
- };
56
- setHoverItemIndex: (value: number) => void;
57
- setIsScrolling: (value: boolean) => void;
58
- onhoveritem: (index: number) => void;
59
- }
60
- export type JustValue = string | number | string[] | number[] | null;
61
- export interface ValueContext {
62
- getState: () => {
63
- value: SelectProps['value'];
64
- prevValue: SelectProps['value'];
65
- items: SelectItem[] | string[] | null;
66
- multiple: boolean;
67
- itemId: string;
68
- label: string;
69
- hasValue: boolean;
70
- normalizedValue: SelectItem | SelectItem[] | null;
71
- useJustValue: boolean;
72
- justValue: JustValue | undefined;
73
- clearState: boolean;
74
- closeListOnChange: boolean;
75
- };
76
- setValue: (value: SelectProps['value']) => void;
77
- setJustValue: (value: JustValue) => void;
78
- setPrevValue: (value: SelectProps['value']) => void;
79
- setClearState: (value: boolean) => void;
80
- setActiveValue: (value: number | undefined) => void;
81
- setFilterText: (value: string) => void;
205
+ export interface ValueActions {
82
206
  closeList: () => void;
83
- oninput: (value: SelectProps['value']) => void;
84
- onchange: (value: SelectProps['value']) => void;
85
- onclear: (value: SelectProps['value'] | SelectItem) => void;
207
+ /**
208
+ * Retire a pending dependency-reload's validation verdict when the selection
209
+ * being committed was picked from fresher results (see useLoadOptions).
210
+ * Optional: standalone useValue has no async loading to retire.
211
+ */
212
+ retireStaleValidation?: () => void;
213
+ onValueChange: (value: SelectItem | string | (SelectItem | string)[] | null | undefined) => void;
214
+ onSelectionChange: (value: SelectItem | string | (SelectItem | string)[] | null | undefined) => void;
215
+ onclear: (value: SelectItem | string | (SelectItem | string)[] | null | undefined) => void;
86
216
  onselect: (selection: SelectItem) => void;
87
217
  }
88
- export interface LoadOptionsContext {
89
- getState: () => {
90
- filterText: string;
91
- prevFilterText: string | undefined;
92
- loadOptionsDeps: any[];
93
- loadOptions: ((filterText: string) => Promise<SelectItem[] | string[]>) | undefined;
94
- disabled: boolean;
95
- multiple: boolean;
96
- value: SelectProps['value'];
97
- items: SelectItem[] | string[] | null;
98
- itemId: string;
99
- useJustValue: boolean;
100
- justValue: JustValue | undefined;
101
- listOpen: boolean;
102
- debounceWait: number;
103
- };
104
- setItems: (items: SelectItem[] | string[] | null) => void;
105
- setValue: (value: SelectProps['value']) => void;
106
- setJustValue: (value: JustValue) => void;
107
- setLoading: (value: boolean) => void;
108
- setListOpen: (value: boolean) => void;
218
+ export interface LoadOptionsActions {
109
219
  debounce: (fn: () => void, wait: number) => void;
110
- convertStringItemsToObjects: (items: string[]) => SelectItem[];
111
220
  onloaded: (options: SelectItem[]) => void;
112
- onerror: (error: ErrorEvent) => void;
113
- }
114
- export interface ScrollActionParams {
115
- scroll: boolean;
116
- }
117
- export interface SelectItem {
118
- value?: any;
119
- label?: string;
120
- index?: number;
121
- group?: string;
122
- groupHeader?: boolean;
123
- groupItem?: boolean;
124
- selectable?: boolean;
125
- id?: string | number;
126
- [key: string]: any;
221
+ onerror: (error: SelectErrorEvent) => void;
127
222
  }
128
- export interface SelectProps {
223
+ /**
224
+ * Props of the `<Select>` component.
225
+ *
226
+ * The surface is deliberately closed: unknown attributes are not spread onto the
227
+ * container element. Style via `class`/`containerStyles`, reach the input element
228
+ * via `inputAttributes`/`id`, and wrap the component when the container itself
229
+ * needs extra attributes (e.g. `data-testid`). Keeping it closed means a later
230
+ * release can add passthrough without a breaking change — the reverse is not true.
231
+ */
232
+ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extends boolean = false> {
233
+ /**
234
+ * Bindable. The text typed into the input; drives filtering and `loadOptions`.
235
+ * An initial value is kept on mount and applied passively: it filters (and
236
+ * fetches, with `loadOptions`) but does not open the list or move focus.
237
+ * Later programmatic writes behave like typing and open the list.
238
+ *
239
+ * The DOM input's value is only ever this filter text — a selection is
240
+ * rendered alongside the input (and announced via the live regions), never
241
+ * written into the textbox. Read the selection from `value`/`justValue` or
242
+ * the form field (`name`), not from the DOM input.
243
+ */
129
244
  filterText?: string;
245
+ /** Which field uniquely identifies an item. Values are compared by this field, not by reference. @default 'value' */
130
246
  itemId?: string;
131
- items?: SelectItem[] | string[] | null;
247
+ /**
248
+ * Bindable. The options to choose from. A `string[]` is converted to
249
+ * `{ value, label, index }` items for you. Leave unset when using `loadOptions`,
250
+ * which populates this.
251
+ */
252
+ items?: Item[] | string[] | null;
253
+ /**
254
+ * Bindable, and only meaningful with `useJustValue`: the selection reduced to bare
255
+ * `itemId` values (`'a'` / `['a', 'b']`) instead of whole items. Writing it while
256
+ * no selection exists hydrates `value` — the matching items are resolved out of
257
+ * `items`, retrying when async items arrive. While a selection exists, `justValue`
258
+ * is derived from it and a conflicting write is corrected back: write (or clear)
259
+ * `value` to change the selection.
260
+ *
261
+ * Empty is always `undefined`: every clear path writes `undefined`, never `''`,
262
+ * `null`, or `[]` — read an emptied `justValue` as falsy rather than `=== null`.
263
+ */
132
264
  justValue?: JustValue;
265
+ /** Which field holds an item's display text. @default 'label' */
133
266
  label?: string;
134
- value?: SelectItem | SelectItem[] | string | null;
267
+ /**
268
+ * Bindable. The current selection: an item (or `Item[]` with `multiple`), or a raw
269
+ * string id that is normalized against `items`. Empty is always `undefined` —
270
+ * see {@link SelectValueProp}.
271
+ */
272
+ value?: SelectValueProp<Item, Multiple>;
273
+ /**
274
+ * Blocks interaction: the list closes, focus is released, and typing and
275
+ * keyboard navigation are ignored. The selection is kept — except with
276
+ * `loadOptions`, where disabling clears `value` and `items` (loaded options
277
+ * may be stale by re-enable). Rendered as `aria-disabled` + `readonly`
278
+ * rather than the native `disabled` attribute, so the current value stays
279
+ * in the accessibility tree.
280
+ */
135
281
  disabled?: boolean;
282
+ /**
283
+ * Bindable. Whether the input has focus. Writable: setting `true` moves DOM
284
+ * focus to the input, `false` blurs it and closes the list.
285
+ */
136
286
  focused?: boolean;
287
+ /** Applies the error styling and sets `aria-invalid`. Pair with `ariaErrorMessage`. */
137
288
  hasError?: boolean;
289
+ /** Renders nothing instead of the "No options" row when the list is empty. Also suppresses a supplied `emptySnippet`. */
138
290
  hideEmptyState?: boolean;
291
+ /** The input's `id`, for an external `<label for>`. Also seeds every internal id; one is generated if unset. */
139
292
  id?: string | null;
293
+ /** Bindable. Whether the option list is open. */
140
294
  listOpen?: boolean;
295
+ /** Bindable. Shows the spinner and sets `aria-busy`. Managed for you while `loadOptions` is in flight. */
141
296
  loading?: boolean;
297
+ /** Name of the hidden input, for native form submission. */
142
298
  name?: string | null;
299
+ /** Prompt shown in the empty input; also the input's last-resort accessible name (see `ariaLabel`). @default 'Please select' */
143
300
  placeholder?: string;
301
+ /** Keep the placeholder visible even when an item is selected (multiple mode). */
144
302
  placeholderAlwaysShow?: boolean;
303
+ /** Show the chevron indicator on the right of the control. @default false */
145
304
  showChevron?: boolean;
305
+ /** Show the clear-all button. @default true */
146
306
  clearable?: boolean;
307
+ /** Reset `filterText` when the input loses focus. @default true */
147
308
  clearFilterTextOnBlur?: boolean;
309
+ /** Close the list after a selection. Set `false` to keep picking in multiple mode. @default true */
148
310
  closeListOnChange?: boolean;
311
+ /** Hide already-selected items from the list. @default true */
149
312
  filterSelectedItems?: boolean;
313
+ /**
314
+ * Let group headers be selected like options; otherwise they are presentational.
315
+ *
316
+ * A selected header is the synthesized {@link SelectGroupHeader} row, not one of
317
+ * your items — but `value` and the `onselect`/`onSelectionChange`/`onValueChange`
318
+ * payloads deliberately keep their ergonomic `Item` typing rather than forcing
319
+ * every consumer through a union. If you enable this, narrow those payloads with
320
+ * `isGroupHeader` before reading item fields off them.
321
+ */
150
322
  groupHeaderSelectable?: boolean;
323
+ /** Clicking anywhere on a tag removes it, and the whole tag becomes a keyboard tab stop. */
151
324
  multiFullItemClearable?: boolean;
152
- multiple?: boolean;
325
+ /**
326
+ * Allow several selections, rendered as tags. Drives typing: with `multiple`,
327
+ * `value` and the `onValueChange`/`onSelectionChange` payloads narrow to `Item[]`.
328
+ */
329
+ multiple?: Multiple;
330
+ /** Block native form submission while nothing is selected, and set `aria-required`. */
153
331
  required?: boolean;
332
+ /** Allow typing to filter. When `false` the input is read-only and printable keys jump to matching options. @default true */
154
333
  searchable?: boolean;
334
+ /** Mirror the selection into `justValue` as bare ids, and hydrate `value` from it on mount. */
155
335
  useJustValue?: boolean;
336
+ /** Extra class(es) on the container, alongside `svelte-select`. */
156
337
  class?: string;
338
+ /** Inline styles on the container. */
157
339
  containerStyles?: string;
340
+ /** Inline styles on the text input. */
158
341
  inputStyles?: string;
159
- listStyle?: string;
342
+ /** Inline styles on the option list. */
343
+ listStyles?: string;
344
+ /** How long typing settles before `loadOptions` fires, in ms. Only typing is debounced. @default 300 */
160
345
  debounceWait?: number;
346
+ /** Passed to floating-ui to position the list (placement, middleware, autoUpdate). */
161
347
  floatingConfig?: FloatingConfig;
348
+ /** Bindable. Index of the option under the keyboard cursor. */
162
349
  hoverItemIndex?: number;
163
- inputAttributes?: Record<string, any>;
350
+ /**
351
+ * Extra attributes for the text input. Merged over the component's own ARIA
352
+ * wiring, so it can override it. Event handlers are the exception: an
353
+ * `oninput`/`onblur`/`onfocus`/`onkeydown` here composes with (runs after)
354
+ * the component's own handler instead of replacing it, so filtering, focus
355
+ * handling, and keyboard navigation keep working alongside yours.
356
+ */
357
+ inputAttributes?: HTMLInputAttributes;
358
+ /** Size the list to the container's width. @default true */
164
359
  listAutoWidth?: boolean;
360
+ /** Gap between the container and the list, in px. @default 5 */
165
361
  listOffset?: number;
166
- loadOptionsDeps?: any[];
167
- createGroupHeaderItem?: (groupValue: string, item: SelectItem) => SelectItem;
362
+ /**
363
+ * Values that `loadOptions` depends on (e.g. a parent select's value). When any of
364
+ * them changes, options are re-fetched immediately (no debounce) and the current
365
+ * selection is validated against the result: entries a non-empty result no longer
366
+ * offers are dropped (the whole value clears to `undefined` only when nothing
367
+ * survives). An empty result is treated as no evidence and never clears — a search
368
+ * endpoint returns nothing for the retained (usually empty) filter text regardless
369
+ * of the selection. Typing-driven loads never invalidate the selection this way.
370
+ *
371
+ * Elements are compared by identity (`===`): pass primitives (`[country.id]`) or
372
+ * stable references, not inline literals (`[{ country }]` recreates the object per
373
+ * parent render and re-fires the reload every time — a dev-only warning surfaces
374
+ * this).
375
+ */
376
+ loadOptionsDeps?: unknown[];
377
+ /** Builds the header row for a group; receives the group's value and its first item. */
378
+ createGroupHeaderItem?: (groupValue: string, item: NoInfer<Item>) => SelectItem;
379
+ /** Replace the debounce strategy used for typing-driven `loadOptions` calls. */
168
380
  debounce?: (fn: () => void, wait: number) => void;
169
- filter?: (config: FilterConfig) => SelectItem[];
170
- getFilteredItems?: () => SelectItem[];
171
- groupBy?: ((item: SelectItem) => string) | undefined;
381
+ /**
382
+ * Replace the whole filtering pipeline. Most cases only need `itemFilter`.
383
+ * May return your own `Item`s directly, rows from `config.applyGrouping` /
384
+ * `config.convertStringItemsToObjects`, or a mix.
385
+ */
386
+ filter?: (config: FilterConfig<NoInfer<Item>>) => (Item | SelectItem)[];
387
+ /**
388
+ * Returns the group an item belongs to. Setting this makes the list interleave
389
+ * synthesized header rows with your items — see {@link SelectRow}.
390
+ */
391
+ groupBy?: ((item: NoInfer<Item>) => string) | undefined;
392
+ /** Reorders or filters the group keys produced by `groupBy`. */
172
393
  groupFilter?: (groups: string[]) => string[];
173
- itemFilter?: (label: string, filterText: string, option: SelectItem) => boolean;
174
- loadOptions?: (filterText: string) => Promise<SelectItem[] | string[]>;
394
+ /** Decides whether one option survives the current `filterText`. Not applied to `loadOptions` results. */
395
+ itemFilter?: (label: string, filterText: string, option: NoInfer<Item>) => boolean;
396
+ /**
397
+ * Fetches options asynchronously. Runs on mount, on typing (debounced by
398
+ * `debounceWait`), and whenever `loadOptionsDeps` or `disabled` changes.
399
+ * Opening or closing the list never fetches, with one exception: reopening
400
+ * with retained filter text whose load was cancelled on close (e.g. with
401
+ * `clearFilterTextOnBlur={false}`) refreshes the stale results immediately.
402
+ * Whatever it returns is what the list shows: results are not re-filtered by
403
+ * `filterText`. Set `items` or this, not both.
404
+ */
405
+ loadOptions?: (filterText: string) => Promise<Item[] | string[]>;
406
+ /** Announced when the selection is cleared. */
407
+ ariaCleared?: () => string;
408
+ /** `aria-label` for the clear-all button. */
409
+ ariaClearSelectLabel?: string;
410
+ /** Announced when an open list has no results. Also rendered as the list's visible empty-state copy (unless `emptySnippet` replaces it), so overriding it localizes both. */
411
+ ariaEmpty?: () => string;
412
+ /** id of an external element describing the error; wired to `aria-errormessage` only while `hasError` is true. */
413
+ ariaErrorMessage?: string;
414
+ /** Announced when the input is focused with the list closed. The default omits its "type to refine list" phrase when `searchable` is `false`. */
175
415
  ariaFocused?: () => string;
416
+ /**
417
+ * Accessible name for the input. Prefer an external `<label for={id}>`; this
418
+ * overrides it. With neither, the placeholder is a last-resort fallback that some
419
+ * screen readers ignore, and a dev-only warning fires.
420
+ */
176
421
  ariaLabel?: string;
422
+ /**
423
+ * Announced when the list opens and when filtering changes the result count — not
424
+ * on every arrow key, since `aria-activedescendant` already announces each option
425
+ * as the cursor reaches it. Receives the focused option's label and the count.
426
+ */
177
427
  ariaListOpen?: (label: string, count: number) => string;
428
+ /** Announced while an open list is still fetching options. Also rendered as the list's visible loading copy (unless `emptySnippet` replaces it), so overriding it localizes both. */
429
+ ariaLoading?: () => string;
430
+ /** `aria-label` for each multi-select tag's remove button; receives the item's label. */
431
+ ariaRemoveItemLabel?: (label: string) => string;
432
+ /** Announced when the selection changes; receives the selected labels, comma-joined. */
178
433
  ariaValues?: (values: string) => string;
179
- handleClear?: () => void;
434
+ /**
435
+ * Replaces the clear button's behavior entirely — the default clear (emptying
436
+ * `value`, firing `onclear`, closing the list, refocusing the input) does not run,
437
+ * so an override owns all of it. To merely observe a clear, use `onclear`.
438
+ */
439
+ handleClear?: (e?: MouseEvent) => void;
440
+ /** Literal DOM `blur` passthrough from the text input. Fires after the component's own blur handling (list close, filter-text reset). */
180
441
  onblur?: (e: FocusEvent) => void;
181
- onchange?: (value: SelectValue) => void;
182
- onclear?: (value: SelectValue) => void;
183
- onerror?: (error: ErrorEvent) => void;
184
- onfilter?: (items: SelectItem[]) => void;
442
+ /** Clear-all receives the full removed value; removing one tag receives that single entry. */
443
+ onclear?: (value: SelectClearValue<Item, Multiple>) => void;
444
+ /** Fires when a `loadOptions` promise rejects. */
445
+ onerror?: (error: SelectErrorEvent) => void;
446
+ /**
447
+ * Fires with the rendered list whenever it changes while open. Rows include any
448
+ * synthesized group headers — narrow with `isGroupHeader`.
449
+ */
450
+ onfilter?: (items: SelectRow<Item>[]) => void;
451
+ /** Literal DOM `focus` passthrough from the text input. Fires after the component's own focus handling. */
185
452
  onfocus?: (e: FocusEvent) => void;
453
+ /** Fires with the index of the option under the keyboard/mouse cursor. */
186
454
  onhoveritem?: (index: number) => void;
187
- oninput?: (value: SelectValue) => void;
188
- onloaded?: (options: SelectItem[]) => void;
189
- onselect?: (selection: SelectItem) => void;
455
+ /**
456
+ * Fires with the options a `loadOptions` call resolved. When the loader
457
+ * resolves raw strings, the delivered options are the synthesized
458
+ * `{ value, label, index }` items built from them — `SelectItem`s, not your
459
+ * item type — hence the widened element type. An item-resolving loader only
460
+ * ever delivers `Item`s.
461
+ */
462
+ onloaded?: (options: (Item | SelectItem)[]) => void;
463
+ /**
464
+ * Fires alongside {@link SelectProps.onSelectionChange} when the user picks an
465
+ * option, but receives just the item selected rather than the whole value.
466
+ */
467
+ onselect?: (selection: Item) => void;
468
+ /**
469
+ * Fires only when the user picks an option from the list — never on a clear, a
470
+ * programmatic `bind:value` write, or a `loadOptionsDeps` invalidation. Use
471
+ * {@link SelectProps.onValueChange} to observe every value change instead.
472
+ * (Renamed from `onchange` in v2: it is a component state-change callback, not
473
+ * the DOM `change` event.)
474
+ */
475
+ onSelectionChange?: (value: SelectValue<Item, Multiple>) => void;
476
+ /**
477
+ * Fires on *every* value change — a user selection, a clear, a programmatic
478
+ * `bind:value` write, or a `loadOptionsDeps` invalidation. Contrast
479
+ * {@link SelectProps.onSelectionChange}, which only fires for a user selection.
480
+ * An emptied value arrives here as `null` (single) or `[]` (multiple), which is
481
+ * the one place those shapes appear — `bind:value` itself always empties to
482
+ * `undefined`. (Renamed from `oninput` in v2: typing is `bind:filterText`, and
483
+ * an `oninput` that never fired per keystroke read as the DOM event it wasn't.)
484
+ */
485
+ onValueChange?: (value: SelectValue<Item, Multiple>) => void;
486
+ /** Replaces the chevron icon; the boolean is `listOpen`, so the icon can flip while open. Rendered only with `showChevron`. */
190
487
  chevronIconSnippet?: Snippet<[boolean]>;
488
+ /** Replaces the icon inside the clear-all button (the button itself, its `ariaClearSelectLabel`, and its behavior stay). */
191
489
  clearIconSnippet?: Snippet;
490
+ /** Replaces the "No options" row of an open, empty list. Suppressed entirely by `hideEmptyState`. */
192
491
  emptySnippet?: Snippet;
193
- inputHiddenSnippet?: Snippet<[SelectValue]>;
194
- itemSnippet?: Snippet<[SelectItem, number]>;
492
+ /**
493
+ * Replaces the hidden input(s) used for native form submission; receives the
494
+ * current value. Overriding takes charge of serialization — render your own
495
+ * `<input type="hidden" name={...}>` (the default submits JSON-stringified
496
+ * items, or bare ids with `useJustValue`).
497
+ */
498
+ inputHiddenSnippet?: Snippet<[SelectValueProp<Item, Multiple> | undefined]>;
499
+ /**
500
+ * Renders one row of the list. Also renders synthesized group headers when
501
+ * `groupBy` is set — narrow with `isGroupHeader`.
502
+ */
503
+ itemSnippet?: Snippet<[SelectRow<Item>, number]>;
504
+ /** Rendered inside the list after the options — e.g. a "load more" row or footer. */
195
505
  listAppendSnippet?: Snippet;
506
+ /** Rendered inside the list before the options — e.g. a sticky header. */
196
507
  listPrependSnippet?: Snippet;
197
- listSnippet?: Snippet<[SelectItem[]]>;
508
+ /**
509
+ * Replaces the entire list body. Rows include any synthesized group headers —
510
+ * narrow with `isGroupHeader`.
511
+ *
512
+ * Note: taking over rendering means `aria-activedescendant` can no longer resolve
513
+ * to an option, so give each rendered option `id="listbox-{id}-item-{index}"` and
514
+ * `role="option"` to keep the combobox navigable by screen readers.
515
+ */
516
+ listSnippet?: Snippet<[SelectRow<Item>[]]>;
517
+ /** Replaces the spinner shown while `loading` is true (or a `loadOptions` fetch is in flight). */
198
518
  loadingIconSnippet?: Snippet;
519
+ /** Replaces the icon inside each tag's remove button in multiple mode (the button and its `ariaRemoveItemLabel` stay). */
199
520
  multiClearIconSnippet?: Snippet;
521
+ /** Rendered inside the control before the selection and input — e.g. a search icon. */
200
522
  prependSnippet?: Snippet;
201
- requiredSnippet?: Snippet<[SelectValue]>;
202
- selectionSnippet?: Snippet<[SelectItem | SelectItem[], number?]>;
523
+ /**
524
+ * Replaces the hidden native `<select required>` fallback that blocks form
525
+ * submission while `required` and empty; receives the current value. An
526
+ * override owns the constraint-validation behavior.
527
+ */
528
+ requiredSnippet?: Snippet<[SelectValueProp<Item, Multiple> | undefined]>;
529
+ /** Receives one item at a time: the value in single mode, each tag in multiple mode. */
530
+ selectionSnippet?: Snippet<[NoInfer<Item>, number?]>;
531
+ /** Bindable, read-only: the container element, once mounted. */
203
532
  container?: HTMLDivElement;
533
+ /** Bindable, read-only: the text input element, once mounted. */
204
534
  input?: HTMLInputElement;
205
535
  }