bits-ui 2.1.0 → 2.2.1

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 (79) hide show
  1. package/dist/bits/avatar/avatar.svelte.d.ts +2 -1
  2. package/dist/bits/avatar/avatar.svelte.js +5 -3
  3. package/dist/bits/calendar/calendar.svelte.d.ts +2 -0
  4. package/dist/bits/calendar/calendar.svelte.js +9 -4
  5. package/dist/bits/combobox/components/combobox-input.svelte +2 -2
  6. package/dist/bits/combobox/components/combobox.svelte +5 -0
  7. package/dist/bits/combobox/types.d.ts +18 -1
  8. package/dist/bits/date-field/date-field.svelte.d.ts +3 -1
  9. package/dist/bits/date-field/date-field.svelte.js +15 -6
  10. package/dist/bits/date-range-field/date-range-field.svelte.d.ts +2 -0
  11. package/dist/bits/date-range-field/date-range-field.svelte.js +4 -2
  12. package/dist/bits/link-preview/link-preview.svelte.d.ts +2 -0
  13. package/dist/bits/link-preview/link-preview.svelte.js +11 -6
  14. package/dist/bits/menu/menu.svelte.d.ts +2 -0
  15. package/dist/bits/menu/menu.svelte.js +15 -10
  16. package/dist/bits/navigation-menu/navigation-menu.svelte.d.ts +3 -1
  17. package/dist/bits/navigation-menu/navigation-menu.svelte.js +21 -11
  18. package/dist/bits/pin-input/pin-input.svelte.d.ts +4 -2
  19. package/dist/bits/pin-input/pin-input.svelte.js +17 -13
  20. package/dist/bits/pin-input/usePasswordManager.svelte.d.ts +3 -2
  21. package/dist/bits/pin-input/usePasswordManager.svelte.js +6 -5
  22. package/dist/bits/range-calendar/range-calendar.svelte.d.ts +2 -0
  23. package/dist/bits/range-calendar/range-calendar.svelte.js +9 -3
  24. package/dist/bits/scroll-area/scroll-area.svelte.d.ts +2 -0
  25. package/dist/bits/scroll-area/scroll-area.svelte.js +15 -12
  26. package/dist/bits/select/components/select.svelte +6 -0
  27. package/dist/bits/select/select.svelte.d.ts +5 -1
  28. package/dist/bits/select/select.svelte.js +34 -18
  29. package/dist/bits/slider/helpers.js +33 -2
  30. package/dist/bits/time-field/time-field.svelte.d.ts +3 -1
  31. package/dist/bits/time-field/time-field.svelte.js +15 -6
  32. package/dist/bits/time-range-field/time-range-field.svelte.d.ts +2 -0
  33. package/dist/bits/time-range-field/time-range-field.svelte.js +4 -2
  34. package/dist/bits/tooltip/components/tooltip-content-static.svelte +2 -0
  35. package/dist/bits/tooltip/components/tooltip-content.svelte +2 -0
  36. package/dist/bits/tooltip/components/tooltip-trigger.svelte +1 -1
  37. package/dist/bits/tooltip/components/tooltip.svelte +1 -1
  38. package/dist/bits/tooltip/tooltip.svelte.d.ts +18 -18
  39. package/dist/bits/tooltip/tooltip.svelte.js +7 -3
  40. package/dist/bits/utilities/floating-layer/components/floating-layer-anchor.svelte +9 -6
  41. package/dist/bits/utilities/floating-layer/components/floating-layer-content.svelte +25 -21
  42. package/dist/bits/utilities/floating-layer/components/floating-layer.svelte +2 -2
  43. package/dist/bits/utilities/floating-layer/components/floating-layer.svelte.d.ts +1 -0
  44. package/dist/bits/utilities/floating-layer/types.d.ts +18 -0
  45. package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.d.ts +3 -3
  46. package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.js +16 -11
  47. package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +14 -9
  48. package/dist/bits/utilities/popper-layer/popper-layer-inner.svelte +2 -0
  49. package/dist/bits/utilities/popper-layer/types.d.ts +9 -0
  50. package/dist/bits/utilities/portal/types.d.ts +1 -1
  51. package/dist/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.d.ts +2 -0
  52. package/dist/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.js +7 -7
  53. package/dist/internal/box-auto-reset.svelte.d.ts +7 -1
  54. package/dist/internal/box-auto-reset.svelte.js +11 -6
  55. package/dist/internal/date-time/announcer.d.ts +1 -1
  56. package/dist/internal/date-time/announcer.js +20 -20
  57. package/dist/internal/date-time/calendar-helpers.svelte.js +7 -5
  58. package/dist/internal/date-time/field/helpers.d.ts +8 -2
  59. package/dist/internal/date-time/field/helpers.js +8 -7
  60. package/dist/internal/date-time/field/time-helpers.d.ts +8 -2
  61. package/dist/internal/date-time/field/time-helpers.js +9 -9
  62. package/dist/internal/dom.d.ts +0 -1
  63. package/dist/internal/dom.js +0 -3
  64. package/dist/internal/focus.d.ts +2 -2
  65. package/dist/internal/focus.js +14 -9
  66. package/dist/internal/math.d.ts +0 -4
  67. package/dist/internal/math.js +0 -28
  68. package/dist/internal/tabbable.d.ts +0 -2
  69. package/dist/internal/tabbable.js +10 -14
  70. package/dist/internal/use-data-typeahead.svelte.d.ts +1 -0
  71. package/dist/internal/use-data-typeahead.svelte.js +4 -1
  72. package/dist/internal/use-dom-typeahead.svelte.d.ts +3 -1
  73. package/dist/internal/use-dom-typeahead.svelte.js +5 -2
  74. package/dist/internal/use-grace-area.svelte.js +9 -5
  75. package/package.json +2 -2
  76. package/dist/internal/dom-context.svelte.d.ts +0 -9
  77. package/dist/internal/dom-context.svelte.js +0 -26
  78. package/dist/internal/use-size.svelte.d.ts +0 -7
  79. package/dist/internal/use-size.svelte.js +0 -54
