bits-ui 1.3.14 → 1.3.16

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 CHANGED
@@ -5,17 +5,22 @@
5
5
  [![npm version](https://flat.badgen.net/npm/v/bits-ui?color=pink)](https://npmjs.com/package/bits-ui)
6
6
  [![npm downloads](https://flat.badgen.net/npm/dm/bits-ui?color=pink)](https://npmjs.com/package/bits-ui)
7
7
  [![license](https://flat.badgen.net/github/license/huntabyte/bits-ui?color=pink)](https://github.com/huntabyte/bits-ui/blob/main/LICENSE)
8
+ [![](https://dcbadge.vercel.app/api/server/fdXy3Sk8Gq?style=flat)](https://discord.gg/fdXy3Sk8Gq)
8
9
 
9
- <!-- /automd -->
10
+ <img style="max-width: 100%" alt="hero" src="https://github.com/user-attachments/assets/19cac792-6a93-4289-b9c7-647794a7de79" />
11
+
12
+ **Bits UI** – the headless components for Svelte.
10
13
 
11
- The headless components for Svelte.
14
+ Flexible, unstyled, and accessible primitives that provide the foundation for building your own high-quality component library.
12
15
 
13
- [Read the docs](https://bits-ui.com)
16
+ ## Documentation
17
+
18
+ Visit https://bits-ui.com/docs to view the documentation.
14
19
 
15
20
  ## Credits
16
21
 
17
22
  - [Bitworks](https://bitworks.cz) - The design team behind the Bits UI documentation and example components.
18
- - [Melt UI](https://melt-ui.com) - The underlying builder API that powers Bits.
23
+ - [Melt UI](https://melt-ui.com) - A powerful builder API that inspired the internal architecture of Bits UI.
19
24
  - [Radix UI](https://radix-ui.com) - The incredible headless component APIs that we've taken heavy inspiration from.
20
25
  - [React Spectrum](https://react-spectrum.adobe.com) - An incredible collection of headless components we've taken inspiration from.
21
26
 
@@ -41,3 +46,14 @@ Built by [@huntabyte](https://github.com/huntabyte) and [community](https://gith
41
46
  </a>
42
47
 
43
48
  <!-- /automd -->
49
+
50
+ ## Community
51
+
52
+ Join the Discord server to ask questions, find collaborators, or just say hi!
53
+
54
+ <a href="https://discord.gg/fdXy3Sk8Gq" alt="Svecosystem Discord community">
55
+ <picture>
56
+ <source media="(prefers-color-scheme: dark)" srcset="https://invidget.switchblade.xyz/fdXy3Sk8Gq">
57
+ <img alt="Svecosystem Discord community" src="https://invidget.switchblade.xyz/fdXy3Sk8Gq?theme=light">
58
+ </picture>
59
+ </a>
@@ -14,6 +14,8 @@ type CommandRootStateProps = WithRefProps<ReadableBoxedValues<{
14
14
  declare class CommandRootState {
15
15
  #private;
16
16
  readonly opts: CommandRootStateProps;
17
+ sortAfterTick: boolean;
18
+ sortAndFilterAfterTick: boolean;
17
19
  allItems: Set<string>;
18
20
  allGroups: Map<string, Set<string>>;
19
21
  allIds: Map<string, {
@@ -32,6 +32,8 @@ const CommandGroupContainerContext = new Context("Command.Group");
32
32
  class CommandRootState {
33
33
  opts;
34
34
  #updateScheduled = false;
35
+ sortAfterTick = false;
36
+ sortAndFilterAfterTick = false;
35
37
  allItems = new Set();
36
38
  allGroups = new Map();
37
39
  allIds = new Map();
@@ -401,7 +403,14 @@ class CommandRootState {
401
403
  return noop;
402
404
  this.allIds.set(id, { value, keywords });
403
405
  this._commandState.filtered.items.set(id, this.#score(value, keywords));
404
- this.#sort();
406
+ // Schedule sorting to run after this tick when all items are added not each time an item is added
407
+ if (!this.sortAfterTick) {
408
+ this.sortAfterTick = true;
409
+ afterTick(() => {
410
+ this.#sort();
411
+ this.sortAfterTick = false;
412
+ });
413
+ }
405
414
  return () => {
406
415
  this.allIds.delete(id);
407
416
  };
@@ -425,8 +434,15 @@ class CommandRootState {
425
434
  this.allGroups.get(groupId).add(id);
426
435
  }
427
436
  }
428
- this.#filterItems();
429
- this.#sort();
437
+ // Schedule sorting and filtering to run after this tick when all items are added not each time an item is added
438
+ if (!this.sortAndFilterAfterTick) {
439
+ this.sortAndFilterAfterTick = true;
440
+ afterTick(() => {
441
+ this.#filterItems();
442
+ this.#sort();
443
+ this.sortAndFilterAfterTick = false;
444
+ });
445
+ }
430
446
  this.#scheduleUpdate();
431
447
  return () => {
432
448
  const selectedItem = this.#getSelectedItem();
@@ -423,6 +423,7 @@ class SelectTriggerState {
423
423
  this.root.opts.value.current = matchedItem.value;
424
424
  },
425
425
  enabled: !this.root.isMulti && this.root.dataTypeaheadEnabled,
426
+ candidateValues: () => (this.root.isMulti ? [] : this.root.candidateLabels),
426
427
  });
427
428
  this.onkeydown = this.onkeydown.bind(this);
428
429
  this.onpointerdown = this.onpointerdown.bind(this);
@@ -437,6 +438,29 @@ class SelectTriggerState {
437
438
  #handlePointerOpen(_) {
438
439
  this.#handleOpen();
439
440
  }
441
+ /**
442
+ * Logic used to handle keyboard selection/deselection.
443
+ *
444
+ * If it returns true, it means the item was selected and whatever is calling
445
+ * this function should return early
446
+ *
447
+ */
448
+ #handleKeyboardSelection() {
449
+ const isCurrentSelectedValue = this.root.highlightedValue === this.root.opts.value.current;
450
+ if (!this.root.opts.allowDeselect.current && isCurrentSelectedValue && !this.root.isMulti) {
451
+ this.root.handleClose();
452
+ return true;
453
+ }
454
+ // "" is a valid value for a select item so we need to check for that
455
+ if (this.root.highlightedValue !== null) {
456
+ this.root.toggleItem(this.root.highlightedValue, this.root.highlightedLabel ?? undefined);
457
+ }
458
+ if (!this.root.isMulti && !isCurrentSelectedValue) {
459
+ this.root.handleClose();
460
+ return true;
461
+ }
462
+ return false;
463
+ }
440
464
  onkeydown(e) {
441
465
  this.root.isUsingKeyboard = true;
442
466
  if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_DOWN)
@@ -450,7 +474,7 @@ class SelectTriggerState {
450
474
  this.root.handleOpen();
451
475
  }
452
476
  else if (!this.root.isMulti && this.root.dataTypeaheadEnabled) {
453
- this.#dataTypeahead.handleTypeaheadSearch(e.key, this.root.candidateLabels);
477
+ this.#dataTypeahead.handleTypeaheadSearch(e.key);
454
478
  return;
455
479
  }
456
480
  // we need to wait for a tick after the menu opens to ensure
@@ -474,23 +498,16 @@ class SelectTriggerState {
474
498
  this.root.handleClose();
475
499
  return;
476
500
  }
477
- if ((e.key === kbd.ENTER || e.key === kbd.SPACE) && !e.isComposing) {
501
+ if ((e.key === kbd.ENTER ||
502
+ // if we're currently "typing ahead", we don't want to select the item
503
+ // just yet as the item the user is trying to get to may have a space in it,
504
+ // so we defer handling the close for this case until further down
505
+ (e.key === kbd.SPACE && this.#domTypeahead.search.current === "")) &&
506
+ !e.isComposing) {
478
507
  e.preventDefault();
479
- const isCurrentSelectedValue = this.root.highlightedValue === this.root.opts.value.current;
480
- if (!this.root.opts.allowDeselect.current &&
481
- isCurrentSelectedValue &&
482
- !this.root.isMulti) {
483
- this.root.handleClose();
484
- return;
485
- }
486
- //"" is a valid value for a select item so we need to check for that
487
- if (this.root.highlightedValue !== null) {
488
- this.root.toggleItem(this.root.highlightedValue, this.root.highlightedLabel ?? undefined);
489
- }
490
- if (!this.root.isMulti && !isCurrentSelectedValue) {
491
- this.root.handleClose();
508
+ const shouldReturn = this.#handleKeyboardSelection();
509
+ if (shouldReturn)
492
510
  return;
493
- }
494
511
  }
495
512
  if (e.key === kbd.ARROW_UP && e.altKey) {
496
513
  this.root.handleClose();
@@ -529,14 +546,16 @@ class SelectTriggerState {
529
546
  }
530
547
  const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
531
548
  const isCharacterKey = e.key.length === 1;
532
- // prevent space from being considered with typeahead
533
- if (e.code === "Space")
534
- return;
549
+ const isSpaceKey = e.key === kbd.SPACE;
535
550
  const candidateNodes = this.root.getCandidateNodes();
536
551
  if (e.key === kbd.TAB)
537
552
  return;
538
- if (!isModifierKey && isCharacterKey) {
539
- this.#domTypeahead.handleTypeaheadSearch(e.key, candidateNodes);
553
+ if (!isModifierKey && (isCharacterKey || isSpaceKey)) {
554
+ const matchedNode = this.#domTypeahead.handleTypeaheadSearch(e.key, candidateNodes);
555
+ if (!matchedNode && isSpaceKey) {
556
+ e.preventDefault();
557
+ this.#handleKeyboardSelection();
558
+ }
540
559
  return;
541
560
  }
542
561
  if (!this.root.highlightedNode) {
@@ -18,6 +18,8 @@ declare class SliderBaseRootState {
18
18
  direction: "rl" | "lr" | "tb" | "bt";
19
19
  constructor(opts: SliderBaseRootStateProps);
20
20
  getAllThumbs: () => HTMLElement[];
21
+ getThumbScale: () => [number, number];
22
+ getPositionFromValue: (thumbValue: number) => number;
21
23
  props: {
22
24
  readonly id: string;
23
25
  readonly "data-orientation": "horizontal" | "vertical";
@@ -46,7 +48,6 @@ declare class SliderSingleRootState extends SliderBaseRootState {
46
48
  handlePointerMove: (e: PointerEvent) => void;
47
49
  handlePointerDown: (e: PointerEvent) => void;
48
50
  handlePointerUp: () => void;
49
- getPositionFromValue: (thumbValue: number) => number;
50
51
  thumbsPropsArr: {
51
52
  readonly role: "slider";
52
53
  readonly "aria-valuemin": number;
@@ -98,7 +99,6 @@ declare class SliderMultiRootState extends SliderBaseRootState {
98
99
  handlePointerMove: (e: PointerEvent) => void;
99
100
  handlePointerDown: (e: PointerEvent) => void;
100
101
  handlePointerUp: () => void;
101
- getPositionFromValue: (thumbValue: number) => number;
102
102
  getAllThumbs: () => HTMLElement[];
103
103
  updateValue: (thumbValue: number, idx: number) => void;
104
104
  thumbsPropsArr: {
@@ -11,7 +11,7 @@ import { getAriaDisabled, getAriaOrientation, getDataDisabled, getDataOrientatio
11
11
  import { kbd } from "../../internal/kbd.js";
12
12
  import { isElementOrSVGElement } from "../../internal/is.js";
13
13
  import { isValidIndex } from "../../internal/arrays.js";
14
- import { snapValueToStep } from "../../internal/math.js";
14
+ import { linearScale, snapValueToStep } from "../../internal/math.js";
15
15
  const SLIDER_ROOT_ATTR = "data-slider-root";
16
16
  const SLIDER_THUMB_ATTR = "data-slider-thumb";
17
17
  const SLIDER_RANGE_ATTR = "data-slider-range";
@@ -42,6 +42,32 @@ class SliderBaseRootState {
42
42
  return [];
43
43
  return Array.from(node.querySelectorAll(`[${SLIDER_THUMB_ATTR}]`));
44
44
  };
45
+ getThumbScale = () => {
46
+ const isVertical = this.opts.orientation.current === "vertical";
47
+ // this assumes all thumbs are the same width
48
+ const activeThumb = this.getAllThumbs()[0];
49
+ const thumbSize = isVertical ? activeThumb?.offsetHeight : activeThumb?.offsetWidth;
50
+ // if thumb size is undefined or 0, fallback to a 0-100 scale
51
+ if (thumbSize === undefined || Number.isNaN(thumbSize) || thumbSize === 0)
52
+ return [0, 100];
53
+ const trackSize = isVertical
54
+ ? this.opts.ref.current?.offsetHeight
55
+ : this.opts.ref.current?.offsetWidth;
56
+ // if track size is undefined or 0, fallback to a 0-100 scale
57
+ if (trackSize === undefined || Number.isNaN(trackSize) || trackSize === 0)
58
+ return [0, 100];
59
+ // the padding on either side
60
+ // half the width of the thumb
61
+ const percentPadding = (thumbSize / 2 / trackSize) * 100;
62
+ const min = percentPadding;
63
+ const max = 100 - percentPadding;
64
+ return [min, max];
65
+ };
66
+ getPositionFromValue = (thumbValue) => {
67
+ const thumbScale = this.getThumbScale();
68
+ const scale = linearScale([this.opts.min.current, this.opts.max.current], thumbScale);
69
+ return scale(thumbValue);
70
+ };
45
71
  props = $derived.by(() => ({
46
72
  id: this.opts.id.current,
47
73
  "data-orientation": getDataOrientation(this.opts.orientation.current),
@@ -169,16 +195,11 @@ class SliderSingleRootState extends SliderBaseRootState {
169
195
  }
170
196
  this.isActive = false;
171
197
  };
172
- getPositionFromValue = (thumbValue) => {
173
- const min = this.opts.min.current;
174
- const max = this.opts.max.current;
175
- return ((thumbValue - min) / (max - min)) * 100;
176
- };
177
198
  thumbsPropsArr = $derived.by(() => {
178
199
  const currValue = this.opts.value.current;
179
200
  return Array.from({ length: 1 }, () => {
180
201
  const thumbValue = currValue;
181
- const thumbPosition = this.getPositionFromValue(thumbValue ?? 0);
202
+ const thumbPosition = this.getPositionFromValue(thumbValue);
182
203
  const style = getThumbStyles(this.direction, thumbPosition);
183
204
  return {
184
205
  role: "slider",
@@ -209,10 +230,11 @@ class SliderSingleRootState extends SliderBaseRootState {
209
230
  const currValue = this.opts.value.current;
210
231
  return Array.from({ length: count }, (_, i) => {
211
232
  const tickPosition = i * (step / difference) * 100;
233
+ const scale = linearScale([this.opts.min.current, this.opts.max.current], this.getThumbScale());
212
234
  const isFirst = i === 0;
213
235
  const isLast = i === count - 1;
214
236
  const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50;
215
- const style = getTickStyles(this.direction, tickPosition, offsetPercentage);
237
+ const style = getTickStyles(this.direction, scale(tickPosition), offsetPercentage);
216
238
  const tickValue = min + i * step;
217
239
  const bounded = tickValue <= currValue;
218
240
  return {
@@ -379,11 +401,6 @@ class SliderMultiRootState extends SliderBaseRootState {
379
401
  }
380
402
  this.isActive = false;
381
403
  };
382
- getPositionFromValue = (thumbValue) => {
383
- const min = this.opts.min.current;
384
- const max = this.opts.max.current;
385
- return ((thumbValue - min) / (max - min)) * 100;
386
- };
387
404
  getAllThumbs = () => {
388
405
  const node = this.opts.ref.current;
389
406
  if (!node)
@@ -66,21 +66,26 @@ export declare function forward<T>(array: T[], index: number, increment: number,
66
66
  */
67
67
  export declare function backward<T>(array: T[], index: number, decrement: number, loop?: boolean): T | undefined;
68
68
  /**
69
- * This is the "meat" of the typeahead matching logic. It takes in all the values,
70
- * the search and the current match, and returns the next match (or `undefined`).
69
+ * Finds the next matching item from a list of values based on a search string.
71
70
  *
72
- * We normalize the search because if a user has repeatedly pressed a character,
73
- * we want the exact same behavior as if we only had that one character
74
- * (ie. cycle through options starting with that character)
71
+ * This function handles several special cases in typeahead behavior:
75
72
  *
76
- * We also reorder the values by wrapping the array around the current match.
77
- * This is so we always look forward from the current match, and picking the first
78
- * match will always be the correct one.
73
+ * 1. Space handling: When a search string ends with a space, it handles it specially:
74
+ * - If there's only one match for the text before the space, it ignores the space
75
+ * - If there are multiple matches and the current match already starts with the search prefix
76
+ * followed by a space, it keeps the current match (doesn't change selection on space)
77
+ * - Only after typing characters beyond the space will it move to a more specific match
79
78
  *
80
- * Finally, if the normalized search is exactly one character, we exclude the
81
- * current match from the values because otherwise it would be the first to match always
82
- * and focus would never move. This is as opposed to the regular case, where we
83
- * don't want focus to move if the current match still matches.
79
+ * 2. Repeated character handling: If a search consists of repeated characters (e.g., "aaa"),
80
+ * it treats it as a single character for matching purposes
81
+ *
82
+ * 3. Cycling behavior: The function wraps around the values array starting from the current match
83
+ * to find the next appropriate match, creating a cycling selection behavior
84
+ *
85
+ * @param values - Array of string values to search through (e.g., the text content of menu items)
86
+ * @param search - The current search string typed by the user
87
+ * @param currentMatch - The currently selected/matched item, if any
88
+ * @returns The next matching value that should be selected, or undefined if no match is found
84
89
  */
85
90
  export declare function getNextMatch(values: string[], search: string, currentMatch?: string): string | undefined;
86
91
  /**
@@ -169,31 +169,76 @@ export function backward(array, index, decrement, loop = true) {
169
169
  return array[targetIndex];
170
170
  }
171
171
  /**
172
- * This is the "meat" of the typeahead matching logic. It takes in all the values,
173
- * the search and the current match, and returns the next match (or `undefined`).
172
+ * Finds the next matching item from a list of values based on a search string.
174
173
  *
175
- * We normalize the search because if a user has repeatedly pressed a character,
176
- * we want the exact same behavior as if we only had that one character
177
- * (ie. cycle through options starting with that character)
174
+ * This function handles several special cases in typeahead behavior:
178
175
  *
179
- * We also reorder the values by wrapping the array around the current match.
180
- * This is so we always look forward from the current match, and picking the first
181
- * match will always be the correct one.
176
+ * 1. Space handling: When a search string ends with a space, it handles it specially:
177
+ * - If there's only one match for the text before the space, it ignores the space
178
+ * - If there are multiple matches and the current match already starts with the search prefix
179
+ * followed by a space, it keeps the current match (doesn't change selection on space)
180
+ * - Only after typing characters beyond the space will it move to a more specific match
182
181
  *
183
- * Finally, if the normalized search is exactly one character, we exclude the
184
- * current match from the values because otherwise it would be the first to match always
185
- * and focus would never move. This is as opposed to the regular case, where we
186
- * don't want focus to move if the current match still matches.
182
+ * 2. Repeated character handling: If a search consists of repeated characters (e.g., "aaa"),
183
+ * it treats it as a single character for matching purposes
184
+ *
185
+ * 3. Cycling behavior: The function wraps around the values array starting from the current match
186
+ * to find the next appropriate match, creating a cycling selection behavior
187
+ *
188
+ * @param values - Array of string values to search through (e.g., the text content of menu items)
189
+ * @param search - The current search string typed by the user
190
+ * @param currentMatch - The currently selected/matched item, if any
191
+ * @returns The next matching value that should be selected, or undefined if no match is found
187
192
  */
188
193
  export function getNextMatch(values, search, currentMatch) {
194
+ const lowerSearch = search.toLowerCase();
195
+ if (lowerSearch.endsWith(" ")) {
196
+ const searchWithoutSpace = lowerSearch.slice(0, -1);
197
+ const matchesWithoutSpace = values.filter((value) => value.toLowerCase().startsWith(searchWithoutSpace));
198
+ /**
199
+ * If there's only one match for the prefix without space, we don't
200
+ * watch to match with space.
201
+ */
202
+ if (matchesWithoutSpace.length <= 1) {
203
+ return getNextMatch(values, searchWithoutSpace, currentMatch);
204
+ }
205
+ const currentMatchLowercase = currentMatch?.toLowerCase();
206
+ /**
207
+ * If the current match already starts with the search prefix and has a space afterward,
208
+ * and the user has only typed up to that space, keep the current match until they
209
+ * disambiguate.
210
+ */
211
+ if (currentMatchLowercase &&
212
+ currentMatchLowercase.startsWith(searchWithoutSpace) &&
213
+ currentMatchLowercase.charAt(searchWithoutSpace.length) === " " &&
214
+ search.trim() === searchWithoutSpace) {
215
+ return currentMatch;
216
+ }
217
+ /**
218
+ * With multiple matches, find items that match the full search string with space
219
+ */
220
+ const spacedMatches = values.filter((value) => value.toLowerCase().startsWith(lowerSearch));
221
+ /**
222
+ * If we found matches with the space, use the first one that's not the current match
223
+ */
224
+ if (spacedMatches.length > 0) {
225
+ const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
226
+ let wrappedMatches = wrapArray(spacedMatches, Math.max(currentMatchIndex, 0));
227
+ // return the first match that is not the current one.
228
+ const nextMatch = wrappedMatches.find((match) => match !== currentMatch);
229
+ // fallback to current if no other is found.
230
+ return nextMatch || currentMatch;
231
+ }
232
+ }
189
233
  const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);
190
234
  const normalizedSearch = isRepeated ? search[0] : search;
235
+ const normalizedLowerSearch = normalizedSearch.toLowerCase();
191
236
  const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;
192
237
  let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));
193
238
  const excludeCurrentMatch = normalizedSearch.length === 1;
194
239
  if (excludeCurrentMatch)
195
240
  wrappedValues = wrappedValues.filter((v) => v !== currentMatch);
196
- const nextMatch = wrappedValues.find((value) => value?.toLowerCase().startsWith(normalizedSearch.toLowerCase()));
241
+ const nextMatch = wrappedValues.find((value) => value?.toLowerCase().startsWith(normalizedLowerSearch));
197
242
  return nextMatch !== currentMatch ? nextMatch : undefined;
198
243
  }
199
244
  /**
@@ -2,3 +2,4 @@
2
2
  * From https://github.com/melt-ui/melt-ui/blob/main/packages/svelte/src/lib/internal/math.ts
3
3
  */
4
4
  export declare function snapValueToStep(value: number, min: number, max: number, step: number): number;
5
+ export declare function linearScale(domain: [number, number], range: [number, number], clamp?: boolean): (x: number) => number;
@@ -26,3 +26,18 @@ export function snapValueToStep(value, min, max, step) {
26
26
  }
27
27
  return snappedValue;
28
28
  }
29
+ export function linearScale(domain, range, clamp = true) {
30
+ const [d0, d1] = domain;
31
+ const [r0, r1] = range;
32
+ const slope = (r1 - r0) / (d1 - d0);
33
+ return (x) => {
34
+ const result = r0 + slope * (x - d0);
35
+ if (!clamp)
36
+ return result;
37
+ if (result > Math.max(r0, r1))
38
+ return Math.max(r0, r1);
39
+ if (result < Math.min(r0, r1))
40
+ return Math.min(r0, r1);
41
+ return result;
42
+ };
43
+ }
@@ -1,12 +1,14 @@
1
+ import type { Getter } from "svelte-toolbelt";
1
2
  export type DataTypeahead = ReturnType<typeof useDataTypeahead>;
2
3
  type UseDataTypeaheadOpts = {
3
4
  onMatch: (value: string) => void;
4
5
  getCurrentItem: () => string;
6
+ candidateValues: Getter<string[]>;
5
7
  enabled: boolean;
6
8
  };
7
9
  export declare function useDataTypeahead(opts: UseDataTypeaheadOpts): {
8
10
  search: import("svelte-toolbelt").WritableBox<string>;
9
- handleTypeaheadSearch: (key: string, candidateValues: string[]) => string | undefined;
11
+ handleTypeaheadSearch: (key: string) => string | undefined;
10
12
  resetTypeahead: () => void;
11
13
  };
12
14
  export {};
@@ -3,7 +3,8 @@ import { boxAutoReset } from "./box-auto-reset.svelte.js";
3
3
  export function useDataTypeahead(opts) {
4
4
  // Reset `search` 1 second after it was last updated
5
5
  const search = boxAutoReset("", 1000);
6
- function handleTypeaheadSearch(key, candidateValues) {
6
+ const candidateValues = $derived(opts.candidateValues());
7
+ function handleTypeaheadSearch(key) {
7
8
  if (!opts.enabled)
8
9
  return;
9
10
  if (!candidateValues.length)
@@ -10,10 +10,10 @@ export function useDOMTypeahead(opts) {
10
10
  return;
11
11
  search.current = search.current + key;
12
12
  const currentItem = getCurrentItem();
13
- const currentMatch = candidates.find((item) => item === currentItem)?.textContent?.trim() ?? "";
14
- const values = candidates.map((item) => item.textContent?.trim() ?? "");
13
+ const currentMatch = candidates.find((item) => item === currentItem)?.textContent ?? "";
14
+ const values = candidates.map((item) => item.textContent ?? "");
15
15
  const nextMatch = getNextMatch(values, search.current, currentMatch);
16
- const newItem = candidates.find((item) => item.textContent?.trim() === nextMatch);
16
+ const newItem = candidates.find((item) => item.textContent === nextMatch);
17
17
  if (newItem) {
18
18
  onMatch(newItem);
19
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.3.14",
3
+ "version": "1.3.16",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",