bits-ui 2.1.0 → 2.2.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 (66) 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/time-field/time-field.svelte.d.ts +3 -1
  30. package/dist/bits/time-field/time-field.svelte.js +15 -6
  31. package/dist/bits/time-range-field/time-range-field.svelte.d.ts +2 -0
  32. package/dist/bits/time-range-field/time-range-field.svelte.js +4 -2
  33. package/dist/bits/tooltip/tooltip.svelte.d.ts +2 -0
  34. package/dist/bits/tooltip/tooltip.svelte.js +4 -2
  35. package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.js +3 -2
  36. package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +14 -9
  37. package/dist/bits/utilities/portal/types.d.ts +1 -1
  38. package/dist/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.d.ts +2 -0
  39. package/dist/bits/utilities/text-selection-layer/use-text-selection-layer.svelte.js +7 -7
  40. package/dist/internal/box-auto-reset.svelte.d.ts +7 -1
  41. package/dist/internal/box-auto-reset.svelte.js +11 -6
  42. package/dist/internal/date-time/announcer.d.ts +1 -1
  43. package/dist/internal/date-time/announcer.js +20 -20
  44. package/dist/internal/date-time/calendar-helpers.svelte.js +7 -5
  45. package/dist/internal/date-time/field/helpers.d.ts +8 -2
  46. package/dist/internal/date-time/field/helpers.js +8 -7
  47. package/dist/internal/date-time/field/time-helpers.d.ts +8 -2
  48. package/dist/internal/date-time/field/time-helpers.js +9 -9
  49. package/dist/internal/dom.d.ts +0 -1
  50. package/dist/internal/dom.js +0 -3
  51. package/dist/internal/focus.d.ts +2 -2
  52. package/dist/internal/focus.js +14 -9
  53. package/dist/internal/math.d.ts +0 -4
  54. package/dist/internal/math.js +0 -28
  55. package/dist/internal/tabbable.d.ts +0 -2
  56. package/dist/internal/tabbable.js +10 -14
  57. package/dist/internal/use-data-typeahead.svelte.d.ts +1 -0
  58. package/dist/internal/use-data-typeahead.svelte.js +4 -1
  59. package/dist/internal/use-dom-typeahead.svelte.d.ts +3 -1
  60. package/dist/internal/use-dom-typeahead.svelte.js +5 -2
  61. package/dist/internal/use-grace-area.svelte.js +9 -5
  62. package/package.json +2 -2
  63. package/dist/internal/dom-context.svelte.d.ts +0 -9
  64. package/dist/internal/dom-context.svelte.js +0 -26
  65. package/dist/internal/use-size.svelte.d.ts +0 -7
  66. package/dist/internal/use-size.svelte.js +0 -54
@@ -1,4 +1,4 @@
1
- import { type ReadableBox, type WritableBox } from "svelte-toolbelt";
1
+ import { DOMContext, type ReadableBox, type WritableBox } from "svelte-toolbelt";
2
2
  import type { HTMLImgAttributes } from "svelte/elements";
3
3
  import type { AvatarImageLoadingStatus } from "./types.js";
4
4
  import type { ReadableBoxedValues } from "../../internal/box.svelte.js";