@@ -1,3 +1,4 @@
1
+ import { DOMContext } from "svelte-toolbelt";
1
2
  import type { PinInputCell, PinInputRootProps as RootComponentProps } from "./types.js";
2
3
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
4
  import type { BitsEvent, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, WithRefProps } from "../../internal/types.js";
@@ -21,6 +22,7 @@ type PinInputRootStateProps = WithRefProps<WritableBoxedValues<{
21
22
  declare class PinInputRootState {
22
23
  #private;
23
24
  readonly opts: PinInputRootStateProps;
25
+ domContext: DOMContext;
24
26
  constructor(opts: PinInputRootStateProps);
25
27
  onkeydown: (e: BitsKeyboardEvent) => void;
26
28
  rootProps: {
@@ -75,7 +77,7 @@ declare class PinInputRootState {
75
77
  "data-pin-input-input": string;
76
78
  "data-pin-input-input-mss": number | null;
77
79
  "data-pin-input-input-mse": number | null;
78
- inputmode: "none" | "search" | "text" | "email" | "tel" | "url" | "numeric" | "decimal" | null | undefined;
80
+ inputmode: "none" | "email" | "tel" | "url" | "text" | "numeric" | "decimal" | "search" | null | undefined;
79
81
  pattern: any;
80
82
  maxlength: number;
81
83
  value: string;
@@ -111,7 +113,7 @@ declare class PinInputCellState {
111
113
  readonly "data-inactive": "" | undefined;
112
114
  };
113
115
  }
114
- export declare function syncTimeouts(cb: (...args: any[]) => unknown): number[];
116
+ export declare function syncTimeouts(cb: (...args: any[]) => unknown, domContext: DOMContext): number[];
115
117
  export declare function usePinInput(props: PinInputRootStateProps): PinInputRootState;
116
118
  export declare function usePinInputCell(props: PinInputCellStateProps): PinInputCellState;
117
119
  export {};
@@ -1,6 +1,6 @@
1
1
  import { Previous, watch } from "runed";
2
2
  import { onMount } from "svelte";
3
- import { box, attachRef } from "svelte-toolbelt";
3
+ import { box, attachRef, DOMContext } from "svelte-toolbelt";
4
4
  import { usePasswordManagerBadge } from "./usePasswordManager.svelte.js";
5
5
  import { getDisabled } from "../../internal/attrs.js";
6
6
  import { on } from "svelte/events";
@@ -47,8 +47,10 @@ class PinInputRootState {
47
47
  });
48
48
  #pwmb;
49
49
  #initialLoad;
50
+ domContext;
50
51
  constructor(opts) {
51
52
  this.opts = opts;
53
+ this.domContext = new DOMContext(opts.ref);
52
54
  this.#initialLoad = {
53
55
  value: this.opts.value,
54
56
  isIOS: typeof window !== "undefined" &&
@@ -59,6 +61,7 @@ class PinInputRootState {
59
61
  inputRef: this.#inputRef,
60
62
  isFocused: this.#isFocused,
61
63
  pushPasswordManagerStrategy: this.opts.pushPasswordManagerStrategy,
64
+ domContext: this.domContext,
62
65
  });
63
66
  onMount(() => {
64
67
  const input = this.#inputRef.current;
@@ -73,14 +76,14 @@ class PinInputRootState {
73
76
  input.selectionEnd,
74
77
  input.selectionDirection ?? "none",
75
78
  ];
76
- const unsub = on(document, "selectionchange", this.#onDocumentSelectionChange, {
79
+ const unsub = on(this.domContext.getDocument(), "selectionchange", this.#onDocumentSelectionChange, {
77
80
  capture: true,
78
81
  });
79
82
  this.#onDocumentSelectionChange();
80
- if (document.activeElement === input) {
83
+ if (this.domContext.getActiveElement() === input) {
81
84
  this.#isFocused.current = true;
82
85
  }
83
- if (!document.getElementById("pin-input-style")) {
86
+ if (!this.domContext.getElementById("pin-input-style")) {
84
87
  this.#applyStyles();
85
88
  }
86
89
  const updateRootHeight = () => {
@@ -112,7 +115,7 @@ class PinInputRootState {
112
115
  this.#mirrorSelectionEnd = end;
113
116
  this.#prevInputMetadata.prev = [start, end, dir];
114
117
  }
115
- });
118
+ }, this.domContext);
116
119
  });
117
120
  $effect(() => {
118
121
  // invoke `onComplete` when the input is completely filled.
@@ -186,9 +189,10 @@ class PinInputRootState {
186
189
  fontVariantNumeric: "tabular-nums",
187
190
  }));
188
191
  #applyStyles() {
189
- const styleEl = document.createElement("style");
192
+ const doc = this.domContext.getDocument();
193
+ const styleEl = doc.createElement("style");
190
194
  styleEl.id = "pin-input-style";
191
- document.head.appendChild(styleEl);
195
+ doc.head.appendChild(styleEl);
192
196
  if (styleEl.sheet) {
193
197
  const autoFillStyles = "background: transparent !important; color: transparent !important; border-color: transparent !important; opacity: 0 !important; box-shadow: none !important; -webkit-box-shadow: none !important; -webkit-text-fill-color: transparent !important;";
194
198
  safeInsertRule(styleEl.sheet, "[data-pin-input-input]::selection { background: transparent !important; color: transparent !important; }");
@@ -205,7 +209,7 @@ class PinInputRootState {
205
209
  const container = this.opts.ref.current;
206
210
  if (!input || !container)
207
211
  return;
208
- if (document.activeElement !== input) {
212
+ if (this.domContext.getActiveElement() !== input) {
209
213
  this.#mirrorSelectionStart = null;
210
214
  this.#mirrorSelectionEnd = null;
211
215
  return;
@@ -272,7 +276,7 @@ class PinInputRootState {
272
276
  // selectionchange event, we'll have to dispatch it manually.
273
277
  // NOTE: The following line also triggers when cmd+A then pasting
274
278
  // a value with smaller length, which is not ideal for performance.
275
- document.dispatchEvent(new Event("selectionchange"));
279
+ this.domContext.getDocument().dispatchEvent(new Event("selectionchange"));
276
280
  }
277
281
  this.opts.value.current = newValue;
278
282
  };
@@ -397,10 +401,10 @@ class PinInputCellState {
397
401
  }));
398
402
  }
399
403
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
400
- export function syncTimeouts(cb) {
401
- const t1 = setTimeout(cb, 0); // For faster machines
402
- const t2 = setTimeout(cb, 1_0);
403
- const t3 = setTimeout(cb, 5_0);
404
+ export function syncTimeouts(cb, domContext) {
405
+ const t1 = domContext.setTimeout(cb, 0); // For faster machines
406
+ const t2 = domContext.setTimeout(cb, 1_0);
407
+ const t3 = domContext.setTimeout(cb, 5_0);
404
408
  return [t1, t2, t3];
405
409
  }
406
410
  function safeInsertRule(sheet, rule) {
@@ -1,12 +1,13 @@
1
- import type { ReadableBox, WritableBox } from "svelte-toolbelt";
1
+ import { type DOMContext, type ReadableBox, type WritableBox } from "svelte-toolbelt";
2
2
  import type { PinInputRootPropsWithoutHTML } from "./types.js";
3
3
  type UsePasswordManagerBadgeProps = {
4
4
  containerRef: WritableBox<HTMLElement | null>;
5
5
  inputRef: WritableBox<HTMLInputElement | null>;
6
6
  pushPasswordManagerStrategy: ReadableBox<PinInputRootPropsWithoutHTML["pushPasswordManagerStrategy"]>;
7
7
  isFocused: ReadableBox<boolean>;
8
+ domContext: DOMContext;
8
9
  };
9
- export declare function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, }: UsePasswordManagerBadgeProps): {
10
+ export declare function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, domContext, }: UsePasswordManagerBadgeProps): {
10
11
  readonly hasPwmBadge: boolean;
11
12
  readonly willPushPwmBadge: boolean;
12
13
  PWM_BADGE_SPACE_WIDTH: "40px";
@@ -1,3 +1,4 @@
1
+ import { getWindow } from "svelte-toolbelt";
1
2
  const PWM_BADGE_MARGIN_RIGHT = 18;
2
3
  const PWM_BADGE_SPACE_WIDTH_PX = 40;
3
4
  const PWM_BADGE_SPACE_WIDTH = `${PWM_BADGE_SPACE_WIDTH_PX}px`;
@@ -7,7 +8,7 @@ const PASSWORD_MANAGER_SELECTORS = [
7
8
  "[data-dashlanecreated]", // Dashlane,
8
9
  '[style$="2147483647 !important;"]', // Bitwarden
9
10
  ].join(",");
10
- export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, }) {
11
+ export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, domContext, }) {
11
12
  let hasPwmBadge = $state(false);
12
13
  let hasPwmBadgeSpace = $state(false);
13
14
  let done = $state(false);
@@ -31,11 +32,11 @@ export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordMa
31
32
  const x = rightCornerX - PWM_BADGE_MARGIN_RIGHT;
32
33
  const y = centeredY;
33
34
  // do an extra search to check for all the password manager badges
34
- const passwordManagerStrategy = document.querySelectorAll(PASSWORD_MANAGER_SELECTORS);
35
+ const passwordManagerStrategy = domContext.querySelectorAll(PASSWORD_MANAGER_SELECTORS);
35
36
  // if no password manager is detected, dispatch document.elementFromPoint to
36
37
  // identify the badges
37
38
  if (passwordManagerStrategy.length === 0) {
38
- const maybeBadgeEl = document.elementFromPoint(x, y);
39
+ const maybeBadgeEl = domContext.getDocument().elementFromPoint(x, y);
39
40
  // if the found element is the container,
40
41
  // then it is not a badge, most times there is no badge in this case
41
42
  if (maybeBadgeEl === container)
@@ -50,7 +51,7 @@ export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordMa
50
51
  return;
51
52
  // check if the pwm area is fully visible
52
53
  function checkHasSpace() {
53
- const viewportWidth = window.innerWidth;
54
+ const viewportWidth = getWindow(container).innerWidth;
54
55
  const distanceToRightEdge = viewportWidth - container.getBoundingClientRect().right;
55
56
  hasPwmBadgeSpace = distanceToRightEdge >= PWM_BADGE_SPACE_WIDTH_PX;
56
57
  }
@@ -61,7 +62,7 @@ export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordMa
61
62
  };
62
63
  });
