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
|
@@ -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;
|
|
@@ -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();
|
|
@@ -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 { ItemLike, SelectItem, SelectState } from './types';
|
|
1
|
+
import type { ItemLike, SelectItem, SelectState } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The prop-backed part of the store: Select.svelte supplies live getter/setter
|
|
4
4
|
* accessors over its `$props()` bindings and derived values, so reads stay
|
package/dist/styles/default.css
CHANGED
|
@@ -425,4 +425,23 @@ input:focus {
|
|
|
425
425
|
outline: 2px dashed Highlight;
|
|
426
426
|
outline-offset: -2px;
|
|
427
427
|
}
|
|
428
|
+
|
|
429
|
+
/* The error state is conveyed only by an author border colour, which is
|
|
430
|
+
flattened to the standard border — border width + style survive the
|
|
431
|
+
forced palette, so a double border keeps the state visible. */
|
|
432
|
+
.svelte-select.error {
|
|
433
|
+
border: 3px double CanvasText;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* Disabled is aria-disabled + readonly, not :disabled, so the UA never
|
|
437
|
+
applies its own GrayText treatment — do it explicitly. */
|
|
438
|
+
.disabled,
|
|
439
|
+
.disabled input,
|
|
440
|
+
.disabled input::placeholder {
|
|
441
|
+
color: GrayText;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.disabled {
|
|
445
|
+
border-color: GrayText;
|
|
446
|
+
}
|
|
428
447
|
}
|
package/dist/tailwind.css
CHANGED
|
@@ -226,6 +226,25 @@
|
|
|
226
226
|
outline: 2px dashed Highlight;
|
|
227
227
|
outline-offset: -2px;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
/* The error state is conveyed only by an author border colour, which is
|
|
231
|
+
flattened to the standard border — border width + style survive the
|
|
232
|
+
forced palette, so a double border keeps the state visible. */
|
|
233
|
+
.svelte-select.error {
|
|
234
|
+
border: 3px double CanvasText;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Disabled is aria-disabled + readonly, not :disabled, so the UA never
|
|
238
|
+
applies its own GrayText treatment — do it explicitly. */
|
|
239
|
+
.svelte-select.disabled,
|
|
240
|
+
.svelte-select.disabled input,
|
|
241
|
+
.svelte-select.disabled input::placeholder {
|
|
242
|
+
color: GrayText;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.svelte-select.disabled {
|
|
246
|
+
border-color: GrayText;
|
|
247
|
+
}
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
.list-item {
|
package/dist/types.d.ts
CHANGED
|
@@ -345,7 +345,7 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
345
345
|
debounceWait?: number;
|
|
346
346
|
/** Passed to floating-ui to position the list (placement, middleware, autoUpdate). */
|
|
347
347
|
floatingConfig?: FloatingConfig;
|
|
348
|
-
/** Bindable. Index of the option under the keyboard cursor. */
|
|
348
|
+
/** Bindable. Index of the option under the keyboard cursor. @default 0 */
|
|
349
349
|
hoverItemIndex?: number;
|
|
350
350
|
/**
|
|
351
351
|
* Extra attributes for the text input. Merged over the component's own ARIA
|
|
@@ -353,6 +353,9 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
353
353
|
* `oninput`/`onblur`/`onfocus`/`onkeydown` here composes with (runs after)
|
|
354
354
|
* the component's own handler instead of replacing it, so filtering, focus
|
|
355
355
|
* handling, and keyboard navigation keep working alongside yours.
|
|
356
|
+
* `value`, `placeholder`, and `style` are ignored — the input's value IS the
|
|
357
|
+
* filter text; use `bind:filterText`, the `placeholder` prop, and
|
|
358
|
+
* `inputStyles` instead.
|
|
356
359
|
*/
|
|
357
360
|
inputAttributes?: HTMLInputAttributes;
|
|
358
361
|
/** Size the list to the container's width. @default true */
|
|
@@ -396,16 +399,26 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
396
399
|
/**
|
|
397
400
|
* Fetches options asynchronously. Runs on mount, on typing (debounced by
|
|
398
401
|
* `debounceWait`), and whenever `loadOptionsDeps` or `disabled` changes.
|
|
399
|
-
* Opening or closing the list never fetches,
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
+
* Opening or closing the list never fetches by itself; beyond that, one rule
|
|
403
|
+
* holds: the open list never shows results stale for the visible text.
|
|
404
|
+
* Reopening refetches when the shown results don't match the retained filter
|
|
405
|
+
* text (`clearFilterTextOnBlur={false}`), when the text was wiped at close
|
|
406
|
+
* (Escape) over query-narrowed results, or after a failed load; emptying the
|
|
407
|
+
* text while the list stays open (deleting the query, or a selection with
|
|
408
|
+
* `closeListOnChange={false}`) refetches the baseline set in place.
|
|
402
409
|
* Whatever it returns is what the list shows: results are not re-filtered by
|
|
403
410
|
* `filterText`. Set `items` or this, not both.
|
|
404
411
|
*/
|
|
405
412
|
loadOptions?: (filterText: string) => Promise<Item[] | string[]>;
|
|
413
|
+
/**
|
|
414
|
+
* Announced when the arrow-key tag cursor lands on a multi-select tag;
|
|
415
|
+
* receives that tag's label. The default explains the Backspace-to-remove
|
|
416
|
+
* and arrow-key mechanics — override it to localize them.
|
|
417
|
+
*/
|
|
418
|
+
ariaActiveTag?: (label: string) => string;
|
|
406
419
|
/** Announced when the selection is cleared. */
|
|
407
420
|
ariaCleared?: () => string;
|
|
408
|
-
/** `aria-label` for the clear-all button. */
|
|
421
|
+
/** `aria-label` for the clear-all button. @default 'Clear selection' */
|
|
409
422
|
ariaClearSelectLabel?: string;
|
|
410
423
|
/** 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
424
|
ariaEmpty?: () => string;
|
|
@@ -427,7 +440,7 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
427
440
|
ariaListOpen?: (label: string, count: number) => string;
|
|
428
441
|
/** 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
442
|
ariaLoading?: () => string;
|
|
430
|
-
/** `aria-label` for each multi-select tag's remove button; receives the item's label. */
|
|
443
|
+
/** `aria-label` for each multi-select tag's remove button; receives the item's label. @default (label) => `Remove ${label}` */
|
|
431
444
|
ariaRemoveItemLabel?: (label: string) => string;
|
|
432
445
|
/** Announced when the selection changes; receives the selected labels, comma-joined. */
|
|
433
446
|
ariaValues?: (values: string) => string;
|
|
@@ -447,10 +460,16 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
447
460
|
* Fires with the rendered list whenever it changes while open. Rows include any
|
|
448
461
|
* synthesized group headers — narrow with `isGroupHeader`.
|
|
449
462
|
*/
|
|
450
|
-
onfilter?: (items: SelectRow<Item
|
|
463
|
+
onfilter?: (items: SelectRow<NoInfer<Item>>[]) => void;
|
|
451
464
|
/** Literal DOM `focus` passthrough from the text input. Fires after the component's own focus handling. */
|
|
452
465
|
onfocus?: (e: FocusEvent) => void;
|
|
453
|
-
/**
|
|
466
|
+
/**
|
|
467
|
+
* Fires with the index of the option under the keyboard/mouse cursor.
|
|
468
|
+
* Naming note: by the camelCase-for-state-changes rule above this would be
|
|
469
|
+
* `onHoverItem` (it tracks the bindable `hoverItemIndex`), but it shipped
|
|
470
|
+
* lowercase in 2.0 and renaming is a breaking change — treat it as the one
|
|
471
|
+
* grandfathered exception, not a pattern to extend.
|
|
472
|
+
*/
|
|
454
473
|
onhoveritem?: (index: number) => void;
|
|
455
474
|
/**
|
|
456
475
|
* Fires with the options a `loadOptions` call resolved. When the loader
|
|
@@ -464,7 +483,7 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
464
483
|
* Fires alongside {@link SelectProps.onSelectionChange} when the user picks an
|
|
465
484
|
* option, but receives just the item selected rather than the whole value.
|
|
466
485
|
*/
|
|
467
|
-
onselect?: (selection: Item) => void;
|
|
486
|
+
onselect?: (selection: NoInfer<Item>) => void;
|
|
468
487
|
/**
|
|
469
488
|
* Fires only when the user picks an option from the list — never on a clear, a
|
|
470
489
|
* programmatic `bind:value` write, or a `loadOptionsDeps` invalidation. Use
|
|
@@ -483,6 +502,12 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
483
502
|
* an `oninput` that never fired per keystroke read as the DOM event it wasn't.)
|
|
484
503
|
*/
|
|
485
504
|
onValueChange?: (value: SelectValue<Item, Multiple>) => void;
|
|
505
|
+
/** @deprecated Removed in v2.0 — renamed to {@link SelectProps.onValueChange} (same payload and firing rules). */
|
|
506
|
+
oninput?: never;
|
|
507
|
+
/** @deprecated Removed in v2.0 — renamed to {@link SelectProps.onSelectionChange} (same payload and firing rules). */
|
|
508
|
+
onchange?: never;
|
|
509
|
+
/** @deprecated Removed in v2.0 — renamed to {@link SelectProps.listStyles}. */
|
|
510
|
+
listStyle?: never;
|
|
486
511
|
/** Replaces the chevron icon; the boolean is `listOpen`, so the icon can flip while open. Rendered only with `showChevron`. */
|
|
487
512
|
chevronIconSnippet?: Snippet<[boolean]>;
|
|
488
513
|
/** Replaces the icon inside the clear-all button (the button itself, its `ariaClearSelectLabel`, and its behavior stay). */
|
|
@@ -495,10 +520,11 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
495
520
|
* `<input type="hidden" name={...}>` (the default submits JSON-stringified
|
|
496
521
|
* items, or bare ids with `useJustValue`).
|
|
497
522
|
*/
|
|
498
|
-
inputHiddenSnippet?: Snippet<[SelectValueProp<Item, Multiple>
|
|
523
|
+
inputHiddenSnippet?: Snippet<[SelectValueProp<Item, Multiple>]>;
|
|
499
524
|
/**
|
|
500
525
|
* Renders one row of the list. Also renders synthesized group headers when
|
|
501
|
-
* `groupBy` is set — narrow with `isGroupHeader`.
|
|
526
|
+
* `groupBy` is set — narrow with `isGroupHeader`. The number is the row's
|
|
527
|
+
* index in the rendered (filtered, grouped) list.
|
|
502
528
|
*/
|
|
503
529
|
itemSnippet?: Snippet<[SelectRow<Item>, number]>;
|
|
504
530
|
/** Rendered inside the list after the options — e.g. a "load more" row or footer. */
|
|
@@ -525,8 +551,11 @@ export interface SelectProps<Item extends ItemLike = SelectItem, Multiple extend
|
|
|
525
551
|
* submission while `required` and empty; receives the current value. An
|
|
526
552
|
* override owns the constraint-validation behavior.
|
|
527
553
|
*/
|
|
528
|
-
requiredSnippet?: Snippet<[SelectValueProp<Item, Multiple>
|
|
529
|
-
/**
|
|
554
|
+
requiredSnippet?: Snippet<[SelectValueProp<Item, Multiple>]>;
|
|
555
|
+
/**
|
|
556
|
+
* Receives one item at a time: the value in single mode, each tag in multiple
|
|
557
|
+
* mode. The number is the tag's index in multiple mode and absent in single mode.
|
|
558
|
+
*/
|
|
530
559
|
selectionSnippet?: Snippet<[NoInfer<Item>, number?]>;
|
|
531
560
|
/** Bindable, read-only: the container element, once mounted. */
|
|
532
561
|
container?: HTMLDivElement;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ItemLike, SelectItem, SelectState } from './types';
|
|
1
|
+
import type { ItemLike, SelectItem, SelectState } from './types.js';
|
|
2
2
|
export declare function useHover<Item extends ItemLike = SelectItem>(state: SelectState<Item>): {
|
|
3
3
|
setHoverIndex: (increment: number) => void;
|
|
4
4
|
isItemActive: (item: SelectItem) => boolean;
|
package/dist/use-hover.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
|
-
import { areItemsEqual, getItemProperty, isItemSelectableCheck, normalizeItem } from './utils';
|
|
2
|
+
import { areItemsEqual, getItemProperty, isItemSelectableCheck, normalizeItem } from './utils.js';
|
|
3
3
|
export function useHover(state) {
|
|
4
4
|
function computeNextIndex(filteredItems, fromIndex, increment) {
|
|
5
5
|
// Same predicate as click/Enter selection: arrow keys must be able to
|
|
@@ -82,10 +82,13 @@ export function useHover(state) {
|
|
|
82
82
|
if (state.isScrolling)
|
|
83
83
|
return;
|
|
84
84
|
state.hoverItemIndex = i;
|
|
85
|
-
//
|
|
86
|
-
// when the list renders
|
|
87
|
-
//
|
|
88
|
-
//
|
|
85
|
+
// Driven by row mousemove (never mouseover): browsers synthesize
|
|
86
|
+
// mouseover when the list renders — or scrolls rows — under a
|
|
87
|
+
// stationary cursor, which is zero user action and used to yank both
|
|
88
|
+
// hover and aria-activedescendant off the keyboard cursor right after
|
|
89
|
+
// open. mousemove only fires for real pointer movement. Deliberately
|
|
90
|
+
// NOT commit-intent for Tab either; that is the list-level mousemove
|
|
91
|
+
// handler in Select.svelte.
|
|
89
92
|
}
|
|
90
93
|
let scrollEndFallback;
|
|
91
94
|
function handleListScroll() {
|
|
@@ -133,6 +136,22 @@ export function useHover(state) {
|
|
|
133
136
|
}
|
|
134
137
|
});
|
|
135
138
|
});
|
|
139
|
+
// A parent can write any number through bind:hoverItemIndex; internal
|
|
140
|
+
// writers are all range-safe. An out-of-range cursor silently kills
|
|
141
|
+
// Enter/Space/Tab (they read filteredItems[hoverItemIndex] → undefined)
|
|
142
|
+
// until the next arrow key, so correct it as soon as it lands. Only
|
|
143
|
+
// genuinely out-of-range values are touched: a pointer resting on a
|
|
144
|
+
// non-selectable row is a valid cursor position, which is why the
|
|
145
|
+
// keep-hover effect above reads the index untracked and must stay that way.
|
|
146
|
+
$effect(() => {
|
|
147
|
+
const idx = state.hoverItemIndex;
|
|
148
|
+
const inRange = idx >= 0 && idx < state.filteredItems.length;
|
|
149
|
+
untrack(() => {
|
|
150
|
+
if (!state.listOpen || state.filteredItems.length === 0 || inRange)
|
|
151
|
+
return;
|
|
152
|
+
checkHoverSelectable();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
136
155
|
// Reset hover to the first selectable item on filterText change
|
|
137
156
|
$effect(() => {
|
|
138
157
|
if (state.filterText) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ItemLike, LoadOptionsActions, SelectItem, SelectState } from './types';
|
|
1
|
+
import type { ItemLike, LoadOptionsActions, SelectItem, SelectState } from './types.js';
|
|
2
2
|
export interface HandleLoadOptionsOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Validate the selection against the loaded items (dependency-driven reloads):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
2
|
import { DEV } from 'esm-env';
|
|
3
|
-
import { convertStringItemsToObjects, getItemProperty } from './utils';
|
|
3
|
+
import { convertStringItemsToObjects, getItemProperty } from './utils.js';
|
|
4
4
|
export function useLoadOptions(state, actions) {
|
|
5
5
|
// Monotonic token so responses that resolve after a newer request started are discarded
|
|
6
6
|
let requestSequence = 0;
|
|
@@ -182,6 +182,13 @@ export function useLoadOptions(state, actions) {
|
|
|
182
182
|
// A restored load has settled; the channel is spent
|
|
183
183
|
if (token === restoredToken)
|
|
184
184
|
restoredToken = 0;
|
|
185
|
+
// The newest load has settled: nothing filter-driven is
|
|
186
|
+
// pending anymore, so a later close must not "cancel" it —
|
|
187
|
+
// pre-fix the stale flag let cancelPendingFilterLoad hand
|
|
188
|
+
// currency back to a deps reload this load superseded,
|
|
189
|
+
// whose late response then overwrote these fresher items
|
|
190
|
+
if (token === requestSequence)
|
|
191
|
+
latestLoadIsFilterDriven = false;
|
|
185
192
|
if (result && result.length > 0 && typeof result[0] === 'string') {
|
|
186
193
|
state.items = convertStringItemsToObjects(result);
|
|
187
194
|
}
|
|
@@ -207,6 +214,10 @@ export function useLoadOptions(state, actions) {
|
|
|
207
214
|
return; // superseded; the newer request manages state
|
|
208
215
|
if (token === restoredToken)
|
|
209
216
|
restoredToken = 0;
|
|
217
|
+
// Settled (with an error) — same as the success path: the flag
|
|
218
|
+
// must not outlive the load it describes
|
|
219
|
+
if (token === requestSequence)
|
|
220
|
+
latestLoadIsFilterDriven = false;
|
|
210
221
|
// Settled (with an error), so no longer live — a reopen may retry it
|
|
211
222
|
liveLoadFilterText = undefined;
|
|
212
223
|
console.error('loadOptions error:', err);
|
|
@@ -320,7 +331,25 @@ export function useLoadOptions(state, actions) {
|
|
|
320
331
|
!disabled &&
|
|
321
332
|
filterText !== loadedFilterText &&
|
|
322
333
|
!loadIsLiveForText;
|
|
323
|
-
|
|
334
|
+
// Filter text emptied in place while the list stays open — a selection
|
|
335
|
+
// wiped it (closeListOnChange={false}) or the user deleted the query —
|
|
336
|
+
// over results still narrowed by the old text. The same staleness rule
|
|
337
|
+
// as reopenedStale applies (results must reflect the current text; the
|
|
338
|
+
// '' re-fetch restores the baseline set), it just never crosses a
|
|
339
|
+
// close/open edge here. loadedFilterText distinguishes this from an
|
|
340
|
+
// armed-but-never-landed load, which the cancel branch below moots.
|
|
341
|
+
const emptiedWhileOpen = !isFirstRun &&
|
|
342
|
+
filterTextChanged &&
|
|
343
|
+
filterText.length === 0 &&
|
|
344
|
+
listOpen &&
|
|
345
|
+
prev.listOpen &&
|
|
346
|
+
!disabled &&
|
|
347
|
+
filterText !== loadedFilterText;
|
|
348
|
+
if (isFirstRun ||
|
|
349
|
+
depsChanged ||
|
|
350
|
+
disabledChanged ||
|
|
351
|
+
(filterTextChanged && filterText.length > 0) ||
|
|
352
|
+
emptiedWhileOpen) {
|
|
324
353
|
untrack(() => handleLoadOptions(filterText, {
|
|
325
354
|
validateValue: depsChanged,
|
|
326
355
|
debounce: filterTextChanged && !depsChanged && !disabledChanged,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ItemLike, SelectItem, SelectState, ValueActions } from './types';
|
|
1
|
+
import type { ItemLike, SelectItem, SelectState, ValueActions } from './types.js';
|
|
2
2
|
export declare function useValue<Item extends ItemLike = SelectItem>(state: SelectState<Item>, actions: ValueActions): {
|
|
3
3
|
itemSelected: (selection: SelectItem) => void;
|
|
4
4
|
handleMultiItemClear: (i: number) => Promise<void>;
|
package/dist/use-value.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
|
-
import { getItemProperty, hasValueChanged } from './utils';
|
|
2
|
+
import { getItemProperty, hasValueChanged } from './utils.js';
|
|
3
3
|
export function useValue(state, actions) {
|
|
4
4
|
// Tracked-read helper for the effects' trigger sections: reading length AND
|
|
5
5
|
// every entry makes any in-place mutation of a bound array retrigger the
|
|
@@ -188,6 +188,12 @@ export function useValue(state, actions) {
|
|
|
188
188
|
// Raw string entries key on the string itself: getItemProperty returns
|
|
189
189
|
// undefined for non-objects, which would collapse all strings into one
|
|
190
190
|
const id = typeof val === 'string' ? val : getItemProperty(val, itemId);
|
|
191
|
+
// An entry with no itemId field yields no evidence of duplication —
|
|
192
|
+
// collapsing every such entry into the first silently loses data on
|
|
193
|
+
// a config error (the dev warning in Select.svelte surfaces it).
|
|
194
|
+
// Only matching real ids may dedup.
|
|
195
|
+
if (id === undefined)
|
|
196
|
+
return true;
|
|
191
197
|
if (seen.has(id))
|
|
192
198
|
return false;
|
|
193
199
|
seen.add(id);
|
|
@@ -307,6 +313,12 @@ export function useValue(state, actions) {
|
|
|
307
313
|
actions.onSelectionChange(state.value);
|
|
308
314
|
actions.onselect(selection);
|
|
309
315
|
}
|
|
316
|
+
// Run once synchronously at creation: effects never run during SSR, so
|
|
317
|
+
// without this pass the server HTML ships raw string entries — empty chip
|
|
318
|
+
// text, aria-label="Remove undefined", and a hidden form input carrying
|
|
319
|
+
// JSON.stringify('NY'). The effect below re-runs the same idempotent
|
|
320
|
+
// resolution on the client.
|
|
321
|
+
untrack(() => normalizeValue());
|
|
310
322
|
// Normalize string values on every value change (not just the hasValue
|
|
311
323
|
// flip — replacing one string with another must re-resolve), and again when
|
|
312
324
|
// async items arrive so fallback entries upgrade to the real item
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ItemLike, SelectGroupHeader, SelectItem } from './types';
|
|
1
|
+
import type { ItemLike, SelectGroupHeader, SelectItem } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Narrows a rendered list row to a synthesized group header (see the `groupBy` prop).
|
|
4
4
|
* Rows that fail this guard are your own item type.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-5-select",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -63,7 +63,11 @@
|
|
|
63
63
|
"select",
|
|
64
64
|
"dropdown",
|
|
65
65
|
"component",
|
|
66
|
-
"select-component"
|
|
66
|
+
"select-component",
|
|
67
|
+
"combobox",
|
|
68
|
+
"autocomplete",
|
|
69
|
+
"typeahead",
|
|
70
|
+
"multiselect"
|
|
67
71
|
],
|
|
68
72
|
"type": "module",
|
|
69
73
|
"exports": {
|