svelte-5-select 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -109
- package/dist/ChevronIcon.svelte +8 -13
- package/dist/ClearIcon.svelte +3 -11
- package/dist/LoadingIcon.svelte +9 -2
- package/dist/Select.svelte +955 -437
- package/dist/Select.svelte.d.ts +37 -5
- package/dist/aria-handlers.svelte.d.ts +4 -1
- package/dist/aria-handlers.svelte.js +19 -8
- package/dist/filter.d.ts +10 -2
- package/dist/filter.js +14 -7
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/keyboard-navigation.svelte.d.ts +7 -2
- package/dist/keyboard-navigation.svelte.js +285 -47
- package/dist/no-styles/ChevronIcon.svelte +11 -0
- package/dist/no-styles/ChevronIcon.svelte.d.ts +26 -0
- package/dist/no-styles/ClearIcon.svelte +8 -0
- package/dist/no-styles/ClearIcon.svelte.d.ts +26 -0
- package/dist/no-styles/LoadingIcon.svelte +13 -0
- package/dist/no-styles/LoadingIcon.svelte.d.ts +26 -0
- package/dist/no-styles/Select.svelte +1383 -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 +112 -71
- package/dist/tailwind.css +120 -17
- package/dist/types.d.ts +459 -129
- package/dist/use-hover.svelte.d.ts +3 -7
- package/dist/use-hover.svelte.js +91 -36
- package/dist/use-load-options.svelte.d.ts +18 -3
- package/dist/use-load-options.svelte.js +333 -42
- package/dist/use-value.svelte.d.ts +2 -11
- package/dist/use-value.svelte.js +322 -111
- package/dist/utils.d.ts +19 -8
- package/dist/utils.js +52 -8
- package/package.json +117 -95
package/dist/Select.svelte.d.ts
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
import type { SelectProps } from './types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { ItemLike, SelectProps, SelectRow } from './types';
|
|
2
|
+
import type { SelectItem } from './types';
|
|
3
|
+
declare function $$render<Item extends ItemLike = SelectItem, Multiple extends boolean = false>(): {
|
|
4
|
+
props: SelectProps<Item, Multiple>;
|
|
5
|
+
exports: {
|
|
6
|
+
getFilteredItems: () => SelectRow<Item>[];
|
|
7
|
+
reset: () => void;
|
|
8
|
+
};
|
|
9
|
+
bindings: "loading" | "value" | "hoverItemIndex" | "listOpen" | "filterText" | "items" | "justValue" | "focused" | "container" | "input";
|
|
10
|
+
slots: {};
|
|
11
|
+
events: {};
|
|
12
|
+
};
|
|
13
|
+
declare class __sveltets_Render<Item extends ItemLike = SelectItem, Multiple extends boolean = false> {
|
|
14
|
+
props(): ReturnType<typeof $$render<Item, Multiple>>['props'];
|
|
15
|
+
events(): ReturnType<typeof $$render<Item, Multiple>>['events'];
|
|
16
|
+
slots(): ReturnType<typeof $$render<Item, Multiple>>['slots'];
|
|
17
|
+
bindings(): "loading" | "value" | "hoverItemIndex" | "listOpen" | "filterText" | "items" | "justValue" | "focused" | "container" | "input";
|
|
18
|
+
exports(): {
|
|
19
|
+
getFilteredItems: () => SelectRow<Item>[];
|
|
20
|
+
reset: () => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface $$IsomorphicComponent {
|
|
24
|
+
new <Item extends ItemLike = SelectItem, Multiple extends boolean = false>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Item, Multiple>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Item, Multiple>['props']>, ReturnType<__sveltets_Render<Item, Multiple>['events']>, ReturnType<__sveltets_Render<Item, Multiple>['slots']>> & {
|
|
25
|
+
$$bindings?: ReturnType<__sveltets_Render<Item, Multiple>['bindings']>;
|
|
26
|
+
} & ReturnType<__sveltets_Render<Item, Multiple>['exports']>;
|
|
27
|
+
<Item extends ItemLike = SelectItem, Multiple extends boolean = false>(internal: unknown, props: ReturnType<__sveltets_Render<Item, Multiple>['props']> & {}): ReturnType<__sveltets_Render<Item, Multiple>['exports']>;
|
|
28
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A select/autocomplete/typeahead control: a WAI-ARIA combobox with a floating
|
|
32
|
+
* listbox popup. Generic over your item type; supports multi-select, async
|
|
33
|
+
* loading (`loadOptions`), grouping (`groupBy`), and snippet-based customization.
|
|
34
|
+
* Bind `value` (and optionally `justValue`, `filterText`, `listOpen`, `focused`).
|
|
35
|
+
*/
|
|
36
|
+
declare const Select: $$IsomorphicComponent;
|
|
37
|
+
type Select<Item extends ItemLike = SelectItem, Multiple extends boolean = false> = InstanceType<typeof Select<Item, Multiple>>;
|
|
6
38
|
export default Select;
|
|
@@ -3,14 +3,17 @@ interface AriaHandlersConfig {
|
|
|
3
3
|
ariaValues: (values: string) => string;
|
|
4
4
|
ariaListOpen: (label: string, count: number) => string;
|
|
5
5
|
ariaFocused: () => string;
|
|
6
|
+
ariaEmpty?: () => string;
|
|
7
|
+
ariaLoading?: () => string;
|
|
6
8
|
}
|
|
7
9
|
interface AriaHandlersContext {
|
|
8
|
-
value: string | SelectItem | SelectItem[] | null | undefined;
|
|
10
|
+
value: string | SelectItem | (SelectItem | string)[] | null | undefined;
|
|
9
11
|
filteredItems: SelectItem[];
|
|
10
12
|
hoverItemIndex: number;
|
|
11
13
|
listOpen: boolean;
|
|
12
14
|
multiple: boolean;
|
|
13
15
|
label: string;
|
|
16
|
+
loading?: boolean;
|
|
14
17
|
}
|
|
15
18
|
export declare function useAriaHandlers(config: AriaHandlersConfig): {
|
|
16
19
|
handleAriaSelection: (context: AriaHandlersContext) => string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export function useAriaHandlers(config) {
|
|
2
|
-
|
|
2
|
+
// Read the config properties at call time so live getters (see Select.svelte) stay reactive
|
|
3
3
|
function handleAriaSelection(context) {
|
|
4
4
|
const { value, multiple, label } = context;
|
|
5
|
-
|
|
5
|
+
// An empty array is no selection: announcing it would claim one exists
|
|
6
|
+
if (!value || (Array.isArray(value) && value.length === 0))
|
|
6
7
|
return '';
|
|
7
8
|
let selected = undefined;
|
|
8
9
|
if (typeof value === 'string') {
|
|
@@ -14,19 +15,29 @@ export function useAriaHandlers(config) {
|
|
|
14
15
|
else if (!multiple && value) {
|
|
15
16
|
selected = value[label];
|
|
16
17
|
}
|
|
17
|
-
return ariaValues(selected || '');
|
|
18
|
+
return config.ariaValues(selected || '');
|
|
18
19
|
}
|
|
19
20
|
function handleAriaContent(context) {
|
|
20
|
-
const { filteredItems, hoverItemIndex, listOpen, label } = context;
|
|
21
|
-
if (!filteredItems || filteredItems.length === 0)
|
|
21
|
+
const { filteredItems, hoverItemIndex, listOpen, label, loading } = context;
|
|
22
|
+
if (!filteredItems || filteredItems.length === 0) {
|
|
23
|
+
// The visible list shows a loading/empty state here; silence would leave
|
|
24
|
+
// screen-reader users unable to tell pending results from no results
|
|
25
|
+
if (listOpen) {
|
|
26
|
+
return loading ? (config.ariaLoading?.() ?? 'Loading Data') : (config.ariaEmpty?.() ?? 'No options');
|
|
27
|
+
}
|
|
22
28
|
return '';
|
|
29
|
+
}
|
|
23
30
|
const _item = filteredItems[hoverItemIndex];
|
|
24
31
|
if (listOpen && _item) {
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
// Presentational group headers are aria-hidden and unreachable by the
|
|
33
|
+
// cursor, so counting them would announce more "results" than a
|
|
34
|
+
// screen-reader user can ever reach. Selectable headers
|
|
35
|
+
// (groupHeaderSelectable) are real options and do count.
|
|
36
|
+
const count = filteredItems.filter((item) => !(item.groupHeader && !item.selectable)).length;
|
|
37
|
+
return config.ariaListOpen(_item[label], count);
|
|
27
38
|
}
|
|
28
39
|
else {
|
|
29
|
-
return ariaFocused();
|
|
40
|
+
return config.ariaFocused();
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
return {
|
package/dist/filter.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
import type { FilterConfig, SelectItem } from './types';
|
|
2
|
-
|
|
1
|
+
import type { FilterConfig, ItemLike, SelectItem } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The built-in filtering pipeline: converts raw string items, applies
|
|
4
|
+
* `itemFilter` against the filter text (skipped when `loadOptions` is set —
|
|
5
|
+
* loader results are already filtered remotely), hides selected options in
|
|
6
|
+
* multiple mode (`filterSelectedItems`), and applies grouping. Exported so a
|
|
7
|
+
* custom `filter` prop can delegate to it before or after its own logic; see
|
|
8
|
+
* {@link FilterConfig} for what each field carries.
|
|
9
|
+
*/
|
|
10
|
+
export default function filter<Item extends ItemLike = SelectItem>({ loadOptions, filterText, items, multiple, value, itemId, groupBy, filterSelectedItems, itemFilter, convertStringItemsToObjects, applyGrouping, label, }: FilterConfig<Item>): SelectItem[];
|
package/dist/filter.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { areItemsEqual, isStringArray } from './utils';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The built-in filtering pipeline: converts raw string items, applies
|
|
4
|
+
* `itemFilter` against the filter text (skipped when `loadOptions` is set —
|
|
5
|
+
* loader results are already filtered remotely), hides selected options in
|
|
6
|
+
* multiple mode (`filterSelectedItems`), and applies grouping. Exported so a
|
|
7
|
+
* custom `filter` prop can delegate to it before or after its own logic; see
|
|
8
|
+
* {@link FilterConfig} for what each field carries.
|
|
9
|
+
*/
|
|
10
|
+
export default function filter({ loadOptions, filterText = '', items, multiple, value, itemId, groupBy, filterSelectedItems, itemFilter, convertStringItemsToObjects, applyGrouping, label, }) {
|
|
6
11
|
// If no items, return empty array
|
|
7
12
|
if (!items) {
|
|
8
13
|
return [];
|
|
@@ -12,9 +17,11 @@ export default function filter({ loadOptions, filterText = '', items, multiple,
|
|
|
12
17
|
if (aStringArray) {
|
|
13
18
|
typedItems = convertStringItemsToObjects(typedItems);
|
|
14
19
|
}
|
|
15
|
-
// Filter items based on filter text and selection
|
|
20
|
+
// Filter items based on filter text and selection.
|
|
21
|
+
// With loadOptions the results are already filtered remotely, so skip itemFilter.
|
|
22
|
+
// String items were converted to plain SelectItems above, hence the Item cast.
|
|
16
23
|
let filterResults = typedItems.filter((item) => {
|
|
17
|
-
let matchesFilter = itemFilter(item[label], filterText, item);
|
|
24
|
+
let matchesFilter = loadOptions ? true : itemFilter(item[label], filterText, item);
|
|
18
25
|
if (matchesFilter && multiple && Array.isArray(value) && value.length > 0) {
|
|
19
26
|
matchesFilter = !value.some((x) => {
|
|
20
27
|
return filterSelectedItems ? areItemsEqual(x, item, itemId) : false;
|
|
@@ -24,7 +31,7 @@ export default function filter({ loadOptions, filterText = '', items, multiple,
|
|
|
24
31
|
});
|
|
25
32
|
// Apply grouping if groupBy function is provided
|
|
26
33
|
if (groupBy) {
|
|
27
|
-
filterResults =
|
|
34
|
+
filterResults = applyGrouping(filterResults);
|
|
28
35
|
}
|
|
29
36
|
return filterResults;
|
|
30
37
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,5 @@ 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
5
|
export { default as filter } from './filter';
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export type { SelectItem, SelectProps, SelectValue, JustValue, FloatingConfig, FilterConfig, KeyboardNavigationContext, ErrorEvent, } from './types';
|
|
6
|
+
export { areItemsEqual, isGroupHeader, normalizeItem } from './utils';
|
|
7
|
+
export type { ItemLike, SelectItem, SelectGroupHeader, SelectRow, SelectProps, SelectValue, SelectValueProp, SelectClearValue, JustValue, FloatingConfig, FilterConfig, SelectErrorEvent, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -6,5 +6,4 @@ export { default as ClearIcon } from './ClearIcon.svelte';
|
|
|
6
6
|
export { default as LoadingIcon } from './LoadingIcon.svelte';
|
|
7
7
|
// Utility functions
|
|
8
8
|
export { default as filter } from './filter';
|
|
9
|
-
export {
|
|
10
|
-
export { isStringArray, isCancelled, areItemsEqual } from './utils';
|
|
9
|
+
export { areItemsEqual, isGroupHeader, normalizeItem } from './utils';
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function useKeyboardNavigation(
|
|
1
|
+
import type { ItemLike, KeyboardNavigationActions, KeyboardNavigationState, SelectItem } from './types';
|
|
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;
|
|
5
5
|
handleEnterKey: (e: KeyboardEvent) => void;
|
|
6
|
+
handleSpaceKey: (e: KeyboardEvent) => void;
|
|
6
7
|
handleArrowDownKey: (e: KeyboardEvent) => void;
|
|
7
8
|
handleArrowUpKey: (e: KeyboardEvent) => void;
|
|
8
9
|
handleTabKey: (e: KeyboardEvent) => void;
|
|
9
10
|
handleBackspaceKey: (e: KeyboardEvent) => void;
|
|
10
11
|
handleArrowLeftKey: (e: KeyboardEvent) => void;
|
|
11
12
|
handleArrowRightKey: (e: KeyboardEvent) => void;
|
|
13
|
+
handleHomeKey: (e: KeyboardEvent) => void;
|
|
14
|
+
handleEndKey: (e: KeyboardEvent) => void;
|
|
15
|
+
handlePageUpKey: (e: KeyboardEvent) => void;
|
|
16
|
+
handlePageDownKey: (e: KeyboardEvent) => void;
|
|
12
17
|
};
|
|
@@ -1,124 +1,362 @@
|
|
|
1
|
-
import { areItemsEqual } from './utils';
|
|
2
|
-
export function useKeyboardNavigation(
|
|
1
|
+
import { areItemsEqual, getItemProperty, isItemSelectableCheck } from './utils';
|
|
2
|
+
export function useKeyboardNavigation(state, actions) {
|
|
3
|
+
// Handlers claim an event (preventDefault/stopPropagation) only on branches
|
|
4
|
+
// that actually act on it. Unclaimed keys keep bubbling so ancestors still
|
|
5
|
+
// see them — Escape can close a surrounding dialog, Enter can submit a form.
|
|
6
|
+
// Claimed keys must stop propagation so the component's window listener does
|
|
7
|
+
// not handle the same bubbled event a second time.
|
|
3
8
|
function handleKeyDown(e) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
// The disabled gate is defence in depth: disabling releases focus, but
|
|
10
|
+
// `focused` can lag DOM focus (e.g. the deferred-blur window), and a
|
|
11
|
+
// disabled control must never mutate its value from the keyboard.
|
|
12
|
+
if (!state.focused || state.disabled)
|
|
7
13
|
return;
|
|
8
14
|
const handlers = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Escape: handleEscapeKey,
|
|
16
|
+
Enter: handleEnterKey,
|
|
17
|
+
' ': handleSpaceKey,
|
|
18
|
+
ArrowDown: handleArrowDownKey,
|
|
19
|
+
ArrowUp: handleArrowUpKey,
|
|
20
|
+
Tab: handleTabKey,
|
|
21
|
+
Backspace: handleBackspaceKey,
|
|
22
|
+
ArrowLeft: handleArrowLeftKey,
|
|
23
|
+
ArrowRight: handleArrowRightKey,
|
|
24
|
+
Home: handleHomeKey,
|
|
25
|
+
End: handleEndKey,
|
|
26
|
+
PageUp: handlePageUpKey,
|
|
27
|
+
PageDown: handlePageDownKey,
|
|
17
28
|
};
|
|
18
29
|
const handler = handlers[e.key];
|
|
19
30
|
if (handler) {
|
|
20
31
|
handler(e);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
handleTypeAheadKey(e);
|
|
35
|
+
}
|
|
36
|
+
// Type-ahead for select-only mode (APG combobox pattern): the input is
|
|
37
|
+
// readonly when searchable is false, so printable characters move hover to
|
|
38
|
+
// the next option whose label starts with what was typed. Searchable
|
|
39
|
+
// selects filter by typing instead and never enter this path.
|
|
40
|
+
let typeAheadQuery = '';
|
|
41
|
+
let typeAheadLastKeyTime = 0;
|
|
42
|
+
const TYPE_AHEAD_RESET_MS = 700;
|
|
43
|
+
function handleTypeAheadKey(e) {
|
|
44
|
+
if (state.searchable)
|
|
45
|
+
return;
|
|
46
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey)
|
|
47
|
+
return;
|
|
48
|
+
if (e.key === ' ' && typeAheadQuery === '')
|
|
49
|
+
return; // a bare Space is not a query
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
// Event timestamps instead of a reset timer: nothing to clean up on destroy
|
|
53
|
+
if (e.timeStamp - typeAheadLastKeyTime > TYPE_AHEAD_RESET_MS)
|
|
54
|
+
typeAheadQuery = '';
|
|
55
|
+
typeAheadLastKeyTime = e.timeStamp;
|
|
56
|
+
typeAheadQuery += e.key.toLowerCase();
|
|
57
|
+
let openedByThisKey = false;
|
|
58
|
+
if (!state.listOpen) {
|
|
59
|
+
state.listOpen = true;
|
|
60
|
+
state.activeValue = undefined;
|
|
61
|
+
openedByThisKey = true;
|
|
62
|
+
}
|
|
63
|
+
const { filteredItems, hoverItemIndex, label } = state;
|
|
64
|
+
if (filteredItems.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
// Repeating one character cycles through the options with that initial;
|
|
67
|
+
// a growing query refines the match starting from the current option
|
|
68
|
+
const isSameCharRun = typeAheadQuery.length > 1 && typeAheadQuery.split('').every((ch) => ch === typeAheadQuery[0]);
|
|
69
|
+
const query = isSameCharRun ? typeAheadQuery[0] : typeAheadQuery;
|
|
70
|
+
const startOffset = isSameCharRun || typeAheadQuery.length === 1 ? 1 : 0;
|
|
71
|
+
for (let step = 0; step < filteredItems.length; step++) {
|
|
72
|
+
const i = (hoverItemIndex + startOffset + step) % filteredItems.length;
|
|
73
|
+
const item = filteredItems[i];
|
|
74
|
+
if (!isItemSelectableCheck(item))
|
|
75
|
+
continue;
|
|
76
|
+
const itemLabel = String(getItemProperty(item, label) ?? '').toLowerCase();
|
|
77
|
+
if (itemLabel.startsWith(query)) {
|
|
78
|
+
state.hoverItemIndex = i;
|
|
79
|
+
state.userNavigatedSinceOpen = true;
|
|
80
|
+
// Handlers run before effects flush: when this keypress also
|
|
81
|
+
// opened the list, the open-with-value hover effect would fire
|
|
82
|
+
// next and snap hover back to the selected value, clobbering
|
|
83
|
+
// the match we just parked it on. Only an open that found a
|
|
84
|
+
// match suppresses the snap — Arrow/Space opens keep it.
|
|
85
|
+
if (openedByThisKey)
|
|
86
|
+
state.suppressValueHoverSnap = true;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Space has combobox semantics (open, then select the current option) only in
|
|
92
|
+
// select-only mode. In a searchable/editable combobox it is an ordinary
|
|
93
|
+
// character for the filter text, so we leave it entirely alone there.
|
|
94
|
+
function handleSpaceKey(e) {
|
|
95
|
+
if (state.searchable)
|
|
96
|
+
return;
|
|
97
|
+
if (e.ctrlKey || e.metaKey || e.altKey)
|
|
98
|
+
return;
|
|
99
|
+
// While a type-ahead query is still live, Space belongs to it so labels
|
|
100
|
+
// that contain spaces ("New York") stay reachable by typing.
|
|
101
|
+
const typeAheadLive = typeAheadQuery !== '' && e.timeStamp - typeAheadLastKeyTime <= TYPE_AHEAD_RESET_MS;
|
|
102
|
+
if (typeAheadLive) {
|
|
103
|
+
handleTypeAheadKey(e);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
if (!state.listOpen) {
|
|
109
|
+
state.listOpen = true;
|
|
110
|
+
state.activeValue = undefined;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// List open: behave like Enter — select the current option, or close if it
|
|
114
|
+
// is already the (single) selected value.
|
|
115
|
+
const { filteredItems, hoverItemIndex, value, multiple, itemId } = state;
|
|
116
|
+
if (filteredItems.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const hoverItem = filteredItems[hoverItemIndex];
|
|
119
|
+
if (!multiple && areItemsEqual(value, hoverItem, itemId)) {
|
|
120
|
+
actions.closeList();
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
actions.handleSelect(hoverItem);
|
|
21
124
|
}
|
|
22
125
|
}
|
|
23
126
|
function handleEscapeKey(e) {
|
|
127
|
+
if (!state.listOpen)
|
|
128
|
+
return;
|
|
24
129
|
e.preventDefault();
|
|
25
|
-
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
actions.closeList();
|
|
26
132
|
}
|
|
27
133
|
function handleEnterKey(e) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
134
|
+
const { listOpen, filteredItems, hoverItemIndex, value, multiple, itemId } = state;
|
|
135
|
+
if (!listOpen) {
|
|
136
|
+
// Searchable mode leaves Enter alone on a closed list (implicit form
|
|
137
|
+
// submission must keep working), but the select-only combobox
|
|
138
|
+
// pattern (APG, searchable={false}) specifies Enter opens the
|
|
139
|
+
// listbox — a native-<select>-like control must not submit the form
|
|
140
|
+
// instead of opening.
|
|
141
|
+
if (state.searchable)
|
|
142
|
+
return;
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
state.listOpen = true;
|
|
146
|
+
state.activeValue = undefined;
|
|
31
147
|
return;
|
|
148
|
+
}
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
32
151
|
if (filteredItems.length === 0)
|
|
33
152
|
return;
|
|
34
153
|
const hoverItem = filteredItems[hoverItemIndex];
|
|
35
154
|
if (!multiple && areItemsEqual(value, hoverItem, itemId)) {
|
|
36
|
-
|
|
155
|
+
actions.closeList();
|
|
37
156
|
}
|
|
38
157
|
else {
|
|
39
|
-
|
|
158
|
+
actions.handleSelect(filteredItems[hoverItemIndex]);
|
|
40
159
|
}
|
|
41
160
|
}
|
|
42
161
|
function handleArrowDownKey(e) {
|
|
43
162
|
e.preventDefault();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
163
|
+
e.stopPropagation();
|
|
164
|
+
// APG: Alt+Down opens the list without moving the visual cursor.
|
|
165
|
+
if (e.altKey) {
|
|
166
|
+
if (!state.listOpen) {
|
|
167
|
+
state.listOpen = true;
|
|
168
|
+
state.activeValue = undefined;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (state.listOpen) {
|
|
173
|
+
actions.setHoverIndex(1);
|
|
174
|
+
state.userNavigatedSinceOpen = true;
|
|
47
175
|
}
|
|
48
176
|
else {
|
|
49
|
-
|
|
50
|
-
|
|
177
|
+
state.listOpen = true;
|
|
178
|
+
state.activeValue = undefined;
|
|
51
179
|
}
|
|
52
180
|
}
|
|
53
181
|
function handleArrowUpKey(e) {
|
|
54
182
|
e.preventDefault();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
183
|
+
e.stopPropagation();
|
|
184
|
+
// APG: Alt+Up closes the list without changing the selection.
|
|
185
|
+
if (e.altKey) {
|
|
186
|
+
if (state.listOpen)
|
|
187
|
+
actions.closeList();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (state.listOpen) {
|
|
191
|
+
actions.setHoverIndex(-1);
|
|
192
|
+
state.userNavigatedSinceOpen = true;
|
|
58
193
|
}
|
|
59
194
|
else {
|
|
60
|
-
|
|
61
|
-
|
|
195
|
+
state.listOpen = true;
|
|
196
|
+
state.activeValue = undefined;
|
|
62
197
|
}
|
|
63
198
|
}
|
|
64
199
|
function handleTabKey(e) {
|
|
65
|
-
const { listOpen, focused, filteredItems, hoverItemIndex, value, itemId } =
|
|
200
|
+
const { listOpen, focused, filteredItems, hoverItemIndex, value, itemId } = state;
|
|
66
201
|
if (!listOpen || !focused)
|
|
67
202
|
return;
|
|
68
|
-
|
|
69
|
-
|
|
203
|
+
// Tab is never claimed: committing must close the popup and move focus in
|
|
204
|
+
// the same press (APG), so no preventDefault, and the event keeps bubbling
|
|
205
|
+
// for focus traps and ancestor handlers. Re-entry via the window listener
|
|
206
|
+
// is safe because the list is closed by then.
|
|
207
|
+
//
|
|
208
|
+
// Committing also requires expressed intent: the cursor auto-parks on an
|
|
209
|
+
// option the moment the list opens, so a bare open-then-Tab (click the
|
|
210
|
+
// control, decide against picking, tab away) must close without
|
|
211
|
+
// selecting. Intent is navigating (keys, or real pointer movement over
|
|
212
|
+
// the list), type-ahead, or typing — all of which set the flag at their
|
|
213
|
+
// event sites — and a completed selection consumes it (see
|
|
214
|
+
// itemSelected): with closeListOnChange={false} the list stays open and
|
|
215
|
+
// the cursor re-parks on a neighbour, which tabbing away must not
|
|
216
|
+
// commit. Deliberately NOT the current filterText: a seeded initial
|
|
217
|
+
// value or text retained across a close (clearFilterTextOnBlur={false})
|
|
218
|
+
// is not something the user typed this open.
|
|
219
|
+
if (e.shiftKey || // Tabbing backwards leaves the field; it must never commit
|
|
220
|
+
!state.userNavigatedSinceOpen ||
|
|
221
|
+
filteredItems.length === 0 ||
|
|
222
|
+
areItemsEqual(value, filteredItems[hoverItemIndex], itemId)) {
|
|
223
|
+
actions.closeList();
|
|
70
224
|
return;
|
|
71
225
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
context.closeList();
|
|
226
|
+
actions.handleSelect(filteredItems[hoverItemIndex]);
|
|
227
|
+
actions.closeList();
|
|
75
228
|
}
|
|
76
229
|
function handleBackspaceKey(e) {
|
|
77
|
-
const { multiple, filterText, value, activeValue } =
|
|
230
|
+
const { multiple, filterText, value, activeValue } = state;
|
|
78
231
|
if (!multiple || filterText.length > 0)
|
|
79
232
|
return;
|
|
80
|
-
if (
|
|
233
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
// A cursor left stale by a mouse removal's reindexing points at
|
|
236
|
+
// nothing: reset it and consume the press, rather than remove a tag
|
|
237
|
+
// the user never targeted
|
|
238
|
+
if (activeValue !== undefined && activeValue >= value.length) {
|
|
239
|
+
state.activeValue = undefined;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
81
242
|
const indexToRemove = activeValue !== undefined ? activeValue : value.length - 1;
|
|
82
|
-
|
|
243
|
+
actions.handleMultiItemClear(indexToRemove);
|
|
83
244
|
if (activeValue === 0 || activeValue === undefined)
|
|
84
245
|
return;
|
|
85
246
|
const newActiveValue = value.length > activeValue ? activeValue - 1 : undefined;
|
|
86
|
-
|
|
247
|
+
state.activeValue = newActiveValue;
|
|
87
248
|
}
|
|
88
249
|
}
|
|
89
250
|
function handleArrowLeftKey(e) {
|
|
90
|
-
const { value, multiple, filterText, activeValue } =
|
|
251
|
+
const { value, multiple, filterText, activeValue } = state;
|
|
91
252
|
if (!value || !multiple || filterText.length > 0)
|
|
92
253
|
return;
|
|
93
254
|
if (Array.isArray(value)) {
|
|
94
|
-
|
|
95
|
-
|
|
255
|
+
e.stopPropagation();
|
|
256
|
+
// `>= length` re-enters like undefined: a stale cursor (mouse
|
|
257
|
+
// removal reindexed the tags) restarts from the last tag
|
|
258
|
+
if (activeValue === undefined || activeValue >= value.length) {
|
|
259
|
+
state.activeValue = value.length - 1;
|
|
260
|
+
}
|
|
261
|
+
else if (activeValue !== 0) {
|
|
262
|
+
state.activeValue = activeValue - 1;
|
|
96
263
|
}
|
|
97
|
-
|
|
98
|
-
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Home/End only take over list navigation while no filter text is
|
|
267
|
+
// entered, so text-caret movement in the input keeps working
|
|
268
|
+
function handleHomeKey(e) {
|
|
269
|
+
const { listOpen, filteredItems, filterText } = state;
|
|
270
|
+
if (!listOpen || filterText.length > 0)
|
|
271
|
+
return;
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
e.stopPropagation();
|
|
274
|
+
const firstSelectable = filteredItems.findIndex((item) => isItemSelectableCheck(item));
|
|
275
|
+
if (firstSelectable >= 0) {
|
|
276
|
+
state.hoverItemIndex = firstSelectable;
|
|
277
|
+
state.userNavigatedSinceOpen = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function handleEndKey(e) {
|
|
281
|
+
const { listOpen, filteredItems, filterText } = state;
|
|
282
|
+
if (!listOpen || filterText.length > 0)
|
|
283
|
+
return;
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
for (let i = filteredItems.length - 1; i >= 0; i--) {
|
|
287
|
+
if (isItemSelectableCheck(filteredItems[i])) {
|
|
288
|
+
state.hoverItemIndex = i;
|
|
289
|
+
state.userNavigatedSinceOpen = true;
|
|
290
|
+
return;
|
|
99
291
|
}
|
|
100
292
|
}
|
|
101
293
|
}
|
|
294
|
+
// PageUp/PageDown jump the hover by a page of selectable options, clamped to
|
|
295
|
+
// the first/last selectable (no wrap, unlike the Arrow keys). They stay active
|
|
296
|
+
// while filtering: PageUp/PageDown do not move the caret in a single-line
|
|
297
|
+
// input, so there is nothing to conflict with the way Home/End would.
|
|
298
|
+
const PAGE_SIZE = 10;
|
|
299
|
+
function pageHover(direction) {
|
|
300
|
+
const { listOpen, filteredItems } = state;
|
|
301
|
+
if (!listOpen)
|
|
302
|
+
return false;
|
|
303
|
+
const selectable = [];
|
|
304
|
+
for (let i = 0; i < filteredItems.length; i++) {
|
|
305
|
+
if (isItemSelectableCheck(filteredItems[i]))
|
|
306
|
+
selectable.push(i);
|
|
307
|
+
}
|
|
308
|
+
if (selectable.length === 0)
|
|
309
|
+
return false;
|
|
310
|
+
// Where the current hover sits among selectable rows; if it is parked on a
|
|
311
|
+
// non-selectable header, anchor just outside so a full page still moves.
|
|
312
|
+
let pos = selectable.indexOf(state.hoverItemIndex);
|
|
313
|
+
if (pos === -1)
|
|
314
|
+
pos = direction > 0 ? -1 : selectable.length;
|
|
315
|
+
const targetPos = Math.min(selectable.length - 1, Math.max(0, pos + direction * PAGE_SIZE));
|
|
316
|
+
state.hoverItemIndex = selectable[targetPos];
|
|
317
|
+
state.userNavigatedSinceOpen = true;
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
function handlePageDownKey(e) {
|
|
321
|
+
if (!pageHover(1))
|
|
322
|
+
return;
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
}
|
|
326
|
+
function handlePageUpKey(e) {
|
|
327
|
+
if (!pageHover(-1))
|
|
328
|
+
return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
}
|
|
102
332
|
function handleArrowRightKey(e) {
|
|
103
|
-
const { value, multiple, filterText, activeValue } =
|
|
333
|
+
const { value, multiple, filterText, activeValue } = state;
|
|
104
334
|
if (!value || !multiple || filterText.length > 0 || activeValue === undefined)
|
|
105
335
|
return;
|
|
336
|
+
if (!Array.isArray(value))
|
|
337
|
+
return;
|
|
338
|
+
e.stopPropagation();
|
|
106
339
|
if (activeValue === value.length - 1) {
|
|
107
|
-
|
|
340
|
+
state.activeValue = undefined;
|
|
108
341
|
}
|
|
109
342
|
else if (activeValue < value.length - 1) {
|
|
110
|
-
|
|
343
|
+
state.activeValue = activeValue + 1;
|
|
111
344
|
}
|
|
112
345
|
}
|
|
113
346
|
return {
|
|
114
347
|
handleKeyDown,
|
|
115
348
|
handleEscapeKey,
|
|
116
349
|
handleEnterKey,
|
|
350
|
+
handleSpaceKey,
|
|
117
351
|
handleArrowDownKey,
|
|
118
352
|
handleArrowUpKey,
|
|
119
353
|
handleTabKey,
|
|
120
354
|
handleBackspaceKey,
|
|
121
355
|
handleArrowLeftKey,
|
|
122
356
|
handleArrowRightKey,
|
|
357
|
+
handleHomeKey,
|
|
358
|
+
handleEndKey,
|
|
359
|
+
handlePageUpKey,
|
|
360
|
+
handlePageDownKey,
|
|
123
361
|
};
|
|
124
362
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg width="100%" height="100%" viewBox="0 0 20 20" focusable="false" aria-hidden="true">
|
|
2
|
+
<path
|
|
3
|
+
fill="currentColor"
|
|
4
|
+
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747
|
|
5
|
+
3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0
|
|
6
|
+
1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502
|
|
7
|
+
0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0
|
|
8
|
+
0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z" />
|
|
9
|
+
</svg>
|
|
10
|
+
|
|
11
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
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> & {
|
|
16
|
+
$$bindings?: Bindings;
|
|
17
|
+
} & Exports;
|
|
18
|
+
(internal: unknown, props: {
|
|
19
|
+
$$events?: Events;
|
|
20
|
+
$$slots?: Slots;
|
|
21
|
+
}): Exports & {
|
|
22
|
+
$set?: any;
|
|
23
|
+
$on?: any;
|
|
24
|
+
};
|
|
25
|
+
z_$$bindings?: Bindings;
|
|
26
|
+
}
|