svelte-5-select 1.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -115
- package/dist/ChevronIcon.svelte +14 -13
- package/dist/ChevronIcon.svelte.d.ts +6 -14
- package/dist/ClearIcon.svelte +9 -11
- package/dist/ClearIcon.svelte.d.ts +6 -14
- package/dist/LoadingIcon.svelte +15 -2
- package/dist/LoadingIcon.svelte.d.ts +6 -14
- package/dist/Select.svelte +1048 -442
- package/dist/Select.svelte.d.ts +37 -5
- package/dist/aria-handlers.svelte.d.ts +5 -2
- package/dist/aria-handlers.svelte.js +30 -8
- package/dist/filter.d.ts +10 -2
- package/dist/filter.js +15 -8
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -3
- package/dist/keyboard-navigation.svelte.d.ts +7 -2
- package/dist/keyboard-navigation.svelte.js +293 -47
- package/dist/no-styles/ChevronIcon.svelte +17 -0
- package/dist/no-styles/ChevronIcon.svelte.d.ts +18 -0
- package/dist/no-styles/ClearIcon.svelte +14 -0
- package/dist/no-styles/ClearIcon.svelte.d.ts +18 -0
- package/dist/no-styles/LoadingIcon.svelte +19 -0
- package/dist/no-styles/LoadingIcon.svelte.d.ts +18 -0
- package/dist/no-styles/Select.svelte +1452 -0
- package/dist/no-styles/Select.svelte.d.ts +38 -0
- package/dist/select-state.svelte.d.ts +15 -0
- package/dist/select-state.svelte.js +161 -0
- package/dist/styles/default.css +131 -71
- package/dist/tailwind.css +139 -17
- package/dist/types.d.ts +488 -129
- package/dist/use-hover.svelte.d.ts +3 -7
- package/dist/use-hover.svelte.js +110 -36
- package/dist/use-load-options.svelte.d.ts +18 -3
- package/dist/use-load-options.svelte.js +362 -42
- package/dist/use-value.svelte.d.ts +2 -11
- package/dist/use-value.svelte.js +334 -111
- package/dist/utils.d.ts +19 -8
- package/dist/utils.js +52 -8
- package/package.json +121 -94
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function useHover(
|
|
3
|
-
computeNextIndex: (filteredItems: SelectItem[], fromIndex: number, increment: number) => number;
|
|
1
|
+
import type { ItemLike, SelectItem, SelectState } from './types.js';
|
|
2
|
+
export declare function useHover<Item extends ItemLike = SelectItem>(state: SelectState<Item>): {
|
|
4
3
|
setHoverIndex: (increment: number) => void;
|
|
5
|
-
|
|
6
|
-
checkHoverSelectable: (startingIndex?: number, ignoreGroup?: boolean) => void;
|
|
7
|
-
getFirstSelectableIndex: () => number;
|
|
8
|
-
isItemActive: (item: SelectItem, val: SelectItem | SelectItem[] | null | undefined, itemId: string) => boolean | undefined;
|
|
4
|
+
isItemActive: (item: SelectItem) => boolean;
|
|
9
5
|
isItemSelectable: (item: SelectItem) => boolean;
|
|
10
6
|
handleHover: (i: number) => void;
|
|
11
7
|
handleListScroll: () => void;
|
package/dist/use-hover.svelte.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
import { areItemsEqual, getItemProperty, isItemSelectableCheck, normalizeItem } from './utils.js';
|
|
3
|
+
export function useHover(state) {
|
|
3
4
|
function computeNextIndex(filteredItems, fromIndex, increment) {
|
|
4
|
-
|
|
5
|
+
// Same predicate as click/Enter selection: arrow keys must be able to
|
|
6
|
+
// reach every item the user can otherwise select
|
|
7
|
+
const selectableFilteredItems = filteredItems.filter((item) => isItemSelectableCheck(item));
|
|
5
8
|
if (selectableFilteredItems.length === 0) {
|
|
6
9
|
return 0;
|
|
7
10
|
}
|
|
@@ -9,7 +12,7 @@ export function useHover(context) {
|
|
|
9
12
|
const isCurrentSelectable = isItemSelectableCheck(currentItem);
|
|
10
13
|
let currentSelectableIndex;
|
|
11
14
|
if (isCurrentSelectable) {
|
|
12
|
-
currentSelectableIndex = selectableFilteredItems.findIndex(item => item === currentItem);
|
|
15
|
+
currentSelectableIndex = selectableFilteredItems.findIndex((item) => item === currentItem);
|
|
13
16
|
}
|
|
14
17
|
else {
|
|
15
18
|
currentSelectableIndex = increment > 0 ? -1 : selectableFilteredItems.length;
|
|
@@ -19,40 +22,40 @@ export function useHover(context) {
|
|
|
19
22
|
newSelectableIndex = (currentSelectableIndex + 1) % selectableFilteredItems.length;
|
|
20
23
|
}
|
|
21
24
|
else {
|
|
22
|
-
newSelectableIndex =
|
|
25
|
+
newSelectableIndex =
|
|
26
|
+
(currentSelectableIndex - 1 + selectableFilteredItems.length) % selectableFilteredItems.length;
|
|
23
27
|
}
|
|
24
28
|
const newItem = selectableFilteredItems[newSelectableIndex];
|
|
25
|
-
return filteredItems.findIndex(item => item === newItem);
|
|
29
|
+
return filteredItems.findIndex((item) => item === newItem);
|
|
26
30
|
}
|
|
27
31
|
function setHoverIndex(increment) {
|
|
28
|
-
const { filteredItems, hoverItemIndex } =
|
|
29
|
-
|
|
32
|
+
const { filteredItems, hoverItemIndex } = state;
|
|
33
|
+
state.hoverItemIndex = computeNextIndex(filteredItems, hoverItemIndex, increment);
|
|
30
34
|
}
|
|
31
35
|
function setValueIndexAsHoverIndex() {
|
|
32
|
-
const {
|
|
33
|
-
const normalizedValue = !value ? null : typeof value === 'string' ? { value, label: value } : value;
|
|
36
|
+
const { normalizedValue, filteredItems, itemId } = state;
|
|
34
37
|
if (!normalizedValue || Array.isArray(normalizedValue))
|
|
35
38
|
return;
|
|
36
|
-
const
|
|
37
|
-
const valueIndex = filteredItems.findIndex((i) => {
|
|
38
|
-
return i[itemId] === singleValue[itemId];
|
|
39
|
-
});
|
|
39
|
+
const valueIndex = filteredItems.findIndex((item) => getItemProperty(item, itemId) === getItemProperty(normalizedValue, itemId));
|
|
40
40
|
checkHoverSelectable(valueIndex, true);
|
|
41
41
|
}
|
|
42
|
-
function checkHoverSelectable(startingIndex = 0,
|
|
43
|
-
const {
|
|
42
|
+
function checkHoverSelectable(startingIndex = 0, trustIndex) {
|
|
43
|
+
const { filteredItems } = state;
|
|
44
44
|
const idx = startingIndex < 0 ? 0 : startingIndex;
|
|
45
|
-
|
|
45
|
+
// Same predicate as click/Enter selection (a missing `selectable` key
|
|
46
|
+
// means selectable), and not gated on groupBy: a plain list can start
|
|
47
|
+
// with a non-selectable item too
|
|
48
|
+
if (!trustIndex && filteredItems[idx] && !isItemSelectableCheck(filteredItems[idx])) {
|
|
46
49
|
// Compute the final index without intermediate state writes
|
|
47
|
-
|
|
50
|
+
state.hoverItemIndex = computeNextIndex(filteredItems, idx, 1);
|
|
48
51
|
}
|
|
49
52
|
else {
|
|
50
|
-
|
|
53
|
+
state.hoverItemIndex = idx;
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
function getFirstSelectableIndex() {
|
|
54
|
-
const {
|
|
55
|
-
if (
|
|
57
|
+
const { filteredItems } = state;
|
|
58
|
+
if (filteredItems.length === 0)
|
|
56
59
|
return 0;
|
|
57
60
|
if (!isItemSelectableCheck(filteredItems[0])) {
|
|
58
61
|
const firstSelectable = filteredItems.findIndex(isItemSelectableCheck);
|
|
@@ -60,34 +63,105 @@ export function useHover(context) {
|
|
|
60
63
|
}
|
|
61
64
|
return 0;
|
|
62
65
|
}
|
|
63
|
-
function isItemActive(item
|
|
64
|
-
const { multiple } =
|
|
65
|
-
if (multiple)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
function isItemActive(item) {
|
|
67
|
+
const { multiple, normalizedValue, itemId } = state;
|
|
68
|
+
if (multiple) {
|
|
69
|
+
if (!Array.isArray(normalizedValue))
|
|
70
|
+
return false;
|
|
71
|
+
// Array entries may still be raw strings; normalize each before comparing
|
|
72
|
+
return normalizedValue.some((v) => areItemsEqual(normalizeItem(v), item, itemId));
|
|
73
|
+
}
|
|
74
|
+
if (Array.isArray(normalizedValue))
|
|
75
|
+
return false;
|
|
76
|
+
return areItemsEqual(normalizedValue, item, itemId);
|
|
69
77
|
}
|
|
70
78
|
function isItemSelectable(item) {
|
|
71
79
|
return (item.groupHeader && item.selectable) || isItemSelectableCheck(item);
|
|
72
80
|
}
|
|
73
81
|
function handleHover(i) {
|
|
74
|
-
|
|
75
|
-
if (isScrolling)
|
|
82
|
+
if (state.isScrolling)
|
|
76
83
|
return;
|
|
77
|
-
|
|
84
|
+
state.hoverItemIndex = i;
|
|
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.
|
|
78
92
|
}
|
|
93
|
+
let scrollEndFallback;
|
|
79
94
|
function handleListScroll() {
|
|
80
|
-
|
|
95
|
+
state.isScrolling = true;
|
|
96
|
+
// scrollend never fires on some browsers (e.g. Safari < 18.2); without a
|
|
97
|
+
// fallback a single scroll would wedge isScrolling and kill hover + blur
|
|
98
|
+
clearTimeout(scrollEndFallback);
|
|
99
|
+
scrollEndFallback = setTimeout(() => {
|
|
100
|
+
state.isScrolling = false;
|
|
101
|
+
}, 150);
|
|
81
102
|
}
|
|
82
103
|
function handleListScrollEnd() {
|
|
83
|
-
|
|
104
|
+
clearTimeout(scrollEndFallback);
|
|
105
|
+
state.isScrolling = false;
|
|
84
106
|
}
|
|
107
|
+
// Teardown-only effect (not onDestroy: composables are also created inside
|
|
108
|
+
// $effect.root in tests, where onDestroy has no component context)
|
|
109
|
+
$effect(() => {
|
|
110
|
+
return () => clearTimeout(scrollEndFallback);
|
|
111
|
+
});
|
|
112
|
+
// Set value index as hover when list opens
|
|
113
|
+
$effect(() => {
|
|
114
|
+
const shouldSnap = !state.multiple && state.listOpen && !!state.value && !!state.filteredItems;
|
|
115
|
+
untrack(() => {
|
|
116
|
+
// One-shot: a type-ahead keypress that opened the list has already
|
|
117
|
+
// parked hover on its match — snapping to the value would clobber
|
|
118
|
+
// it. Consume the flag on every run so it can never go stale.
|
|
119
|
+
const suppressed = state.suppressValueHoverSnap;
|
|
120
|
+
state.suppressValueHoverSnap = false;
|
|
121
|
+
if (shouldSnap && !suppressed)
|
|
122
|
+
setValueIndexAsHoverIndex();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// Keep hover on a selectable item
|
|
126
|
+
$effect(() => {
|
|
127
|
+
state.filteredItems;
|
|
128
|
+
state.value;
|
|
129
|
+
state.multiple;
|
|
130
|
+
state.listOpen;
|
|
131
|
+
untrack(() => {
|
|
132
|
+
if (state.listOpen && state.filteredItems.length > 0) {
|
|
133
|
+
if (!isItemSelectableCheck(state.filteredItems[state.hoverItemIndex])) {
|
|
134
|
+
checkHoverSelectable();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
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
|
+
});
|
|
155
|
+
// Reset hover to the first selectable item on filterText change
|
|
156
|
+
$effect(() => {
|
|
157
|
+
if (state.filterText) {
|
|
158
|
+
untrack(() => {
|
|
159
|
+
state.hoverItemIndex = getFirstSelectableIndex();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
85
163
|
return {
|
|
86
|
-
computeNextIndex,
|
|
87
164
|
setHoverIndex,
|
|
88
|
-
setValueIndexAsHoverIndex,
|
|
89
|
-
checkHoverSelectable,
|
|
90
|
-
getFirstSelectableIndex,
|
|
91
165
|
isItemActive,
|
|
92
166
|
isItemSelectable,
|
|
93
167
|
handleHover,
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
|
|
1
|
+
import type { ItemLike, LoadOptionsActions, SelectItem, SelectState } from './types.js';
|
|
2
|
+
export interface HandleLoadOptionsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Validate the selection against the loaded items (dependency-driven reloads):
|
|
5
|
+
* entries missing from a non-empty result are dropped; an empty result is no
|
|
6
|
+
* evidence and never clears.
|
|
7
|
+
*/
|
|
8
|
+
validateValue?: boolean;
|
|
9
|
+
/** Defer the fetch through the debounce action (typing); defaults to the legacy prevFilterText comparison. */
|
|
10
|
+
debounce?: boolean;
|
|
11
|
+
/** Clear value/items in the disabled branch; the load effect only passes true on an actual disabled transition. */
|
|
12
|
+
clearValueOnDisabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function useLoadOptions<Item extends ItemLike = SelectItem>(state: SelectState<Item>, actions: LoadOptionsActions): {
|
|
15
|
+
handleLoadOptions: (currentFilterText: string, options?: HandleLoadOptionsOptions) => void;
|
|
16
|
+
cancelPendingFilterLoad: () => void;
|
|
17
|
+
retireValidationForFreshSelection: () => void;
|
|
18
|
+
invalidateLoads: () => void;
|
|
4
19
|
};
|