svelte-5-select 2.0.0 → 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 (37) hide show
  1. package/README.md +21 -11
  2. package/dist/ChevronIcon.svelte +6 -0
  3. package/dist/ChevronIcon.svelte.d.ts +6 -14
  4. package/dist/ClearIcon.svelte +6 -0
  5. package/dist/ClearIcon.svelte.d.ts +6 -14
  6. package/dist/LoadingIcon.svelte +6 -0
  7. package/dist/LoadingIcon.svelte.d.ts +6 -14
  8. package/dist/Select.svelte +104 -16
  9. package/dist/Select.svelte.d.ts +2 -2
  10. package/dist/aria-handlers.svelte.d.ts +1 -1
  11. package/dist/aria-handlers.svelte.js +12 -1
  12. package/dist/filter.d.ts +1 -1
  13. package/dist/filter.js +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +2 -2
  16. package/dist/keyboard-navigation.svelte.d.ts +1 -1
  17. package/dist/keyboard-navigation.svelte.js +9 -1
  18. package/dist/no-styles/ChevronIcon.svelte +6 -0
  19. package/dist/no-styles/ChevronIcon.svelte.d.ts +6 -14
  20. package/dist/no-styles/ClearIcon.svelte +6 -0
  21. package/dist/no-styles/ClearIcon.svelte.d.ts +6 -14
  22. package/dist/no-styles/LoadingIcon.svelte +6 -0
  23. package/dist/no-styles/LoadingIcon.svelte.d.ts +6 -14
  24. package/dist/no-styles/Select.svelte +85 -16
  25. package/dist/no-styles/Select.svelte.d.ts +2 -2
  26. package/dist/select-state.svelte.d.ts +1 -1
  27. package/dist/styles/default.css +19 -0
  28. package/dist/tailwind.css +19 -0
  29. package/dist/types.d.ts +42 -13
  30. package/dist/use-hover.svelte.d.ts +1 -1
  31. package/dist/use-hover.svelte.js +24 -5
  32. package/dist/use-load-options.svelte.d.ts +1 -1
  33. package/dist/use-load-options.svelte.js +31 -2
  34. package/dist/use-value.svelte.d.ts +1 -1
  35. package/dist/use-value.svelte.js +13 -1
  36. package/dist/utils.d.ts +1 -1
  37. package/package.json +6 -2
package/README.md CHANGED
@@ -1,16 +1,16 @@
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
 
@@ -54,7 +54,7 @@ List position and floating is powered by `floating-ui`, see their [package-entry
54
54
  | focused | `boolean` | `false` | Input focus; set `true` to focus the input, `false` to blur it and close the list |
55
55
  | listAutoWidth | `boolean` | `true` | If `false` will ignore width of select |
56
56
  | showChevron | `boolean` | `false` | Show chevron |
57
- | inputAttributes | `object` | `{}` | Pass in HTML attributes to Select's input; `on*` handlers run after the component's own, not instead of them |
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` |
58
58
  | placeholderAlwaysShow | `boolean` | `false` | When `multiple` placeholder text will always show |
59
59
  | loading | `boolean` | `false` | Shows `loading-icon`. `loadOptions` will override this |
60
60
  | listOffset | `number` | `5` | `px` space between select and list |
@@ -82,6 +82,7 @@ List position and floating is powered by `floating-ui`, see their [package-entry
82
82
  | ariaFocused | `() => string` | see below | Announcement when the input receives focus |
83
83
  | ariaListOpen | `(label: string, count: number) => string` | see below | Announcement when the list opens on a focused option |
84
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 |
85
86
 
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).
87
88
 
@@ -219,7 +220,7 @@ You can also use custom collections.
219
220
 
220
221
  ### Async Items
221
222
 
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.
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.
223
224
 
224
225
  ```html
225
226
  <script>
@@ -340,10 +341,12 @@ Internal wiring (the shared state store, the composables, and their types) is de
340
341
 
341
342
  ## TypeScript
342
343
 
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
+ 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).
344
345
 