@@ -15,6 +15,7 @@ type AvatarRootStateProps = WithRefProps<{
15
15
  type AvatarImageSrc = string | null | undefined;
16
16
  declare class AvatarRootState {
17
17
  readonly opts: AvatarRootStateProps;
18
+ readonly domContext: DOMContext;
18
19
  constructor(opts: AvatarRootStateProps);
19
20
  loadImage(src: string, crossorigin?: CrossOrigin, referrerPolicy?: ReferrerPolicy): (() => void) | undefined;
20
21
  props: {
@@ -1,13 +1,15 @@
1
1
  import { untrack } from "svelte";
2
- import { attachRef } from "svelte-toolbelt";
2
+ import { DOMContext, attachRef } from "svelte-toolbelt";
3
3
  import { Context } from "runed";
4
4
  const AVATAR_ROOT_ATTR = "data-avatar-root";
5
5
  const AVATAR_IMAGE_ATTR = "data-avatar-image";
6
6
  const AVATAR_FALLBACK_ATTR = "data-avatar-fallback";
7
7
  class AvatarRootState {
8
8
  opts;
9
+ domContext;
9
10
  constructor(opts) {
10
11
  this.opts = opts;
12
+ this.domContext = new DOMContext(this.opts.ref);
11
13
  this.loadImage = this.loadImage.bind(this);
12
14
  }
13
15
  loadImage(src, crossorigin, referrerPolicy) {
@@ -22,7 +24,7 @@ class AvatarRootState {
22
24
  image.referrerPolicy = referrerPolicy;
23
25
  this.opts.loadingStatus.current = "loading";
24
26
  image.onload = () => {
25
- imageTimerId = window.setTimeout(() => {
27
+ imageTimerId = this.domContext.setTimeout(() => {
26
28
  this.opts.loadingStatus.current = "loaded";
27
29
  }, this.opts.delayMs.current);
28
30
  };
@@ -30,7 +32,7 @@ class AvatarRootState {
30
32
  this.opts.loadingStatus.current = "error";
31
33
  };
32
34
  return () => {
33
- window.clearTimeout(imageTimerId);
35
+ this.domContext.clearTimeout(imageTimerId);
34
36
  };
35
37
  }
36
38
  props = $derived.by(() => ({
@@ -1,4 +1,5 @@
1
1
  import { type DateValue } from "@internationalized/date";
2
+ import { DOMContext } from "svelte-toolbelt";
2
3
  import { Context } from "runed";
3
4
  import type { RangeCalendarRootState } from "../range-calendar/range-calendar.svelte.js";
4
5
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
@@ -45,6 +46,7 @@ export declare class CalendarRootState {
45
46
  announcer: Announcer;
46
47
  formatter: Formatter;
47
48
  accessibleHeadingId: string;
49
+ domContext: DOMContext;
48
50
  constructor(opts: CalendarRootStateProps);
49
51
  setMonths(months: Month<DateValue>[]): void;
50
52
  /**
@@ -1,7 +1,7 @@
1
1
  import { getLocalTimeZone, isSameDay, isSameMonth, isToday, } from "@internationalized/date";
2
2
  import { DEV } from "esm-env";
3
- import { untrack } from "svelte";
4
- import { attachRef } from "svelte-toolbelt";
3
+ import { onMount, untrack } from "svelte";
4
+ import { attachRef, DOMContext } from "svelte-toolbelt";
5
5
  import { Context, watch } from "runed";
6
6
  import { getAriaDisabled, getAriaHidden, getAriaReadonly, getAriaSelected, getDataDisabled, getDataReadonly, getDataSelected, getDataUnavailable, } from "../../internal/attrs.js";
7
7
  import { useId } from "../../internal/use-id.js";
@@ -16,9 +16,11 @@ export class CalendarRootState {
16
16
  announcer;
17
17
  formatter;
18
18
  accessibleHeadingId = useId();
19
+ domContext;
19
20
  constructor(opts) {
20
21
  this.opts = opts;
21
- this.announcer = getAnnouncer();
22
+ this.domContext = new DOMContext(opts.ref);
23
+ this.announcer = getAnnouncer(null);
22
24
  this.formatter = createFormatter(this.opts.locale.current);
23
25
  this.setMonths = this.setMonths.bind(this);
24
26
  this.nextPage = this.nextPage.bind(this);
@@ -36,6 +38,9 @@ export class CalendarRootState {
36
38
  this.handleSingleUpdate = this.handleSingleUpdate.bind(this);
37
39
  this.onkeydown = this.onkeydown.bind(this);
38
40
  this.getBitsAttr = this.getBitsAttr.bind(this);
41
+ onMount(() => {
42
+ this.announcer = getAnnouncer(this.domContext.getDocument());
43
+ });
39
44
  this.months = createMonths({
40
45
  dateObj: this.opts.placeholder.current,
41
46
  weekStartsOn: this.opts.weekStartsOn.current,
@@ -75,7 +80,7 @@ export class CalendarRootState {
75
80
  * changes.
76
81
  */
77
82
  $effect(() => {
78
- const node = document.getElementById(this.accessibleHeadingId);
83
+ const node = this.domContext.getElementById(this.accessibleHeadingId);
79
84
  if (!node)
80
85
  return;
81
86
  node.textContent = this.fullCalendarLabel;
@@ -24,11 +24,11 @@
24
24
  });
25
25
 
26
26
  if (defaultValue) {
27
- inputState.root.inputValue = defaultValue;
27
+ inputState.root.opts.inputValue.current = defaultValue;
28
28
  }
29
29
 
30
30
  const mergedProps = $derived(
31
- mergeProps(restProps, inputState.props, { value: inputState.root.inputValue })
31
+ mergeProps(restProps, inputState.props, { value: inputState.root.opts.inputValue.current })
32
32
  );
33
33
  </script>
34
34
 
@@ -20,6 +20,7 @@
20
20
  required = false,
21
21
  items = [],
22
22
  allowDeselect = true,
23
+ inputValue = "",
23
24
  children,
24
25
  }: ComboboxRootProps = $props();
25
26
 
@@ -61,6 +62,10 @@
61
62
  isCombobox: true,
62
63
  items: box.with(() => items),
63
64
  allowDeselect: box.with(() => allowDeselect),
65
+ inputValue: box.with(
66
+ () => inputValue,
67
+ (v) => (inputValue = v)
68
+ ),
64
69
  });
65
70
  </script>
66
71
 
@@ -1,6 +1,23 @@
1
1
  import type { BitsPrimitiveInputAttributes } from "../../shared/attributes.js";
2
+ import type { SelectBaseRootPropsWithoutHTML, SelectMultipleRootPropsWithoutHTML, SelectSingleRootPropsWithoutHTML } from "../select/types.js";
2
3
  import type { WithChild, Without } from "../../internal/types.js";
3
- export type { SelectBaseRootPropsWithoutHTML as ComboboxBaseRootPropsWithoutHTML, SelectContentProps as ComboboxContentProps, SelectContentPropsWithoutHTML as ComboboxContentPropsWithoutHTML, SelectContentStaticProps as ComboboxContentStaticProps, SelectContentStaticPropsWithoutHTML as ComboboxContentStaticPropsWithoutHTML, SelectItemProps as ComboboxItemProps, SelectItemPropsWithoutHTML as ComboboxItemPropsWithoutHTML, SelectItemSnippetProps as ComboboxItemSnippetProps, SelectMultipleRootProps as ComboboxMultipleRootProps, SelectMultipleRootPropsWithoutHTML as ComboboxMultipleRootPropsWithoutHTML, SelectRootProps as ComboboxRootProps, SelectRootPropsWithoutHTML as ComboboxRootPropsWithoutHTML, SelectSingleRootProps as ComboboxSingleRootProps, SelectSingleRootPropsWithoutHTML as ComboboxSingleRootPropsWithoutHTML, SelectTriggerProps as ComboboxTriggerProps, SelectTriggerPropsWithoutHTML as ComboboxTriggerPropsWithoutHTML, SelectGroupPropsWithoutHTML as ComboboxGroupPropsWithoutHTML, SelectGroupProps as ComboboxGroupProps, SelectGroupHeadingPropsWithoutHTML as ComboboxGroupHeadingPropsWithoutHTML, SelectGroupHeadingProps as ComboboxGroupHeadingProps, SelectViewportPropsWithoutHTML as ComboboxViewportPropsWithoutHTML, SelectViewportProps as ComboboxViewportProps, SelectScrollDownButtonProps as ComboboxScrollDownButtonProps, SelectScrollDownButtonPropsWithoutHTML as ComboboxScrollDownButtonPropsWithoutHTML, SelectScrollUpButtonProps as ComboboxScrollUpButtonProps, SelectScrollUpButtonPropsWithoutHTML as ComboboxScrollUpButtonPropsWithoutHTML, SelectArrowProps as ComboboxArrowProps, SelectArrowPropsWithoutHTML as ComboboxArrowPropsWithoutHTML, SelectPortalProps as ComboboxPortalProps, SelectPortalPropsWithoutHTML as ComboboxPortalPropsWithoutHTML, } from "../select/types.js";
4
+ export type ComboboxBaseRootPropsWithoutHTML = SelectBaseRootPropsWithoutHTML & {
5
+ /**
6
+ * A read-only value that can be used to programmatically
7
+ * update the input value.
8
+ *
9
+ * This is useful for updating the displayed label/input
10
+ * when the value changes outside of Bits UI.
11
+ */
12
+ inputValue?: string;
13
+ };
14
+ export type ComboboxSingleRootPropsWithoutHTML = ComboboxBaseRootPropsWithoutHTML & SelectSingleRootPropsWithoutHTML;
15
+ export type ComboboxSingleRootProps = ComboboxSingleRootPropsWithoutHTML;
16
+ export type ComboboxMultipleRootPropsWithoutHTML = ComboboxBaseRootPropsWithoutHTML & SelectMultipleRootPropsWithoutHTML;
17
+ export type ComboboxMultipleRootProps = ComboboxMultipleRootPropsWithoutHTML;
18
+ export type ComboboxRootPropsWithoutHTML = ComboboxBaseRootPropsWithoutHTML & (ComboboxSingleRootPropsWithoutHTML | ComboboxMultipleRootPropsWithoutHTML);
19
+ export type ComboboxRootProps = ComboboxRootPropsWithoutHTML;
20
+ export type { SelectContentProps as ComboboxContentProps, SelectContentPropsWithoutHTML as ComboboxContentPropsWithoutHTML, SelectContentStaticProps as ComboboxContentStaticProps, SelectContentStaticPropsWithoutHTML as ComboboxContentStaticPropsWithoutHTML, SelectItemProps as ComboboxItemProps, SelectItemPropsWithoutHTML as ComboboxItemPropsWithoutHTML, SelectItemSnippetProps as ComboboxItemSnippetProps, SelectTriggerProps as ComboboxTriggerProps, SelectTriggerPropsWithoutHTML as ComboboxTriggerPropsWithoutHTML, SelectGroupPropsWithoutHTML as ComboboxGroupPropsWithoutHTML, SelectGroupProps as ComboboxGroupProps, SelectGroupHeadingPropsWithoutHTML as ComboboxGroupHeadingPropsWithoutHTML, SelectGroupHeadingProps as ComboboxGroupHeadingProps, SelectViewportPropsWithoutHTML as ComboboxViewportPropsWithoutHTML, SelectViewportProps as ComboboxViewportProps, SelectScrollDownButtonProps as ComboboxScrollDownButtonProps, SelectScrollDownButtonPropsWithoutHTML as ComboboxScrollDownButtonPropsWithoutHTML, SelectScrollUpButtonProps as ComboboxScrollUpButtonProps, SelectScrollUpButtonPropsWithoutHTML as ComboboxScrollUpButtonPropsWithoutHTML, SelectArrowProps as ComboboxArrowProps, SelectArrowPropsWithoutHTML as ComboboxArrowPropsWithoutHTML, SelectPortalProps as ComboboxPortalProps, SelectPortalPropsWithoutHTML as ComboboxPortalPropsWithoutHTML, } from "../select/types.js";
4
21
  export type ComboboxInputPropsWithoutHTML = WithChild<{
5
22
  /**
6
23
  * The default value of the input. This is not a reactive prop and is only used to populate
@@ -1,6 +1,6 @@
1
1
  import type { Updater } from "svelte/store";
2
2
  import type { DateValue } from "@internationalized/date";
3
- import { type WritableBox } from "svelte-toolbelt";
3
+ import { type WritableBox, DOMContext } from "svelte-toolbelt";
4
4
  import type { DateRangeFieldRootState } from "../date-range-field/date-range-field.svelte.js";
5
5
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
6
6
  import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, WithRefProps } from "../../internal/types.js";
@@ -74,6 +74,7 @@ export declare class DateFieldRootState {
74
74
  dayPeriodNode: HTMLElement | null;
75
75
  rangeRoot: DateRangeFieldRootState | undefined;
76
76
  name: string;
77
+ domContext: DOMContext;
77
78
  constructor(props: DateFieldRootStateProps, rangeRoot?: DateRangeFieldRootState);
78
79
  setName(name: string): void;
79
80
  /**
@@ -165,6 +166,7 @@ export declare class DateFieldInputState {
165
166
  #private;
166
167
  readonly opts: DateFieldInputStateProps;
167
168
  readonly root: DateFieldRootState;
169
+ readonly domContext: DOMContext;
168
170
  constructor(opts: DateFieldInputStateProps, root: DateFieldRootState);
169
171
  props: {
170
172
  readonly id: string;
@@ -1,4 +1,4 @@
1
- import { box, onDestroyEffect, attachRef } from "svelte-toolbelt";
1
+ import { box, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
2
2
  import { onMount, untrack } from "svelte";
3
3
  import { Context, watch } from "runed";
4
4
  import { getAriaDisabled, getAriaHidden, getAriaInvalid, getAriaReadonly, getDataDisabled, getDataInvalid, getDataReadonly, } from "../../internal/attrs.js";
@@ -99,6 +99,7 @@ export class DateFieldRootState {
99
99
  dayPeriodNode = $state(null);
100
100
  rangeRoot = undefined;
101
101
  name = $state("");
102
+ domContext = new DOMContext(() => null);
102
103
  constructor(props, rangeRoot) {
103
104
  this.rangeRoot = rangeRoot;
104
105
  /**
@@ -127,7 +128,7 @@ export class DateFieldRootState {
127
128
  this.formatter = createFormatter(this.locale.current);
128
129
  this.initialSegments = initializeSegmentValues(this.inferredGranularity);
129
130
  this.segmentValues = this.initialSegments;
130
- this.announcer = getAnnouncer();
131
+ this.announcer = getAnnouncer(null);
131
132
  this.getFieldNode = this.getFieldNode.bind(this);
132
133
  this.updateSegment = this.updateSegment.bind(this);
133
134
  this.handleSegmentClick = this.handleSegmentClick.bind(this);
@@ -138,12 +139,12 @@ export class DateFieldRootState {
138
139
  });
139
140
  });
140
141
  onMount(() => {
141
- this.announcer = getAnnouncer();
142
+ this.announcer = getAnnouncer(this.domContext.getDocument());
142
143
  });
143
144
  onDestroyEffect(() => {
144
145
  if (rangeRoot)
145
146
  return;
146
- removeDescriptionElement(this.descriptionId);
147
+ removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
147
148
  });
148
149
  $effect(() => {
149
150
  if (rangeRoot)
@@ -157,7 +158,12 @@ export class DateFieldRootState {
157
158
  return;
158
159
  if (this.value.current) {
159
160
  const descriptionId = untrack(() => this.descriptionId);
160
- setDescription(descriptionId, this.formatter, this.value.current);
161
+ setDescription({
162
+ id: descriptionId,
163
+ formatter: this.formatter,
164
+ value: this.value.current,
165
+ doc: this.domContext.getDocument(),
166
+ });
161
167
  }
162
168
  const placeholder = untrack(() => this.placeholder.current);
163
169
  if (this.value.current && placeholder !== this.value.current) {
@@ -553,9 +559,12 @@ export class DateFieldRootState {
553
559
  export class DateFieldInputState {
554
560
  opts;
555
561
  root;
562
+ domContext;
556
563
  constructor(opts, root) {
557
564
  this.opts = opts;
558
565
  this.root = root;
566
+ this.domContext = new DOMContext(opts.ref);
567
+ this.root.domContext = this.domContext;
559
568
  $effect(() => {
560
569
  this.root.setName(this.opts.name.current);
561
570
  });
@@ -563,7 +572,7 @@ export class DateFieldInputState {
563
572
  #ariaDescribedBy = $derived.by(() => {
564
573
  if (!isBrowser)
565
574
  return undefined;
566
- const doesDescriptionExist = document.getElementById(this.root.descriptionId);
575
+ const doesDescriptionExist = this.domContext.getElementById(this.root.descriptionId);
567
576
  if (!doesDescriptionExist)
568
577
  return undefined;
569
578
  return this.root.descriptionId;
@@ -1,4 +1,5 @@
1
1
  import type { DateValue } from "@internationalized/date";
2
+ import { DOMContext } from "svelte-toolbelt";
2
3
  import { Context } from "runed";
3
4
  import type { DateFieldRootState } from "../date-field/date-field.svelte.js";
4
5
  import { DateFieldInputState } from "../date-field/date-field.svelte.js";
@@ -41,6 +42,7 @@ export declare class DateRangeFieldRootState {
41
42
  startValueComplete: boolean;
42
43
  endValueComplete: boolean;
43
44
  rangeComplete: boolean;
45
+ domContext: DOMContext;
44
46
  constructor(opts: DateRangeFieldRootStateProps);
45
47
  validationStatus: false | {
46
48
  readonly reason: "custom";
@@ -1,4 +1,4 @@
1
- import { box, onDestroyEffect, attachRef } from "svelte-toolbelt";
1
+ import { box, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
2
2
  import { Context, watch } from "runed";
3
3
  import { DateFieldInputState, useDateFieldRoot } from "../date-field/date-field.svelte.js";
4
4
  import { useId } from "../../internal/use-id.js";
@@ -21,11 +21,13 @@ export class DateRangeFieldRootState {
21
21
  startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
22
22
  endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
23
23
  rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
24
+ domContext;
24
25
  constructor(opts) {
25
26
  this.opts = opts;
26
27
  this.formatter = createFormatter(this.opts.locale.current);
28
+ this.domContext = new DOMContext(this.opts.ref);
27
29
  onDestroyEffect(() => {
28
- removeDescriptionElement(this.descriptionId);
30
+ removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
29
31
  });
30
32
  $effect(() => {
31
33
  if (this.formatter.getLocale() === this.opts.locale.current)
@@ -1,3 +1,4 @@
1
+ import { DOMContext } from "svelte-toolbelt";
1
2
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
2
3
  import type { BitsFocusEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
3
4
  type LinkPreviewRootStateProps = WritableBoxedValues<{
@@ -16,6 +17,7 @@ declare class LinkPreviewRootState {
16
17
  contentMounted: boolean;
17
18
  triggerNode: HTMLElement | null;
18
19
  isOpening: boolean;
20
+ domContext: DOMContext;
19
21
  constructor(opts: LinkPreviewRootStateProps);
20
22
  clearTimeout(): void;
21
23
  handleOpen(): void;
@@ -1,4 +1,4 @@
1
- import { afterSleep, onDestroyEffect, attachRef } from "svelte-toolbelt";
1
+ import { afterSleep, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
2
2
  import { Context, watch } from "runed";
3
3
  import { on } from "svelte/events";
4
4
  import { getAriaExpanded, getDataOpenClosed } from "../../internal/attrs.js";
@@ -17,6 +17,7 @@ class LinkPreviewRootState {
17
17
  contentMounted = $state(false);
18
18
  triggerNode = $state(null);
19
19
  isOpening = false;
20
+ domContext = new DOMContext(() => null);
20
21
  constructor(opts) {
21
22
  this.opts = opts;
22
23
  watch(() => this.opts.open.current, (isOpen) => {
@@ -24,11 +25,13 @@ class LinkPreviewRootState {
24
25
  this.hasSelection = false;
25
26
  return;
26
27
  }
28
+ if (!this.domContext)
29
+ return;
27
30
  const handlePointerUp = () => {
28
31
  this.containsSelection = false;
29
32
  this.isPointerDownOnContent = false;
30
33
  afterSleep(1, () => {
31
- const isSelection = document.getSelection()?.toString() !== "";
34
+ const isSelection = this.domContext.getDocument().getSelection()?.toString() !== "";
32
35
  if (isSelection) {
33
36
  this.hasSelection = true;
34
37
  }
@@ -37,7 +40,7 @@ class LinkPreviewRootState {
37
40
  }
38
41
  });
39
42
  };
40
- const unsubListener = on(document, "pointerup", handlePointerUp);
43
+ const unsubListener = on(this.domContext.getDocument(), "pointerup", handlePointerUp);
41
44
  if (!this.contentNode)
42
45
  return;
43
46
  const tabCandidates = getTabbableCandidates(this.contentNode);
@@ -53,7 +56,7 @@ class LinkPreviewRootState {
53
56
  }
54
57
  clearTimeout() {
55
58
  if (this.timeout) {
56
- window.clearTimeout(this.timeout);
59
+ this.domContext.clearTimeout(this.timeout);
57
60
  this.timeout = null;
58
61
  }
59
62
  }
@@ -62,7 +65,7 @@ class LinkPreviewRootState {
62
65
  if (this.opts.open.current)
63
66
  return;
64
67
  this.isOpening = true;
65
- this.timeout = window.setTimeout(() => {
68
+ this.timeout = this.domContext.setTimeout(() => {
66
69
  if (this.isOpening) {
67
70
  this.opts.open.current = true;
68
71
  this.isOpening = false;
@@ -78,7 +81,7 @@ class LinkPreviewRootState {
78
81
  this.isOpening = false;
79
82
  this.clearTimeout();
80
83
  if (!this.isPointerDownOnContent && !this.hasSelection) {
81
- this.timeout = window.setTimeout(() => {
84
+ this.timeout = this.domContext.setTimeout(() => {
82
85
  this.opts.open.current = false;
83
86
  }, this.opts.closeDelay.current);
84
87
  }
@@ -90,6 +93,7 @@ class LinkPreviewTriggerState {
90
93
  constructor(opts, root) {
91
94
  this.opts = opts;
92
95
  this.root = root;
96
+ this.root.domContext = new DOMContext(opts.ref);
93
97
  this.onpointerenter = this.onpointerenter.bind(this);
94
98
  this.onpointerleave = this.onpointerleave.bind(this);
95
99
  this.onfocus = this.onfocus.bind(this);
@@ -136,6 +140,7 @@ class LinkPreviewContentState {
136
140
  constructor(opts, root) {
137
141
  this.opts = opts;
138
142
  this.root = root;
143
+ this.root.domContext = new DOMContext(opts.ref);
139
144
  this.onpointerdown = this.onpointerdown.bind(this);
140
145
  this.onpointerenter = this.onpointerenter.bind(this);
141
146
  this.onfocusout = this.onfocusout.bind(this);
@@ -1,3 +1,4 @@
1
+ import { DOMContext } from "svelte-toolbelt";
1
2
  import { Context } from "runed";
2
3
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
4
  import { CustomEventDispatcher } from "../../internal/events.js";
@@ -52,6 +53,7 @@ declare class MenuContentState {
52
53
  search: string;
53
54
  rovingFocusGroup: ReturnType<typeof useRovingFocus>;
54
55
  mounted: boolean;
56
+ domContext: DOMContext;
55
57
  constructor(opts: MenuContentStateProps, parentMenu: MenuMenuState);
56
58
  onCloseAutoFocus: (e: Event) => void;
57
59
  handleTabKeyDown(e: BitsKeyboardEvent): void;
@@ -1,4 +1,4 @@
1
- import { afterTick, box, mergeProps, onDestroyEffect, attachRef } from "svelte-toolbelt";
1
+ import { afterTick, box, mergeProps, onDestroyEffect, attachRef, DOMContext, getWindow, } from "svelte-toolbelt";
2
2
  import { Context, watch } from "runed";
3
3
  import { FIRST_LAST_KEYS, LAST_KEYS, SELECTION_KEYS, SUB_OPEN_KEYS, getCheckedState, isMouseEvent, } from "./utils.js";
4
4
  import { focusFirst } from "../../internal/focus.js";
@@ -75,9 +75,11 @@ class MenuContentState {
75
75
  rovingFocusGroup;
76
76
  mounted = $state(false);
77
77
  #isSub;
78
+ domContext;
78
79
  constructor(opts, parentMenu) {
79
80
  this.opts = opts;
80
81
  this.parentMenu = parentMenu;
82
+ this.domContext = new DOMContext(opts.ref);
81
83
  parentMenu.contentId = opts.id;
82
84
  this.#isSub = opts.isSub ?? false;
83
85
  this.onkeydown = this.onkeydown.bind(this);
@@ -96,7 +98,10 @@ class MenuContentState {
96
98
  this.parentMenu.root.isPointerInTransit = value;
97
99
  },
98
100
  });
99
- this.#handleTypeaheadSearch = useDOMTypeahead().handleTypeaheadSearch;
101
+ this.#handleTypeaheadSearch = useDOMTypeahead({
102
+ getActiveElement: () => this.domContext.getActiveElement(),
103
+ getWindow: () => this.domContext.getWindow(),
104
+ }).handleTypeaheadSearch;
100
105
  this.rovingFocusGroup = useRovingFocus({
101
106
  rootNode: box.with(() => this.parentMenu.contentNode),
102
107
  candidateAttr: this.parentMenu.root.getAttr("item"),
@@ -117,7 +122,7 @@ class MenuContentState {
117
122
  });
118
123
  $effect(() => {
119
124
  if (!this.parentMenu.opts.open.current) {
120
- window.clearTimeout(this.#timer);
125
+ this.domContext.getWindow().clearTimeout(this.#timer);
121
126
  }
122
127
  });
123
128
  }
@@ -174,7 +179,7 @@ class MenuContentState {
174
179
  });
175
180
  }
176
181
  else {
177
- document.body.focus();
182
+ this.domContext.getDocument().body.focus();
178
183
  }
179
184
  }
180
185
  onkeydown(e) {
@@ -213,7 +218,7 @@ class MenuContentState {
213
218
  if (LAST_KEYS.includes(e.key)) {
214
219
  candidateNodes.reverse();
215
220
  }
216
- focusFirst(candidateNodes);
221
+ focusFirst(candidateNodes, { select: false }, () => this.domContext.getActiveElement());
217
222
  }
218
223
  onblur(e) {
219
224
  if (!isElement(e.currentTarget))
@@ -222,7 +227,7 @@ class MenuContentState {
222
227
  return;
223
228
  // clear search buffer when leaving the menu
224
229
  if (!e.currentTarget.contains?.(e.target)) {
225
- window.clearTimeout(this.#timer);
230
+ this.domContext.getWindow().clearTimeout(this.#timer);
226
231
  this.search = "";
227
232
  }
228
233
  }
@@ -452,7 +457,7 @@ class MenuSubTriggerState {
452
457
  #clearOpenTimer() {
453
458
  if (this.#openTimer === null)
454
459
  return;
455
- window.clearTimeout(this.#openTimer);
460
+ this.content.domContext.getWindow().clearTimeout(this.#openTimer);
456
461
  this.#openTimer = null;
457
462
  }
458
463
  onpointermove(e) {
@@ -462,7 +467,7 @@ class MenuSubTriggerState {
462
467
  !this.submenu.opts.open.current &&
463
468
  !this.#openTimer &&
464
469
  !this.content.parentMenu.root.isPointerInTransit) {
465
- this.#openTimer = window.setTimeout(() => {
470
+ this.#openTimer = this.content.domContext.setTimeout(() => {
466
471
  this.submenu.onOpen();
467
472
  this.#clearOpenTimer();
468
473
  }, 100);
@@ -757,7 +762,7 @@ class ContextMenuTriggerState {
757
762
  #clearLongPressTimer() {
758
763
  if (this.#longPressTimer === null)
759
764
  return;
760
- window.clearTimeout(this.#longPressTimer);
765
+ getWindow(this.opts.ref.current).clearTimeout(this.#longPressTimer);
761
766
  }
762
767
  #handleOpen(e) {
763
768
  this.#point = { x: e.clientX, y: e.clientY };
@@ -775,7 +780,7 @@ class ContextMenuTriggerState {
775
780
  if (this.opts.disabled.current || isMouseEvent(e))
776
781
  return;
777
782
  this.#clearLongPressTimer();
778
- this.#longPressTimer = window.setTimeout(() => this.#handleOpen(e), 700);
783
+ this.#longPressTimer = getWindow(this.opts.ref.current).setTimeout(() => this.#handleOpen(e), 700);
779
784
  }
780
785
  onpointermove(e) {
781
786
  if (this.opts.disabled.current || isMouseEvent(e))
@@ -2,7 +2,7 @@
2
2
  * Based on Radix UI's Navigation Menu
3
3
  * https://www.radix-ui.com/docs/primitives/components/navigation-menu
4
4
  */
5
- import { type AnyFn, type ReadableBox, type ReadableBoxedValues, type WithRefProps, type WritableBox, type WritableBoxedValues } from "svelte-toolbelt";
5
+ import { type AnyFn, type ReadableBox, type ReadableBoxedValues, type WithRefProps, type WritableBox, type WritableBoxedValues, DOMContext } from "svelte-toolbelt";
6
6
  import { Context } from "runed";
7
7
  import { type Snippet } from "svelte";
8
8
  import { SvelteMap } from "svelte/reactivity";
@@ -125,6 +125,7 @@ export declare class NavigationMenuItemState {
125
125
  props: Record<string, unknown>;
126
126
  }]> | undefined>;
127
127
  contentProps: ReadableBox<Record<string, unknown>>;
128
+ domContext: DOMContext;
128
129
  constructor(opts: NavigationMenuItemStateProps, listContext: NavigationMenuListState);
129
130
  onEntryKeydown: (side?: "start" | "end") => void;
130
131
  onFocusProxyEnter: (side?: "start" | "end") => void;
@@ -294,6 +295,7 @@ declare class NavigationMenuContentImplState {
294
295
  listContext: NavigationMenuListState;
295
296
  prevMotionAttribute: MotionAttribute | null;
296
297
  motionAttribute: MotionAttribute | null;
298
+ domContext: DOMContext;
297
299
  constructor(opts: NavigationMenuContentImplStateProps, itemContext: NavigationMenuItemState);
298
300
  onFocusOutside: (e: Event) => void;
299
301
  onInteractOutside: (e: PointerEvent) => void;
@@ -2,7 +2,7 @@
2
2
  * Based on Radix UI's Navigation Menu
3
3
  * https://www.radix-ui.com/docs/primitives/components/navigation-menu
4
4
  */
5
- import { afterSleep, afterTick, box, attachRef, } from "svelte-toolbelt";
5
+ import { afterSleep, afterTick, box, attachRef, DOMContext, getWindow, } from "svelte-toolbelt";
6
6
  import { Context, useDebounce, watch } from "runed";
7
7
  import { untrack } from "svelte";
8
8
  import { SvelteMap } from "svelte/reactivity";
@@ -71,7 +71,10 @@ class NavigationMenuRootState {
71
71
  });
72
72
  constructor(opts) {
73
73
  this.opts = opts;
74
- this.isDelaySkipped = boxAutoReset(false, this.opts.skipDelayDuration.current);
74
+ this.isDelaySkipped = boxAutoReset(false, {
75
+ afterMs: this.opts.skipDelayDuration.current,
76
+ getWindow: () => getWindow(opts.ref.current),
77
+ });
75
78
  this.provider = useNavigationMenuProvider({
76
79
  value: this.opts.value,
77
80
  previousValue: this.previousValue,
@@ -224,9 +227,11 @@ export class NavigationMenuItemState {
224
227
  contentChildren = box(undefined);
225
228
  contentChild = box(undefined);
226
229
  contentProps = box({});
230
+ domContext;
227
231
  constructor(opts, listContext) {
228
232
  this.opts = opts;
229
233
  this.listContext = listContext;
234
+ this.domContext = new DOMContext(opts.ref);
230
235
  }
231
236
  #handleContentEntry = (side = "start") => {
232
237
  if (!this.contentNode)
@@ -234,7 +239,7 @@ export class NavigationMenuItemState {
234
239
  this.restoreContentTabOrder();
235
240
  const candidates = getTabbableCandidates(this.contentNode);
236
241
  if (candidates.length)
237
- focusFirst(side === "start" ? candidates : candidates.reverse());
242
+ focusFirst(side === "start" ? candidates : candidates.reverse(), () => this.domContext.getActiveElement());
238
243
  };
239
244
  #handleContentExit = () => {
240
245
  if (!this.contentNode)
@@ -265,7 +270,10 @@ class NavigationMenuTriggerState {
265
270
  focusProxyMounted = $state(false);
266
271
  constructor(opts, context) {
267
272
  this.opts = opts;
268
- this.hasPointerMoveOpened = boxAutoReset(false, 300);
273
+ this.hasPointerMoveOpened = boxAutoReset(false, {
274
+ afterMs: 300,
275
+ getWindow: () => getWindow(opts.ref.current),
276
+ });
269
277
  this.context = context.provider;
270
278
  this.itemContext = context.item;
271
279
  this.listContext = context.list;
@@ -565,11 +573,13 @@ class NavigationMenuContentImplState {
565
573
  untrack(() => (this.prevMotionAttribute = attribute));
566
574
  return attribute;
567
575
  });
576
+ domContext;
568
577
  constructor(opts, itemContext) {
569
578
  this.opts = opts;
570
579
  this.itemContext = itemContext;
571
580
  this.listContext = itemContext.listContext;
572
581
  this.context = itemContext.listContext.context;
582
+ this.domContext = new DOMContext(opts.ref);
573
583
  watch([
574
584
  () => this.itemContext.opts.value.current,
575
585
  () => this.itemContext.triggerNode,
@@ -581,7 +591,7 @@ class NavigationMenuContentImplState {
581
591
  const handleClose = () => {
582
592
  this.context.onItemDismiss();
583
593
  this.itemContext.onRootContentClose();
584
- if (content.contains(document.activeElement)) {
594
+ if (content.contains(this.domContext.getActiveElement())) {
585
595
  this.itemContext.triggerNode?.focus();
586
596
  }
587
597
  };
@@ -629,13 +639,13 @@ class NavigationMenuContentImplState {
629
639
  const isTabKey = e.key === kbd.TAB && !isMetaKey;
630
640
  const candidates = getTabbableCandidates(e.currentTarget);
631
641
  if (isTabKey) {
632
- const focusedElement = document.activeElement;
642
+ const focusedElement = this.domContext.getActiveElement();
633
643
  const index = candidates.findIndex((candidate) => candidate === focusedElement);
634
644
  const isMovingBackwards = e.shiftKey;
635
645
  const nextCandidates = isMovingBackwards
636
646
  ? candidates.slice(0, index).reverse()
637
647
  : candidates.slice(index + 1, candidates.length);
638
- if (focusFirst(nextCandidates)) {
648
+ if (focusFirst(nextCandidates, () => this.domContext.getActiveElement())) {
639
649
  // prevent browser tab keydown because we've handled focus
640
650
  e.preventDefault();
641
651
  return;
@@ -648,7 +658,7 @@ class NavigationMenuContentImplState {
648
658
  return;
649
659
  }
650
660
  }
651
- let activeEl = document.activeElement;
661
+ let activeEl = this.domContext.getActiveElement();
652
662
  if (this.itemContext.contentNode) {
653
663
  const focusedNode = this.itemContext.contentNode.querySelector("[data-focused]");
654
664
  if (focusedNode) {
@@ -798,14 +808,14 @@ export function useNavigationMenuIndicator() {
798
808
  return new NavigationMenuIndicatorState(NavigationMenuProviderContext.get());
799
809
  }
800
810
  //
801
- function focusFirst(candidates) {
802
- const previouslyFocusedElement = document.activeElement;
811
+ function focusFirst(candidates, getActiveElement) {
812
+ const previouslyFocusedElement = getActiveElement();
803
813
  return candidates.some((candidate) => {
804
814
  // if focus is already where we want to go, we don't want to keep going through the candidates
805
815
  if (candidate === previouslyFocusedElement)
806
816
  return true;
807
817
  candidate.focus();
808
- return document.activeElement !== previouslyFocusedElement;
818
+ return getActiveElement() !== previouslyFocusedElement;
809
819
  });
810
820
  }
811
821
  function removeFromTabOrder(candidates) {