svelte-5-select 1.0.2 → 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.
Files changed (36) hide show
  1. package/README.md +239 -109
  2. package/dist/ChevronIcon.svelte +8 -13
  3. package/dist/ClearIcon.svelte +3 -11
  4. package/dist/LoadingIcon.svelte +9 -2
  5. package/dist/Select.svelte +954 -436
  6. package/dist/Select.svelte.d.ts +37 -5
  7. package/dist/aria-handlers.svelte.d.ts +4 -1
  8. package/dist/aria-handlers.svelte.js +19 -8
  9. package/dist/filter.d.ts +10 -2
  10. package/dist/filter.js +14 -7
  11. package/dist/index.d.ts +2 -3
  12. package/dist/index.js +1 -2
  13. package/dist/keyboard-navigation.svelte.d.ts +7 -2
  14. package/dist/keyboard-navigation.svelte.js +285 -47
  15. package/dist/no-styles/ChevronIcon.svelte +11 -0
  16. package/dist/no-styles/ChevronIcon.svelte.d.ts +26 -0
  17. package/dist/no-styles/ClearIcon.svelte +8 -0
  18. package/dist/no-styles/ClearIcon.svelte.d.ts +26 -0
  19. package/dist/no-styles/LoadingIcon.svelte +13 -0
  20. package/dist/no-styles/LoadingIcon.svelte.d.ts +26 -0
  21. package/dist/no-styles/Select.svelte +1383 -0
  22. package/dist/no-styles/Select.svelte.d.ts +38 -0
  23. package/dist/select-state.svelte.d.ts +15 -0
  24. package/dist/select-state.svelte.js +161 -0
  25. package/dist/styles/default.css +112 -71
  26. package/dist/tailwind.css +120 -17
  27. package/dist/types.d.ts +459 -129
  28. package/dist/use-hover.svelte.d.ts +3 -7
  29. package/dist/use-hover.svelte.js +91 -36
  30. package/dist/use-load-options.svelte.d.ts +18 -3
  31. package/dist/use-load-options.svelte.js +333 -42
  32. package/dist/use-value.svelte.d.ts +2 -11
  33. package/dist/use-value.svelte.js +322 -111
  34. package/dist/utils.d.ts +19 -8
  35. package/dist/utils.js +52 -8
  36. package/package.json +117 -94
@@ -1,6 +1,38 @@
1
- import type { SelectProps } from './types';
2
- declare const Select: import("svelte").Component<SelectProps, {
3
- reset: () => void;
4
- }, "loading" | "value" | "hoverItemIndex" | "listOpen" | "filterText" | "items" | "focused" | "justValue">;
5
- type Select = ReturnType<typeof Select>;
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
- const { ariaValues, ariaListOpen, ariaFocused } = config;
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
- if (!value)
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
- const count = filteredItems.length;
26
- return ariaListOpen(_item[label], count);
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
- export default function filter({ loadOptions, filterText, items, multiple, value, itemId, groupBy, filterSelectedItems, itemFilter, convertStringItemsToObjects, filterGroupedItems, label, }: FilterConfig): SelectItem[];
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
- export default function filter({ loadOptions, filterText = '', items, multiple, value, itemId, groupBy, filterSelectedItems, itemFilter, convertStringItemsToObjects, filterGroupedItems, label, }) {
3
- if (items && loadOptions) {
4
- return items;
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 = filterGroupedItems(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 { useKeyboardNavigation } from './keyboard-navigation.svelte';
7
- export { isStringArray, isCancelled, areItemsEqual } from './utils';
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 { useKeyboardNavigation } from './keyboard-navigation.svelte';
10
- export { isStringArray, isCancelled, areItemsEqual } from './utils';
9
+ export { areItemsEqual, isGroupHeader, normalizeItem } from './utils';
@@ -1,12 +1,17 @@
1
- import type { KeyboardNavigationContext } from './types';
2
- export declare function useKeyboardNavigation(context: KeyboardNavigationContext): {
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(context) {
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
- e.stopPropagation();
5
- const { focused } = context.getState();
6
- if (!focused)
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
- 'Escape': handleEscapeKey,
10
- 'Enter': handleEnterKey,
11
- 'ArrowDown': handleArrowDownKey,
12
- 'ArrowUp': handleArrowUpKey,
13
- 'Tab': handleTabKey,
14
- 'Backspace': handleBackspaceKey,
15
- 'ArrowLeft': handleArrowLeftKey,
16
- 'ArrowRight': handleArrowRightKey,
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
- context.closeList();
130
+ e.stopPropagation();
131
+ actions.closeList();
26
132
  }
27
133
  function handleEnterKey(e) {
28
- e.preventDefault();
29
- const { listOpen, filteredItems, hoverItemIndex, value, multiple, itemId } = context.getState();
30
- if (!listOpen)
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
- context.closeList();
155
+ actions.closeList();
37
156
  }
38
157
  else {
39
- context.handleSelect(filteredItems[hoverItemIndex]);
158
+ actions.handleSelect(filteredItems[hoverItemIndex]);
40
159
  }
41
160
  }
42
161
  function handleArrowDownKey(e) {
43
162
  e.preventDefault();
44
- const { listOpen } = context.getState();
45
- if (listOpen) {
46
- context.setHoverIndex(1);
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
- context.setListOpen(true);
50
- context.setActiveValue(undefined);
177
+ state.listOpen = true;
178
+ state.activeValue = undefined;
51
179
  }
52
180
  }
53
181
  function handleArrowUpKey(e) {
54
182
  e.preventDefault();
55
- const { listOpen } = context.getState();
56
- if (listOpen) {
57
- context.setHoverIndex(-1);
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
- context.setListOpen(true);
61
- context.setActiveValue(undefined);
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 } = context.getState();
200
+ const { listOpen, focused, filteredItems, hoverItemIndex, value, itemId } = state;
66
201
  if (!listOpen || !focused)
67
202
  return;
68
- if (filteredItems.length === 0 || areItemsEqual(value, filteredItems[hoverItemIndex], itemId)) {
69
- context.closeList();
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
- e.preventDefault();
73
- context.handleSelect(filteredItems[hoverItemIndex]);
74
- context.closeList();
226
+ actions.handleSelect(filteredItems[hoverItemIndex]);
227
+ actions.closeList();
75
228
  }
76
229
  function handleBackspaceKey(e) {
77
- const { multiple, filterText, value, activeValue } = context.getState();
230
+ const { multiple, filterText, value, activeValue } = state;
78
231
  if (!multiple || filterText.length > 0)
79
232
  return;
80
- if (multiple && value && value.length > 0) {
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
- context.handleMultiItemClear(indexToRemove);
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
- context.setActiveValue(newActiveValue);
247
+ state.activeValue = newActiveValue;
87
248
  }
88
249
  }
89
250
  function handleArrowLeftKey(e) {
90
- const { value, multiple, filterText, activeValue } = context.getState();
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
- if (activeValue === undefined) {
95
- context.setActiveValue(value.length - 1);
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
- else if (value.length > activeValue && activeValue !== 0) {
98
- context.setActiveValue(activeValue - 1);
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 } = context.getState();
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
- context.setActiveValue(undefined);
340
+ state.activeValue = undefined;
108
341
  }
109
342
  else if (activeValue < value.length - 1) {
110
- context.setActiveValue(activeValue + 1);
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
+ }