345
346
  ```svelte
346
347
  <script lang="ts">
348
+ import { Select } from 'svelte-5-select';
349
+
347
350
  interface Country {
348
351
  code: string;
349
352
  name: string;
@@ -361,11 +364,17 @@ The component is generic over your item type: values, items, snippets, and callb
361
364
 
362
365
  `SelectProps`, `SelectItem`, `SelectValue`, and `SelectValueProp` (the bindable `value` shape, which also accepts raw string ids) are exported for annotating your own wrappers.
363
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
+
364
373
  ## A11y (Accessibility)
365
374
 
366
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`).
367
376
 
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.
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.
369
378
 
370
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.
371
380
 
@@ -383,6 +392,7 @@ Selection and list state (including the empty/loading state) are announced throu
383
392
  ariaCleared={() => `Selection cleared.`}
384
393
  ariaClearSelectLabel="Clear selection"
385
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.`}
386
396
  />
387
397
  ```
388
398
 
@@ -393,7 +403,7 @@ The public API moved to idiomatic Svelte 5:
393
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.).
394
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`.
395
405
  - **`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).
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).
397
407
 
398
408
  ## Migrating from 1.x to 2.0
399
409
 
@@ -426,7 +436,7 @@ You can style a component by overriding [the available CSS custom properties](/d
426
436
  import { Select } from 'svelte-5-select';
427
437
  </script>
428
438
 
429
- <Select --border-radius= "10px" --placeholder-color="blue" />
439
+ <Select --border-radius="10px" --placeholder-color="blue" />
430
440
  ```
431
441
 
432
442
  You can also use the `inputStyles` prop to write in any override styles needed for the input.
@@ -1,3 +1,9 @@
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
+
1
7
  <svg width="100%" height="100%" viewBox="0 0 20 20" focusable="false" aria-hidden="true">
2
8
  <path
3
9
  fill="currentColor"
@@ -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,3 +1,9 @@
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
+
1
7
  <svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation">
2
8
  <path
3
9
  fill="currentColor"
@@ -1,18 +1,5 @@
1
- export default ClearIcon;
2
- type ClearIcon = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const ClearIcon: $$__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 ClearIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type ClearIcon = InstanceType<typeof ClearIcon>;
18
+ export default ClearIcon;
@@ -1,3 +1,9 @@
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
+
1
7
  <svg class="loading" viewBox="25 25 50 50" aria-hidden="true" focusable="false">
2
8
  <circle
3
9
  class="circle_path"
@@ -1,18 +1,5 @@
1
- export default LoadingIcon;
2
- type LoadingIcon = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const LoadingIcon: $$__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 LoadingIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type LoadingIcon = InstanceType<typeof LoadingIcon>;
18
+ export default LoadingIcon;
@@ -20,16 +20,16 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
20
20
  SelectValue,
21
21
  SelectClearValue,
22
22
  SelectErrorEvent,
23
- } from './types';
23
+ } from './types.js';
24
24
  import { createFloatingActions } from 'svelte-floating-ui';
25
25
  import { useAriaHandlers } from './aria-handlers.svelte';
26
26
 
27
- import _filter from './filter';
27
+ import _filter from './filter.js';
28
28
 
29
29
  import ChevronIcon from './ChevronIcon.svelte';
30
30
  import ClearIcon from './ClearIcon.svelte';
31
31
  import LoadingIcon from './LoadingIcon.svelte';
32
- import type { SelectItem } from './types';
32
+ import type { SelectItem } from './types.js';
33
33
  import type { HTMLInputAttributes } from 'svelte/elements';
34
34
  import { createSelectState } from './select-state.svelte';
35
35
  import { useKeyboardNavigation } from './keyboard-navigation.svelte';
@@ -43,7 +43,7 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
43
43
  isItemSelectableCheck,
44
44
  normalizeItem,
45
45
  createGroupHeaderItem as _createGroupHeaderItem,
46
- } from './utils';
46
+ } from './utils.js';
47
47
 
48
48
  const defaultItemFilter = (label: string, filterText: string, _option: SelectItem): boolean =>
49
49
  `${label}`.toLowerCase().includes(filterText?.toLowerCase());
@@ -118,6 +118,8 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
118
118
  // ARIA props
119
119
  ariaLabel = undefined,
120
120
  ariaErrorMessage = undefined,
121
+ ariaActiveTag = (label: string) =>
122
+ `${label} is active. Press Backspace to remove, or left and right arrow keys to move between selected options.`,
121
123
  ariaClearSelectLabel = 'Clear selection',
122
124
  ariaRemoveItemLabel = (label: string) => `Remove ${label}`,
123
125
  ariaCleared = () => {
@@ -335,6 +337,15 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
335
337
  (consumer as (e: Event) => unknown)(e);
336
338
  };
337
339
  }
340
+ // aria-describedby composes the same way: the attribute takes a
341
+ // space-separated id list, so a consumer description must extend the
342
+ // selection description, not silently replace it (later-spread-wins
343
+ // dropped the selection from browse-mode reading).
344
+ const consumerDescribedby = inputAttributes?.['aria-describedby'];
345
+ const ownDescribedby = !multiple && value ? `selected-${_id}` : undefined;
346
+ if (consumerDescribedby && ownDescribedby) {
347
+ attrs['aria-describedby'] = `${ownDescribedby} ${consumerDescribedby}`;
348
+ }
338
349
  return attrs;
339
350
  });
340
351
  let prefloat = $state(true);
@@ -505,7 +516,7 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
505
516
  );
506
517
  let ariaContext = $derived.by(() => {
507
518
  if (activeTagLabel !== undefined) {
508
- return `${activeTagLabel} is active. Press Backspace to remove, or left and right arrow keys to move between selected options.`;
519
+ return ariaActiveTag(activeTagLabel);
509
520
  }
510
521
  // Tracked triggers: the list opening/closing, the result count changing
511
522
  // (filtering), and the loading flag. hoverItemIndex is deliberately read
@@ -561,6 +572,10 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
561
572
  // triggers scrolling.
562
573
  function handleKeyDown(e: KeyboardEvent): void {
563
574
  keyboardNav.handleKeyDown(e);
575
+ // Mirror keyboardNav's IME gate: during composition the arrows move
576
+ // through the IME candidate window, not the list — the keyboard cursor
577
+ // stays put, so the list must not scroll either.
578
+ if (e.isComposing || e.keyCode === 229) return;
564
579
  const isTypeAhead = !searchable && e.key.length === 1;
565
580
  if (
566
581
  e.key === 'ArrowDown' ||
@@ -740,8 +755,13 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
740
755
  const currentItemId = itemId;
741
756
  untrack(() => {
742
757
  if (warnedAboutMissingItemId || !currentItems?.length) return;
743
- const missing = (currentItems as (Item | string)[]).some(
744
- (item) => typeof item !== 'string' && getItemProperty(item, currentItemId) === undefined,
758
+ // String items are converted to `{ value, label, index }` rows, so
759
+ // any other `itemId` never exists on them the same silent
760
+ // identity collapse the object-item check catches.
761
+ const missing = (currentItems as (Item | string)[]).some((item) =>
762
+ typeof item === 'string'
763
+ ? !['value', 'label', 'index'].includes(currentItemId)
764
+ : getItemProperty(item, currentItemId) === undefined,
745
765
  );
746
766
  if (!missing) return;
747
767
  warnedAboutMissingItemId = true;
@@ -754,6 +774,33 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
754
774
  });
755
775
  });
756
776
 
777
+ // Dev-only: the same identity collapse can enter through `value` — partial
778
+ // objects that lack the itemId field (e.g. seeded from a form payload) all
779
+ // compare equal to each other and to nothing in `items`, so selection
780
+ // display and dedup misbehave while `items` itself passes the check above.
781
+ let warnedAboutValueMissingItemId = false;
782
+ $effect(() => {
783
+ if (!DEV) return;
784
+ const currentValue = value;
785
+ const currentItemId = itemId;
786
+ untrack(() => {
787
+ if (warnedAboutValueMissingItemId || currentValue == null) return;
788
+ const entries = Array.isArray(currentValue) ? currentValue : [currentValue];
789
+ // Raw strings are their own id, so only object entries can be missing it
790
+ const missing = (entries as (Item | string)[]).some(
791
+ (entry) => typeof entry !== 'string' && getItemProperty(entry, currentItemId) === undefined,
792
+ );
793
+ if (!missing) return;
794
+ warnedAboutValueMissingItemId = true;
795
+ console.warn(
796
+ `[svelte-select] Some \`value\` entries have no "${currentItemId}" field (the current ` +
797
+ '`itemId`). Value entries are matched against items by that field, so entries ' +
798
+ 'without it cannot be recognized as selected, deduplicated, or validated. Pass ' +
799
+ 'value entries that carry the `itemId` field.',
800
+ );
801
+ });
802
+ });
803
+
757
804
  // Dev-only: warn once the input is mounted if it has no robust accessible
