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.
- package/README.md +21 -11
- package/dist/ChevronIcon.svelte +6 -0
- package/dist/ChevronIcon.svelte.d.ts +6 -14
- package/dist/ClearIcon.svelte +6 -0
- package/dist/ClearIcon.svelte.d.ts +6 -14
- package/dist/LoadingIcon.svelte +6 -0
- package/dist/LoadingIcon.svelte.d.ts +6 -14
- package/dist/Select.svelte +104 -16
- package/dist/Select.svelte.d.ts +2 -2
- package/dist/aria-handlers.svelte.d.ts +1 -1
- package/dist/aria-handlers.svelte.js +12 -1
- package/dist/filter.d.ts +1 -1
- package/dist/filter.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/keyboard-navigation.svelte.d.ts +1 -1
- package/dist/keyboard-navigation.svelte.js +9 -1
- package/dist/no-styles/ChevronIcon.svelte +6 -0
- package/dist/no-styles/ChevronIcon.svelte.d.ts +6 -14
- package/dist/no-styles/ClearIcon.svelte +6 -0
- package/dist/no-styles/ClearIcon.svelte.d.ts +6 -14
- package/dist/no-styles/LoadingIcon.svelte +6 -0
- package/dist/no-styles/LoadingIcon.svelte.d.ts +6 -14
- package/dist/no-styles/Select.svelte +85 -16
- package/dist/no-styles/Select.svelte.d.ts +2 -2
- package/dist/select-state.svelte.d.ts +1 -1
- package/dist/styles/default.css +19 -0
- package/dist/tailwind.css +19 -0
- package/dist/types.d.ts +42 -13
- package/dist/use-hover.svelte.d.ts +1 -1
- package/dist/use-hover.svelte.js +24 -5
- package/dist/use-load-options.svelte.d.ts +1 -1
- package/dist/use-load-options.svelte.js +31 -2
- package/dist/use-value.svelte.d.ts +1 -1
- package/dist/use-value.svelte.js +13 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
<div
|
|
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
|
|
5
|
+
<div align="center">
|
|
6
6
|
<a href="https://npmjs.org/package/svelte-5-select">
|
|
7
|
-
<img src="https://badgen.
|
|
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.
|
|
10
|
+
<img src="https://badgen.net/npm/dm/svelte-5-select" alt="downloads" />
|
|
11
11
|
</a>
|
|
12
12
|
</div>
|
|
13
|
-
<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.
|
|
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
|
|
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 `
|
|
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`
|
|
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=
|
|
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.
|
package/dist/ChevronIcon.svelte
CHANGED
|
@@ -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(
|
|
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;
|
package/dist/ClearIcon.svelte
CHANGED
|
@@ -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(
|
|
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;
|
package/dist/LoadingIcon.svelte
CHANGED
|
@@ -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(
|
|
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;
|
package/dist/Select.svelte
CHANGED
|
@@ -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
|
|
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
|
-
|
|
744
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
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>
|
package/dist/Select.svelte.d.ts
CHANGED
|
@@ -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,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
|
-
|
|
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(
|
|
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(
|
|
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;
|