63
64
  $effect(() => {
64
- const focused = isFocused.current || document.activeElement === inputRef.current;
65
+ const focused = isFocused.current || domContext.getActiveElement() === inputRef.current;
65
66
  if (pushPasswordManagerStrategy.current === "none" || !focused)
66
67
  return;
67
68
  const t1 = setTimeout(trackPwmBadge, 0);
@@ -1,4 +1,5 @@
1
1
  import { type DateValue } from "@internationalized/date";
2
+ import { DOMContext } from "svelte-toolbelt";
2
3
  import type { DateRange, Month } from "../../shared/index.js";
3
4
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
4
5
  import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, WithRefProps } from "../../internal/types.js";
@@ -45,6 +46,7 @@ export declare class RangeCalendarRootState {
45
46
  accessibleHeadingId: string;
46
47
  focusedValue: DateValue | undefined;
47
48
  lastPressedDateValue: DateValue | undefined;
49
+ domContext: DOMContext;
48
50
  constructor(opts: RangeCalendarRootStateProps);
49
51
  setMonths: (months: Month<DateValue>[]) => void;
50
52
  /**
@@ -1,5 +1,5 @@
1
1
  import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date";
2
- import { attachRef } from "svelte-toolbelt";
2
+ import { attachRef, DOMContext } from "svelte-toolbelt";
3
3
  import { Context, watch } from "runed";
4
4
  import { CalendarRootContext } from "../calendar/calendar.svelte.js";
5
5
  import { useId } from "../../internal/use-id.js";
@@ -8,6 +8,7 @@ import { getAnnouncer } from "../../internal/date-time/announcer.js";
8
8
  import { createFormatter } from "../../internal/date-time/formatter.js";
9
9
  import { createMonths, getCalendarElementProps, getCalendarHeadingValue, getIsNextButtonDisabled, getIsPrevButtonDisabled, getWeekdays, handleCalendarKeydown, handleCalendarNextPage, handleCalendarPrevPage, shiftCalendarFocus, useEnsureNonDisabledPlaceholder, useMonthViewOptionsSync, useMonthViewPlaceholderSync, } from "../../internal/date-time/calendar-helpers.svelte.js";
10
10
  import { areAllDaysBetweenValid, getDateValueType, isAfter, isBefore, isBetweenInclusive, toDate, } from "../../internal/date-time/utils.js";
11
+ import { onMount } from "svelte";
11
12
  export class RangeCalendarRootState {
12
13
  opts;
13
14
  months = $state([]);
@@ -17,9 +18,11 @@ export class RangeCalendarRootState {
17
18
  accessibleHeadingId = useId();
18
19
  focusedValue = $state(undefined);
19
20
  lastPressedDateValue = undefined;
21
+ domContext;
20
22
  constructor(opts) {
21
23
  this.opts = opts;
22
- this.announcer = getAnnouncer();
24
+ this.domContext = new DOMContext(opts.ref);
25
+ this.announcer = getAnnouncer(null);
23
26
  this.formatter = createFormatter(this.opts.locale.current);
24
27
  this.months = createMonths({
25
28
  dateObj: this.opts.placeholder.current,
@@ -33,6 +36,9 @@ export class RangeCalendarRootState {
33
36
  return;
34
37
  this.formatter.setLocale(this.opts.locale.current);
35
38
  });
39
+ onMount(() => {
40
+ this.announcer = getAnnouncer(this.domContext.getDocument());
41
+ });
36
42
  /**
37
43
  * Updates the displayed months based on changes in the placeholder values,
38
44
  * which determines the month to show in the calendar.
@@ -63,7 +69,7 @@ export class RangeCalendarRootState {
63
69
  * changes.
64
70
  */
65
71
  $effect(() => {
66
- const node = document.getElementById(this.accessibleHeadingId);
72
+ const node = this.domContext.getElementById(this.accessibleHeadingId);
67
73
  if (!node)
68
74
  return;
69
75
  node.textContent = this.fullCalendarLabel;
@@ -5,6 +5,7 @@
5
5
  * Incredible thought must have went into solving all the intricacies of this component.
6
6
  */
7
7
  import { Context } from "runed";
8
+ import { DOMContext } from "svelte-toolbelt";
8
9
  import type { ScrollAreaType } from "./types.js";
9
10
  import type { ReadableBoxedValues } from "../../internal/box.svelte.js";
10
11
  import type { BitsPointerEvent, WithRefProps } from "../../internal/types.js";
@@ -34,6 +35,7 @@ declare class ScrollAreaRootState {
34
35
  cornerHeight: number;
35
36
  scrollbarXEnabled: boolean;
36
37
  scrollbarYEnabled: boolean;
38
+ domContext: DOMContext;
37
39
  constructor(opts: ScrollAreaRootStateProps);
38
40
  props: {
39
41
  readonly id: string;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { Context, useDebounce } from "runed";
8
8
  import { untrack } from "svelte";
9
- import { box, executeCallbacks, attachRef } from "svelte-toolbelt";
9
+ import { box, executeCallbacks, attachRef, DOMContext, getWindow } from "svelte-toolbelt";
10
10
  import { addEventListener } from "../../internal/events.js";
11
11
  import { mergeProps, useId } from "../../shared/index.js";
12
12
  import { useStateMachine } from "../../internal/use-state-machine.svelte.js";
@@ -29,8 +29,10 @@ class ScrollAreaRootState {
29
29
  cornerHeight = $state(0);
30
30
  scrollbarXEnabled = $state(false);
31
31
  scrollbarYEnabled = $state(false);
32
+ domContext;
32
33
  constructor(opts) {
33
34
  this.opts = opts;
35
+ this.domContext = new DOMContext(opts.ref);
34
36
  }
35
37
  props = $derived.by(() => ({
36
38
  id: this.opts.id.current,
@@ -110,13 +112,13 @@ class ScrollAreaScrollbarHoverState {
110
112
  if (!scrollAreaNode)
111
113
  return;
112
114
  const handlePointerEnter = () => {
113
- window.clearTimeout(hideTimer);
115
+ this.root.domContext.clearTimeout(hideTimer);
114
116
  untrack(() => (this.isVisible = true));
115
117
  };
116
118
  const handlePointerLeave = () => {
117
119
  if (hideTimer)
118
- window.clearTimeout(hideTimer);
119
- hideTimer = window.setTimeout(() => {
120
+ this.root.domContext.clearTimeout(hideTimer);
121
+ hideTimer = this.root.domContext.setTimeout(() => {
120
122
  untrack(() => {
121
123
  this.scrollbar.hasThumb = false;
122
124
  this.isVisible = false;
@@ -125,7 +127,7 @@ class ScrollAreaScrollbarHoverState {
125
127
  };
126
128
  const unsubListeners = executeCallbacks(on(scrollAreaNode, "pointerenter", handlePointerEnter), on(scrollAreaNode, "pointerleave", handlePointerLeave));
127
129
  return () => {
128
- window.clearTimeout(hideTimer);
130
+ this.root.domContext.getWindow().clearTimeout(hideTimer);
129
131
  unsubListeners();
130
132
  };
131
133
  });
@@ -164,8 +166,8 @@ class ScrollAreaScrollbarScrollState {
164
166
  const _state = this.machine.state.current;
165
167
  const scrollHideDelay = this.root.opts.scrollHideDelay.current;
166
168
  if (_state === "idle") {
167
- const hideTimer = window.setTimeout(() => this.machine.dispatch("HIDE"), scrollHideDelay);
168
- return () => window.clearTimeout(hideTimer);
169
+ const hideTimer = this.root.domContext.setTimeout(() => this.machine.dispatch("HIDE"), scrollHideDelay);
170
+ return () => this.root.domContext.clearTimeout(hideTimer);
169
171
  }
170
172
  });
171
173
  $effect(() => {
@@ -538,8 +540,8 @@ class ScrollAreaScrollbarSharedState {
538
540
  this.rect = this.scrollbar.opts.ref.current?.getBoundingClientRect() ?? null;
539
541
  // pointer capture doesn't prevent text selection in Safari
540
542
  // so we remove text selection manually when scrolling
541
- this.prevWebkitUserSelect = document.body.style.webkitUserSelect;
542
- document.body.style.webkitUserSelect = "none";
543
+ this.prevWebkitUserSelect = this.root.domContext.getDocument().body.style.webkitUserSelect;
544
+ this.root.domContext.getDocument().body.style.webkitUserSelect = "none";
543
545
  if (this.root.viewportNode)
544
546
  this.root.viewportNode.style.scrollBehavior = "auto";
545
547
  this.handleDragScroll(e);
@@ -552,7 +554,7 @@ class ScrollAreaScrollbarSharedState {
552
554
  if (target.hasPointerCapture(e.pointerId)) {
553
555
  target.releasePointerCapture(e.pointerId);
554
556
  }
555
- document.body.style.webkitUserSelect = this.prevWebkitUserSelect;
557
+ this.root.domContext.getDocument().body.style.webkitUserSelect = this.prevWebkitUserSelect;
556
558
  if (this.root.viewportNode)
557
559
  this.root.viewportNode.style.scrollBehavior = "";
558
560
  this.rect = null;
@@ -755,6 +757,7 @@ function isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos) {
755
757
  function addUnlinkedScrollListener(node, handler) {
756
758
  let prevPosition = { left: node.scrollLeft, top: node.scrollTop };
757
759
  let rAF = 0;
760
+ const win = getWindow(node);
758
761
  (function loop() {
759
762
  const position = { left: node.scrollLeft, top: node.scrollTop };
760
763
  const isHorizontalScroll = prevPosition.left !== position.left;
@@ -762,7 +765,7 @@ function addUnlinkedScrollListener(node, handler) {
762
765
  if (isHorizontalScroll || isVerticalScroll)
763
766
  handler();
764
767
  prevPosition = position;
765
- rAF = window.requestAnimationFrame(loop);
768
+ rAF = win.requestAnimationFrame(loop);
766
769
  })();
767
- return () => window.cancelAnimationFrame(rAF);
770
+ return () => win.cancelAnimationFrame(rAF);
768
771
  }
@@ -38,6 +38,8 @@
38
38
  }
39
39
  );
40
40
 
41
+ let inputValue = $state("");
42
+
41
43
  const rootState = useSelectRoot({
42
44
  type,
43
45
  value: box.with(
@@ -63,6 +65,10 @@
63
65
  isCombobox: false,
64
66
  items: box.with(() => items),
65
67
  allowDeselect: box.with(() => allowDeselect),
68
+ inputValue: box.with(
69
+ () => inputValue,
70
+ (v) => (inputValue = v)
71
+ ),
66
72
  });
67
73
  </script>
68
74
 
@@ -1,4 +1,5 @@
1
1
  import { Previous } from "runed";
2
+ import { DOMContext } from "svelte-toolbelt";
2
3
  import type { Box, ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
4
  import type { BitsEvent, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
4
5
  export declare const INTERACTION_KEYS: string[];
@@ -21,13 +22,13 @@ type SelectBaseRootStateProps = ReadableBoxedValues<{
21
22
  allowDeselect: boolean;
22
23
  }> & WritableBoxedValues<{
23
24
  open: boolean;
25
+ inputValue: string;
24
26
  }> & {
25
27
  isCombobox: boolean;
26
28
  };
27
29
  declare class SelectBaseRootState {
28
30
  readonly opts: SelectBaseRootStateProps;
29
31
  touchedInput: boolean;
30
- inputValue: string;
31
32
  inputNode: HTMLElement | null;
32
33
  contentNode: HTMLElement | null;
33
34
  triggerNode: HTMLElement | null;
@@ -39,6 +40,7 @@ declare class SelectBaseRootState {
39
40
  isUsingKeyboard: boolean;
40
41
  isCombobox: boolean;
41
42
  bitsAttrs: SelectBitsAttrs;
43
+ domContext: DOMContext;
42
44
  constructor(opts: SelectBaseRootStateProps);
43
45
  setHighlightedNode(node: HTMLElement | null, initial?: boolean): void;
44
46
  getCandidateNodes(): HTMLElement[];
@@ -156,6 +158,7 @@ declare class SelectContentState {
156
158
  readonly root: SelectRootState;
157
159
  viewportNode: HTMLElement | null;
158
160
  isPositioned: boolean;
161
+ domContext: DOMContext;
159
162
  constructor(opts: SelectContentStateProps, root: SelectRootState);
160
163
  onpointermove(_: BitsPointerEvent): void;
161
164
  onInteractOutside: (e: PointerEvent) => void;
@@ -379,6 +382,7 @@ type InitSelectProps = {
379
382
  allowDeselect: boolean;
380
383
  }> & WritableBoxedValues<{
381
384
  open: boolean;
385
+ inputValue: string;
382
386
  }> & {
383
387
  isCombobox: boolean;
384
388
  };
@@ -1,5 +1,5 @@
1
1
  import { Context, Previous, watch } from "runed";
2
- import { afterSleep, afterTick, onDestroyEffect, attachRef } from "svelte-toolbelt";
2
+ import { afterSleep, afterTick, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
3
3
  import { on } from "svelte/events";
4
4
  import { backward, forward, next, prev } from "../../internal/arrays.js";
5
5
  import { getAriaExpanded, getAriaHidden, getDataDisabled, getDataOpenClosed, getDisabled, getRequired, } from "../../internal/attrs.js";
@@ -18,7 +18,6 @@ export const CONTENT_MARGIN = 10;
18
18
  class SelectBaseRootState {
19
19
  opts;
20
20
  touchedInput = $state(false);
21
- inputValue = $state("");
22
21
  inputNode = $state(null);
23
22
  contentNode = $state(null);
24
23
  triggerNode = $state(null);
@@ -42,6 +41,7 @@ class SelectBaseRootState {
42
41
  isUsingKeyboard = false;
43
42
  isCombobox = false;
44
43
  bitsAttrs;
44
+ domContext = new DOMContext(() => null);
45
45
  constructor(opts) {
46
46
  this.opts = opts;
47
47
  this.isCombobox = opts.isCombobox;
@@ -134,11 +134,12 @@ class SelectSingleRootState extends SelectBaseRootState {
134
134
  }
135
135
  toggleItem(itemValue, itemLabel = itemValue) {
136
136
  this.opts.value.current = this.includesItem(itemValue) ? "" : itemValue;
137
- this.inputValue = itemLabel;
137
+ this.opts.inputValue.current = itemLabel;
138
138
  }
139
139
  setInitialHighlightedNode() {
140
140
  afterTick(() => {
141
- if (this.highlightedNode && document.contains(this.highlightedNode))
141
+ if (this.highlightedNode &&
142
+ this.domContext.getDocument().contains(this.highlightedNode))
142
143
  return;
143
144
  if (this.opts.value.current !== "") {
144
145
  const node = this.getNodeByValue(this.opts.value.current);
@@ -183,11 +184,14 @@ class SelectMultipleRootState extends SelectBaseRootState {
183
184
  else {
184
185
  this.opts.value.current = [...this.opts.value.current, itemValue];
185
186
  }
186
- this.inputValue = itemLabel;
187
+ this.opts.inputValue.current = itemLabel;
187
188
  }
188
189
  setInitialHighlightedNode() {
189
190
  afterTick(() => {
190
- if (this.highlightedNode && document.contains(this.highlightedNode))
191
+ if (!this.domContext)
192
+ return;
193
+ if (this.highlightedNode &&
194
+ this.domContext.getDocument().contains(this.highlightedNode))
191
195
  return;
192
196
  if (this.opts.value.current.length && this.opts.value.current[0] !== "") {
193
197
  const node = this.getNodeByValue(this.opts.value.current[0]);
@@ -210,6 +214,7 @@ class SelectInputState {
210
214
  constructor(opts, root) {
211
215
  this.opts = opts;
212
216
  this.root = root;
217
+ this.root.domContext = new DOMContext(opts.ref);
213
218
  this.onkeydown = this.onkeydown.bind(this);
214
219
  this.oninput = this.oninput.bind(this);
215
220
  watch([() => this.root.opts.value.current, () => this.opts.clearOnDeselect.current], ([value, clearOnDeselect], [prevValue]) => {
@@ -217,11 +222,11 @@ class SelectInputState {
217
222
  return;
218
223
  if (Array.isArray(value) && Array.isArray(prevValue)) {
219
224
  if (value.length === 0 && prevValue.length !== 0) {
220
- this.root.inputValue = "";
225
+ this.root.opts.inputValue.current = "";
221
226
  }
222
227
  }
223
228
  else if (value === "" && prevValue !== "") {
224
- this.root.inputValue = "";
229
+ this.root.opts.inputValue.current = "";
225
230
  }
226
231
  });
227
232
  }
@@ -237,7 +242,7 @@ class SelectInputState {
237
242
  return;
238
243
  if (e.key === kbd.TAB)
239
244
  return;
240
- if (e.key === kbd.BACKSPACE && this.root.inputValue === "")
245
+ if (e.key === kbd.BACKSPACE && this.root.opts.inputValue.current === "")
241
246
  return;
242
247
  this.root.handleOpen();
243
248
  // we need to wait for a tick after the menu opens to ensure the highlighted nodes are
@@ -319,7 +324,7 @@ class SelectInputState {
319
324
  }
320
325
  }
321
326
  oninput(e) {
322
- this.root.inputValue = e.currentTarget.value;
327
+ this.root.opts.inputValue.current = e.currentTarget.value;
323
328
  this.root.setHighlightedToFirstCandidate();
324
329
  }
325
330
  props = $derived.by(() => ({
@@ -347,9 +352,11 @@ class SelectComboTriggerState {
347
352
  this.onpointerdown = this.onpointerdown.bind(this);
348
353
  }
349
354
  onkeydown(e) {
355
+ if (!this.root.domContext)
356
+ return;
350
357
  if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
351
358
  e.preventDefault();
352
- if (document.activeElement !== this.root.inputNode) {
359
+ if (this.root.domContext.getActiveElement() !== this.root.inputNode) {
353
360
  this.root.inputNode?.focus();
354
361
  }
355
362
  this.root.toggleMenu();
@@ -360,10 +367,10 @@ class SelectComboTriggerState {
360
367
  * behavior of focusing the button and keep focus on the input.
361
368
  */
362
369
  onpointerdown(e) {
363
- if (this.root.opts.disabled.current)
370
+ if (this.root.opts.disabled.current || !this.root.domContext)
364
371
  return;
365
372
  e.preventDefault();
366
- if (document.activeElement !== this.root.inputNode) {
373
+ if (this.root.domContext.getActiveElement() !== this.root.inputNode) {
367
374
  this.root.inputNode?.focus();
368
375
  }
369
376
  this.root.toggleMenu();
@@ -388,11 +395,14 @@ class SelectTriggerState {
388
395
  constructor(opts, root) {
389
396
  this.opts = opts;
390
397
  this.root = root;
398
+ this.root.domContext = new DOMContext(opts.ref);
391
399
  this.#domTypeahead = useDOMTypeahead({
392
400
  getCurrentItem: () => this.root.highlightedNode,
393
401
  onMatch: (node) => {
394
402
  this.root.setHighlightedNode(node);
395
403
  },
404
+ getActiveElement: () => this.root.domContext.getActiveElement(),
405
+ getWindow: () => this.root.domContext.getWindow(),
396
406
  });
397
407
  this.#dataTypeahead = useDataTypeahead({
398
408
  getCurrentItem: () => {
@@ -412,6 +422,7 @@ class SelectTriggerState {
412
422
  },
413
423
  enabled: !this.root.isMulti && this.root.dataTypeaheadEnabled,
414
424
  candidateValues: () => (this.root.isMulti ? [] : this.root.candidateLabels),
425
+ getWindow: () => this.root.domContext.getWindow(),
415
426
  });
416
427
  this.onkeydown = this.onkeydown.bind(this);
417
428
  this.onpointerdown = this.onpointerdown.bind(this);
@@ -614,9 +625,14 @@ class SelectContentState {
614
625
  root;
615
626
  viewportNode = $state(null);
616
627
  isPositioned = $state(false);
628
+ domContext;
617
629
  constructor(opts, root) {
618
630
  this.opts = opts;
619
631
  this.root = root;
632
+ this.domContext = new DOMContext(this.opts.ref);
633
+ if (this.root.domContext === null) {
634
+ this.root.domContext = this.domContext;
635
+ }
620
636
  onDestroyEffect(() => {
621
637
  this.root.contentNode = null;
622
638
  this.isPositioned = false;
@@ -924,16 +940,16 @@ class SelectScrollButtonImplState {
924
940
  this.onpointerleave = this.onpointerleave.bind(this);
925
941
  }
926
942
  handleUserScroll() {
927
- window.clearTimeout(this.userScrollTimer);
943
+ this.content.domContext.clearTimeout(this.userScrollTimer);
928
944
  this.isUserScrolling = true;
929
- this.userScrollTimer = window.setTimeout(() => {
945
+ this.userScrollTimer = this.content.domContext.setTimeout(() => {
930
946
  this.isUserScrolling = false;
931
947
  }, 200);
932
948
  }
933
949
  clearAutoScrollInterval() {
934
950
  if (this.autoScrollTimer === null)
935
951
  return;
936
- window.clearTimeout(this.autoScrollTimer);
952
+ this.content.domContext.clearTimeout(this.autoScrollTimer);
937
953
  this.autoScrollTimer = null;
938
954
  }
939
955
  onpointerdown(_) {
@@ -941,9 +957,9 @@ class SelectScrollButtonImplState {
941
957
  return;
942
958
  const autoScroll = (tick) => {
943
959
  this.onAutoScroll();
944
- this.autoScrollTimer = window.setTimeout(() => autoScroll(tick + 1), this.opts.delay.current(tick));
960
+ this.autoScrollTimer = this.content.domContext.setTimeout(() => autoScroll(tick + 1), this.opts.delay.current(tick));
945
961
  };
946
- this.autoScrollTimer = window.setTimeout(() => autoScroll(1), this.opts.delay.current(0));
962
+ this.autoScrollTimer = this.content.domContext.setTimeout(() => autoScroll(1), this.opts.delay.current(0));
947
963
  }
948
964
  onpointermove(e) {
949
965
  this.onpointerdown(e);
@@ -132,6 +132,29 @@ export function getThumbLabelStyles(direction, thumbPosition, labelPosition = "t
132
132
  }
133
133
  return style;
134
134
  }
135
+ /**
136
+ * Gets the number of decimal places in a number
137
+ */
138
+ function getDecimalPlaces(num) {
139
+ if (Math.floor(num) === num)
140
+ return 0;
141
+ const str = num.toString();
142
+ if (str.indexOf(".") !== -1 && str.indexOf("e-") === -1) {
143
+ return str.split(".")[1].length;
144
+ }
145
+ else if (str.indexOf("e-") !== -1) {
146
+ const parts = str.split("e-");
147
+ return parseInt(parts[1], 10);
148
+ }
149
+ return 0;
150
+ }
151
+ /**
152
+ * Rounds a number to the specified number of decimal places
153
+ */
154
+ function roundToPrecision(num, precision) {
155
+ const factor = Math.pow(10, precision);
156
+ return Math.round(num * factor) / factor;
157
+ }
135
158
  /**
136
159
  * Normalizes step to always be a sorted array of valid values within min/max range
137
160
  */
@@ -140,13 +163,21 @@ export function normalizeSteps(step, min, max) {
140
163
  // generate regular steps - match original behavior exactly
141
164
  const difference = max - min;
142
165
  let count = Math.ceil(difference / step);
143
- if (difference % step === 0) {
166
+ // Get precision from step to avoid floating point errors
167
+ const precision = getDecimalPlaces(step);
168
+ // Check if difference is divisible by step using integer arithmetic to avoid floating point errors
169
+ const factor = Math.pow(10, precision);
170
+ const intDifference = Math.round(difference * factor);
171
+ const intStep = Math.round(step * factor);
172
+ if (intDifference % intStep === 0) {
144
173
  count++;
145
174
  }
146
175
  const steps = [];
147
176
  for (let i = 0; i < count; i++) {
148
177
  const value = min + i * step;
149
- steps.push(value);
178
+ // Round to the precision of the step to avoid floating point errors
179
+ const roundedValue = roundToPrecision(value, precision);
180
+ steps.push(roundedValue);
150
181
  }
151
182
  return steps;
152
183
  }