758
805
  // name. The placeholder is only a last-resort fallback that some screen
759
806
  // readers ignore, so an unnamed combobox is a real gap worth surfacing.
@@ -767,7 +814,13 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
767
814
  ariaLabel;
768
815
  untrack(() => {
769
816
  if (!input) return;
770
- const named = !!ariaLabel || !!input.getAttribute('aria-labelledby') || (input.labels?.length ?? 0) > 0;
817
+ // getAttribute over the props: an aria-label supplied through
818
+ // inputAttributes names the input just as well as the ariaLabel prop
819
+ const named =
820
+ !!ariaLabel ||
821
+ !!input.getAttribute('aria-label') ||
822
+ !!input.getAttribute('aria-labelledby') ||
823
+ (input.labels?.length ?? 0) > 0;
771
824
  if (!named) {
772
825
  console.warn(
773
826
  '[svelte-select] The Select input has no accessible name. Pass `ariaLabel`, ' +
@@ -801,6 +854,14 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
801
854
  listboxLabelledby = inputLabelledby;
802
855
  return;
803
856
  }
857
+ // An aria-label supplied through inputAttributes names only the
858
+ // input; forward it so the listbox is named the same way the
859
+ // ariaLabel prop's is (accname precedence: labelledby above wins).
860
+ const inputAriaLabel = input.getAttribute('aria-label');
861
+ if (inputAriaLabel) {
862
+ listboxLabelText = inputAriaLabel;
863
+ return;
864
+ }
804
865
  const labelEl = input.labels?.[0];
805
866
  if (!labelEl) return;
806
867
  // An implicit wrapping <label> contains the component itself, so
@@ -867,13 +928,21 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
867
928
  groupValues.push(groupValue);
868
929
  groups[groupValue] = [];
869
930
  if (groupValue) {
870
- groups[groupValue].push(
871
- Object.assign(createGroupHeaderItem(groupValue, item as Item), {
872
- id: groupValue,
873
- groupHeader: true,
874
- selectable: groupHeaderSelectable,
875
- }),
876
- );
931
+ const header = Object.assign(createGroupHeaderItem(groupValue, item as Item), {
932
+ id: groupValue,
933
+ groupHeader: true,
934
+ selectable: groupHeaderSelectable,
935
+ });
936
+ // Headers are compared by `itemId` like any other row
937
+ // (selection, hover, dedup): one lacking that field
938
+ // compares equal to every other bare header, so selecting
939
+ // one marked them all selected and made the rest
940
+ // unselectable. Stamp the group value when the builder
941
+ // didn't provide the field.
942
+ if (getItemProperty(header, itemId) === undefined) {
943
+ (header as SelectItem)[itemId] = groupValue;
944
+ }
945
+ groups[groupValue].push(header);
877
946
  }
878
947
  }
879
948
 
@@ -1123,7 +1192,7 @@ Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
1123
1192
  The group's accessible name still resolves via aria-labelledby,
1124
1193
  which follows hidden targets. Selectable headers are real options. -->
1125
1194
  <div
1126
- onmouseover={() => hoverManager.handleHover(i)}
1195
+ onmousemove={() => hoverManager.handleHover(i)}
1127
1196
  onfocus={() => hoverManager.handleHover(i)}
1128
1197
  onclick={(ev) => {
1129
1198
  ev.stopPropagation();
@@ -1807,5 +1876,24 @@ input:focus {
1807
1876
  outline: 2px dashed Highlight;
1808
1877
  outline-offset: -2px;
1809
1878
  }
1879
+
1880
+ /* The error state is conveyed only by an author border colour, which is
1881
+ flattened to the standard border — border width + style survive the
1882
+ forced palette, so a double border keeps the state visible. */
1883
+ .svelte-select.error {
1884
+ border: 3px double CanvasText;
1885
+ }
1886
+
1887
+ /* Disabled is aria-disabled + readonly, not :disabled, so the UA never
1888
+ applies its own GrayText treatment — do it explicitly. */
1889
+ .disabled,
1890
+ .disabled input,
1891
+ .disabled input::placeholder {
1892
+ color: GrayText;
1893
+ }
1894
+
1895
+ .disabled {
1896
+ border-color: GrayText;
1897
+ }
1810
1898
  }
1811
1899
  </style>
@@ -1,5 +1,5 @@
1
- import type { ItemLike, SelectProps, SelectRow } from './types';
2
- import type { SelectItem } from './types';
1
+ import type { ItemLike, SelectProps, SelectRow } from './types.js';
2
+ import type { SelectItem } from './types.js';
3
3
  declare function $$render<Item extends ItemLike = SelectItem, Multiple extends boolean = false>(): {
4
4
  props: SelectProps<Item, Multiple>;
5
5
  exports: {
@@ -1,4 +1,4 @@
1
- import type { SelectItem } from './types';
1
+ import type { SelectItem } from './types.js';
2
2
  interface AriaHandlersConfig {
3
3
  ariaValues: (values: string) => string;
4
4
  ariaListOpen: (label: string, count: number) => string;
@@ -1,3 +1,4 @@
1
+ import { isItemSelectableCheck } from './utils.js';
1
2
  export function useAriaHandlers(config) {
2
3
  // Read the config properties at call time so live getters (see Select.svelte) stay reactive
3
4
  function handleAriaSelection(context) {
@@ -34,7 +35,17 @@ export function useAriaHandlers(config) {
34
35
  // screen-reader user can ever reach. Selectable headers
35
36
  // (groupHeaderSelectable) are real options and do count.
36
37
  const count = filteredItems.filter((item) => !(item.groupHeader && !item.selectable)).length;
37
- return config.ariaListOpen(_item[label], count);
38
+ // This text renders before use-hover's keep-hover-selectable
39
+ // correction runs, so on the first open of a grouped list the cursor
40
+ // is still parked on a non-selectable header. Announce the option the
41
+ // correction is about to land on (the first selectable row — the same
42
+ // index checkHoverSelectable computes) instead of a row the cursor can
43
+ // never reach; the region won't recompute after the correction because
44
+ // hoverItemIndex is deliberately untracked (see Select.svelte).
45
+ const announced = isItemSelectableCheck(_item)
46
+ ? _item
47
+ : (filteredItems.find(isItemSelectableCheck) ?? _item);
48
+ return config.ariaListOpen(announced[label], count);
38
49
  }
39
50
  else {
40
51
  return config.ariaFocused();
package/dist/filter.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FilterConfig, ItemLike, SelectItem } from './types';
1
+ import type { FilterConfig, ItemLike, SelectItem } from './types.js';
2
2
  /**
3
3
  * The built-in filtering pipeline: converts raw string items, applies
4
4
  * `itemFilter` against the filter text (skipped when `loadOptions` is set —
package/dist/filter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { areItemsEqual, isStringArray } from './utils';
1
+ import { areItemsEqual, isStringArray } from './utils.js';
2
2
  /**
3
3
  * The built-in filtering pipeline: converts raw string items, applies
4
4
  * `itemFilter` against the filter text (skipped when `loadOptions` is set —
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { default as Select } from './Select.svelte';
2
2
  export { default as ChevronIcon } from './ChevronIcon.svelte';
3
3
  export { default as ClearIcon } from './ClearIcon.svelte';
4
4
  export { default as LoadingIcon } from './LoadingIcon.svelte';
5
- export { default as filter } from './filter';
6
- export { areItemsEqual, isGroupHeader, normalizeItem } from './utils';
7
- export type { ItemLike, SelectItem, SelectGroupHeader, SelectRow, SelectProps, SelectValue, SelectValueProp, SelectClearValue, JustValue, FloatingConfig, FilterConfig, SelectErrorEvent, } from './types';
5
+ export { default as filter } from './filter.js';
6
+ export { areItemsEqual, isGroupHeader, normalizeItem } from './utils.js';
7
+ export type { ItemLike, SelectItem, SelectGroupHeader, SelectRow, SelectProps, SelectValue, SelectValueProp, SelectClearValue, JustValue, FloatingConfig, FilterConfig, SelectErrorEvent, } from './types.js';
package/dist/index.js CHANGED
@@ -5,5 +5,5 @@ export { default as ChevronIcon } from './ChevronIcon.svelte';
5
5
  export { default as ClearIcon } from './ClearIcon.svelte';
6
6
  export { default as LoadingIcon } from './LoadingIcon.svelte';
7
7
  // Utility functions
8
- export { default as filter } from './filter';
9
- export { areItemsEqual, isGroupHeader, normalizeItem } from './utils';
8
+ export { default as filter } from './filter.js';
9
+ export { areItemsEqual, isGroupHeader, normalizeItem } from './utils.js';
@@ -1,4 +1,4 @@
1
- import type { ItemLike, KeyboardNavigationActions, KeyboardNavigationState, SelectItem } from './types';
1
+ import type { ItemLike, KeyboardNavigationActions, KeyboardNavigationState, SelectItem } from './types.js';
2
2
  export declare function useKeyboardNavigation<Item extends ItemLike = SelectItem>(state: KeyboardNavigationState<Item>, actions: KeyboardNavigationActions): {
3
3
  handleKeyDown: (e: KeyboardEvent) => void;
4
4
  handleEscapeKey: (e: KeyboardEvent) => void;
@@ -1,4 +1,4 @@
1
- import { areItemsEqual, getItemProperty, isItemSelectableCheck } from './utils';
1
+ import { areItemsEqual, getItemProperty, isItemSelectableCheck } from './utils.js';
2
2
  export function useKeyboardNavigation(state, actions) {
3
3
  // Handlers claim an event (preventDefault/stopPropagation) only on branches
4
4
  // that actually act on it. Unclaimed keys keep bubbling so ancestors still
@@ -11,6 +11,14 @@ export function useKeyboardNavigation(state, actions) {
11
11
  // disabled control must never mutate its value from the keyboard.
12
12
  if (!state.focused || state.disabled)
13
13
  return;
14
+ // While an IME composition is active every key belongs to the IME:
15
+ // Enter commits the conversion, arrows move through the candidate
16
+ // window, Escape cancels it. Acting on those presses would select the
17
+ // hovered option and wipe the half-composed filter text. keyCode 229
18
+ // catches the WebKit/legacy path where the composition keydown
19
+ // reports isComposing === false.
20
+ if (e.isComposing || e.keyCode === 229)
21
+ return;
14
22
  const handlers = {
15
23
  Escape: handleEscapeKey,
16
24
  Enter: handleEnterKey,
@@ -1,3 +1,9 @@
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
+
1
7
  <svg width="100%" height="100%" viewBox="0 0 20 20" focusable="false" aria-hidden="true">
2
8
  <path
3
9
  fill="currentColor"
@@ -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,3 +1,9 @@
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
+
1
7
  <svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation">
2
8
  <path
3
9
  fill="currentColor"
@@ -1,18 +1,5 @@
1
- export default ClearIcon;
2
- type ClearIcon = SvelteComponent<{
3
- [x: string]: never;
4
- }, {
5
- [evt: string]: CustomEvent<any>;
6
- }, {}> & {
7
- $$bindings?: string | undefined;
8
- };
9
- declare const ClearIcon: $$__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 ClearIcon: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type ClearIcon = InstanceType<typeof ClearIcon>;
18
+ export default ClearIcon;