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,15 +1,16 @@
1
- import { composeHandlers, executeCallbacks } from "svelte-toolbelt";
1
+ import { DOMContext, composeHandlers, contains, executeCallbacks, } from "svelte-toolbelt";
2
2
  import { watch } from "runed";
3
3
  import { on } from "svelte/events";
4
4
  import { noop } from "../../../internal/noop.js";
5
5
  import { isHTMLElement } from "../../../internal/is.js";
6
- import { isOrContainsTarget } from "../../../internal/elements.js";
7
6
  globalThis.bitsTextSelectionLayers ??= new Map();
8
7
  export class TextSelectionLayerState {
9
8
  opts;
9
+ domContext;
10
10
  #unsubSelectionLock = noop;
11
11
  constructor(opts) {
12
12
  this.opts = opts;
13
+ this.domContext = new DOMContext(opts.ref);
13
14
  let unsubEvents = noop;
14
15
  watch(() => this.opts.enabled.current, (isEnabled) => {
15
16
  if (isEnabled) {
@@ -25,7 +26,7 @@ export class TextSelectionLayerState {
25
26
  });
26
27
  }
27
28
  #addEventListeners() {
28
- return executeCallbacks(on(document, "pointerdown", this.#pointerdown), on(document, "pointerup", composeHandlers(this.#resetSelectionLock, this.opts.onPointerUp.current)));
29
+ return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.#pointerdown), on(this.domContext.getDocument(), "pointerup", composeHandlers(this.#resetSelectionLock, this.opts.onPointerUp.current)));
29
30
  }
30
31
  #pointerdown = (e) => {
31
32
  const node = this.opts.ref.current;
@@ -37,12 +38,12 @@ export class TextSelectionLayerState {
37
38
  * pointerdown occurred inside the node. You are still allowed to select text
38
39
  * outside the node provided pointerdown occurs outside the node.
39
40
  */
40
- if (!isHighestLayer(this) || !isOrContainsTarget(node, target))
41
+ if (!isHighestLayer(this) || !contains(node, target))
41
42
  return;
42
43
  this.opts.onPointerDown.current(e);
43
44
  if (e.defaultPrevented)
44
45
  return;
45
- this.#unsubSelectionLock = preventTextSelectionOverflow(node);
46
+ this.#unsubSelectionLock = preventTextSelectionOverflow(node, this.domContext.getDocument().body);
46
47
  };
47
48
  #resetSelectionLock = () => {
48
49
  this.#unsubSelectionLock();
@@ -53,8 +54,7 @@ export function useTextSelectionLayer(props) {
53
54
  return new TextSelectionLayerState(props);
54
55
  }
55
56
  const getUserSelect = (node) => node.style.userSelect || node.style.webkitUserSelect;
56
- function preventTextSelectionOverflow(node) {
57
- const body = document.body;
57
+ function preventTextSelectionOverflow(node, body) {
58
58
  const originalBodyUserSelect = getUserSelect(body);
59
59
  const originalNodeUserSelect = getUserSelect(node);
60
60
  setUserSelect(body, "none");
@@ -1,8 +1,14 @@
1
1
  import { type WritableBox } from "svelte-toolbelt";
2
+ type BoxAutoResetOptions<T> = {
3
+ afterMs?: number;
4
+ onChange?: (value: T) => void;
5
+ getWindow: () => Window & typeof globalThis;
6
+ };
2
7
  /**
3
8
  * Creates a box which will be reset to the default value after some time.
4
9
  *
5
10
  * @param defaultValue The value which will be set.
6
11
  * @param afterMs A zero-or-greater delay in milliseconds.
7
12
  */
8
- export declare function boxAutoReset<T>(defaultValue: T, afterMs?: number, onChange?: (value: T) => void): WritableBox<T>;
13
+ export declare function boxAutoReset<T>(defaultValue: T, options: BoxAutoResetOptions<T>): WritableBox<T>;
14
+ export {};
@@ -1,31 +1,36 @@
1
1
  import { box } from "svelte-toolbelt";
2
2
  import { noop } from "./noop.js";
3
+ const defaultOptions = {
4
+ afterMs: 10000,
5
+ onChange: noop,
6
+ };
3
7
  /**
4
8
  * Creates a box which will be reset to the default value after some time.
5
9
  *
6
10
  * @param defaultValue The value which will be set.
7
11
  * @param afterMs A zero-or-greater delay in milliseconds.
8
12
  */
9
- export function boxAutoReset(defaultValue, afterMs = 10000, onChange = noop) {
13
+ export function boxAutoReset(defaultValue, options) {
14
+ const { afterMs, onChange, getWindow } = { ...defaultOptions, ...options };
10
15
  let timeout = null;
11
16
  let value = $state(defaultValue);
12
17
  function resetAfter() {
13
- return window.setTimeout(() => {
18
+ return getWindow().setTimeout(() => {
14
19
  value = defaultValue;
15
- onChange(defaultValue);
20
+ onChange?.(defaultValue);
16
21
  }, afterMs);
17
22
  }
18
23
  $effect(() => {
19
24
  return () => {
20
25
  if (timeout)
21
- clearTimeout(timeout);
26
+ getWindow().clearTimeout(timeout);
22
27
  };
23
28
  });
24
29
  return box.with(() => value, (v) => {
25
30
  value = v;
26
- onChange(v);
31
+ onChange?.(v);
27
32
  if (timeout)
28
- clearTimeout(timeout);
33
+ getWindow().clearTimeout(timeout);
29
34
  timeout = resetAfter();
30
35
  });
31
36
  }
@@ -2,6 +2,6 @@ export type Announcer = ReturnType<typeof getAnnouncer>;
2
2
  /**
3
3
  * Creates an announcer object that can be used to make `aria-live` announcements to screen readers.
4
4
  */
5
- export declare function getAnnouncer(): {
5
+ export declare function getAnnouncer(doc: Document | null): {
6
6
  announce: (value: string | null | number, kind?: "assertive" | "polite", timeout?: number) => NodeJS.Timeout | undefined;
7
7
  };
@@ -5,40 +5,40 @@ import { isBrowser, isHTMLElement } from "../is.js";
5
5
  * Within the date components, we use this to announce when the values of the individual segments
6
6
  * change, as without it we get inconsistent behavior across screen readers.
7
7
  */
8
- function initAnnouncer() {
9
- if (!isBrowser)
8
+ function initAnnouncer(doc) {
9
+ if (!isBrowser || !doc)
10
10
  return null;
11
- let el = document.querySelector("[data-bits-announcer]");
12
- if (!isHTMLElement(el)) {
13
- const div = document.createElement("div");
14
- div.style.cssText = srOnlyStylesString;
15
- div.setAttribute("data-bits-announcer", "");
16
- div.appendChild(createLog("assertive"));
17
- div.appendChild(createLog("polite"));
18
- el = div;
19
- document.body.insertBefore(el, document.body.firstChild);
20
- }
11
+ let el = doc.querySelector("[data-bits-announcer]");
21
12
  /**
22
13
  * Creates a log element for assertive or polite announcements.
23
14
  */
24
- function createLog(kind) {
25
- const log = document.createElement("div");
15
+ const createLog = (kind) => {
16
+ const log = doc.createElement("div");
26
17
  log.role = "log";
27
18
  log.ariaLive = kind;
28
19
  log.setAttribute("aria-relevant", "additions");
29
20
  return log;
21
+ };
22
+ if (!isHTMLElement(el)) {
23
+ const div = doc.createElement("div");
24
+ div.style.cssText = srOnlyStylesString;
25
+ div.setAttribute("data-bits-announcer", "");
26
+ div.appendChild(createLog("assertive"));
27
+ div.appendChild(createLog("polite"));
28
+ el = div;
29
+ doc.body.insertBefore(el, doc.body.firstChild);
30
30
  }
31
31
  /**
32
32
  * Retrieves the log element for assertive or polite announcements.
33
33
  */
34
- function getLog(kind) {
34
+ const getLog = (kind) => {
35
35
  if (!isHTMLElement(el))
36
36
  return null;
37
37
  const log = el.querySelector(`[aria-live="${kind}"]`);
38
38
  if (!isHTMLElement(log))
39
39
  return null;
40
40
  return log;
41
- }
41
+ };
42
42
  return {
43
43
  getLog,
44
44
  };
@@ -46,16 +46,16 @@ function initAnnouncer() {
46
46
  /**
47
47
  * Creates an announcer object that can be used to make `aria-live` announcements to screen readers.
48
48
  */
49
- export function getAnnouncer() {
50
- const announcer = initAnnouncer();
49
+ export function getAnnouncer(doc) {
50
+ const announcer = initAnnouncer(doc);
51
51
  /**
52
52
  * Announces a message to screen readers using the specified kind of announcement.
53
53
  */
54
54
  function announce(value, kind = "assertive", timeout = 7500) {
55
- if (!announcer || !isBrowser)
55
+ if (!announcer || !isBrowser || !doc)
56
56
  return;
57
57
  const log = announcer.getLog(kind);
58
- const content = document.createElement("div");
58
+ const content = doc.createElement("div");
59
59
  if (typeof value === "number") {
60
60
  value = value.toString();
61
61
  }
@@ -1,5 +1,5 @@
1
1
  import { endOfMonth, isSameDay, isSameMonth, startOfMonth, } from "@internationalized/date";
2
- import { afterTick, styleToString } from "svelte-toolbelt";
2
+ import { afterTick, getDocument, styleToString, } from "svelte-toolbelt";
3
3
  import { untrack } from "svelte";
4
4
  import { getDaysInMonth, getLastFirstDayOfWeek, getNextLastDayOfWeek, hasTime, isAfter, isBefore, parseAnyDateValue, parseStringToDateValue, toDate, } from "./utils.js";
5
5
  import { getDataDisabled, getDataInvalid, getDataReadonly } from "../attrs.js";
@@ -331,7 +331,8 @@ export function useMonthViewOptionsSync(props) {
331
331
  * Returns a function that removes the heading element.
332
332
  */
333
333
  export function createAccessibleHeading({ calendarNode, label, accessibleHeadingId, }) {
334
- const div = document.createElement("div");
334
+ const doc = getDocument(calendarNode);
335
+ const div = doc.createElement("div");
335
336
  div.style.cssText = styleToString({
336
337
  border: "0px",
337
338
  clip: "rect(0px, 0px, 0px, 0px)",
@@ -344,7 +345,7 @@ export function createAccessibleHeading({ calendarNode, label, accessibleHeading
344
345
  whiteSpace: "nowrap",
345
346
  width: "1px",
346
347
  });
347
- const h2 = document.createElement("div");
348
+ const h2 = doc.createElement("div");
348
349
  h2.textContent = label;
349
350
  h2.id = accessibleHeadingId;
350
351
  h2.role = "heading";
@@ -352,7 +353,7 @@ export function createAccessibleHeading({ calendarNode, label, accessibleHeading
352
353
  calendarNode.insertBefore(div, calendarNode.firstChild);
353
354
  div.appendChild(h2);
354
355
  return () => {
355
- const h2 = document.getElementById(accessibleHeadingId);
356
+ const h2 = doc.getElementById(accessibleHeadingId);
356
357
  if (!h2)
357
358
  return;
358
359
  div.parentElement?.removeChild(div);
@@ -442,7 +443,8 @@ export function getCalendarElementProps({ fullCalendarLabel, id, isInvalid, disa
442
443
  };
443
444
  }
444
445
  export function pickerOpenFocus(e) {
445
- const nodeToFocus = document.querySelector("[data-bits-day][data-focused]");
446
+ const doc = getDocument(e.target);
447
+ const nodeToFocus = doc.querySelector("[data-bits-day][data-focused]");
446
448
  if (nodeToFocus) {
447
449
  e.preventDefault();
448
450
  nodeToFocus?.focus();
@@ -61,6 +61,12 @@ export declare function isAcceptableSegmentKey(key: string): boolean;
61
61
  * @param fieldNode - The id of the date field associated with the segment
62
62
  */
63
63
  export declare function isFirstSegment(id: string, fieldNode: HTMLElement | null): boolean;
64
+ type SetDescriptionProps = {
65
+ id: string;
66
+ formatter: Formatter;
67
+ value: DateValue;
68
+ doc: Document;
69
+ };
64
70
  /**
65
71
  * Creates or updates a description element for a date field
66
72
  * which enables screen readers to read the date field's value.
@@ -69,12 +75,12 @@ export declare function isFirstSegment(id: string, fieldNode: HTMLElement | null
69
75
  * so it can be associated via `aria-describedby` and read by
70
76
  * screen readers as the user interacts with the date field.
71
77
  */
72
- export declare function setDescription(id: string, formatter: Formatter, value: DateValue): void;
78
+ export declare function setDescription(props: SetDescriptionProps): void;
73
79
  /**
74
80
  * Removes the description element for the date field with
75
81
  * the provided ID. This function should be called when the
76
82
  * date field is unmounted.
77
83
  */
78
- export declare function removeDescriptionElement(id: string): void;
84
+ export declare function removeDescriptionElement(id: string, doc: Document): void;
79
85
  export declare function getDefaultHourCycle(locale: string): 12 | 24;
80
86
  export {};
@@ -345,19 +345,20 @@ export function isFirstSegment(id, fieldNode) {
345
345
  * so it can be associated via `aria-describedby` and read by
346
346
  * screen readers as the user interacts with the date field.
347
347
  */
348
- export function setDescription(id, formatter, value) {
348
+ export function setDescription(props) {
349
+ const { id, formatter, value, doc } = props;
349
350
  if (!isBrowser)
350
351
  return;
351
352
  const valueString = formatter.selectedDate(value);
352
- const el = document.getElementById(id);
353
+ const el = doc.getElementById(id);
353
354
  if (!el) {
354
- const div = document.createElement("div");
355
+ const div = doc.createElement("div");
355
356
  div.style.cssText = styleToString({
356
357
  display: "none",
357
358
  });
358
359
  div.id = id;
359
360
  div.innerText = `Selected Date: ${valueString}`;
360
- document.body.appendChild(div);
361
+ doc.body.appendChild(div);
361
362
  }
362
363
  else {
363
364
  el.innerText = `Selected Date: ${valueString}`;
@@ -368,13 +369,13 @@ export function setDescription(id, formatter, value) {
368
369
  * the provided ID. This function should be called when the
369
370
  * date field is unmounted.
370
371
  */
371
- export function removeDescriptionElement(id) {
372
+ export function removeDescriptionElement(id, doc) {
372
373
  if (!isBrowser)
373
374
  return;
374
- const el = document.getElementById(id);
375
+ const el = doc.getElementById(id);
375
376
  if (!el)
376
377
  return;
377
- document.body.removeChild(el);
378
+ doc.body.removeChild(el);
378
379
  }
379
380
  export function getDefaultHourCycle(locale) {
380
381
  const formatter = new Intl.DateTimeFormat(locale, { hour: "numeric" });
@@ -54,6 +54,12 @@ export declare function inferTimeGranularity(granularity: TimeGranularity | unde
54
54
  * @param fieldNode - The id of the date field associated with the segment
55
55
  */
56
56
  export declare function isFirstTimeSegment(id: string, fieldNode: HTMLElement | null): boolean;
57
+ type SetTimeDescriptionProps = {
58
+ id: string;
59
+ formatter: TimeFormatter;
60
+ value: TimeValue;
61
+ doc: Document;
62
+ };
57
63
  /**
58
64
  * Creates or updates a description element for a date field
59
65
  * which enables screen readers to read the date field's value.
@@ -62,13 +68,13 @@ export declare function isFirstTimeSegment(id: string, fieldNode: HTMLElement |
62
68
  * so it can be associated via `aria-describedby` and read by
63
69
  * screen readers as the user interacts with the date field.
64
70
  */
65
- export declare function setTimeDescription(id: string, formatter: TimeFormatter, value: TimeValue): void;
71
+ export declare function setTimeDescription(props: SetTimeDescriptionProps): void;
66
72
  /**
67
73
  * Removes the description element for the date field with
68
74
  * the provided ID. This function should be called when the
69
75
  * date field is unmounted.
70
76
  */
71
- export declare function removeTimeDescriptionElement(id: string): void;
77
+ export declare function removeTimeDescriptionElement(id: string, doc: Document): void;
72
78
  export declare function convertTimeValueToDateValue(time: TimeValue): CalendarDateTime | ZonedDateTime;
73
79
  export declare function convertTimeValueToTime(time: TimeValue): Time;
74
80
  export declare function isTimeBefore(timeToCompare: Time, referenceTime: Time): boolean;
@@ -245,19 +245,19 @@ export function isFirstTimeSegment(id, fieldNode) {
245
245
  * so it can be associated via `aria-describedby` and read by
246
246
  * screen readers as the user interacts with the date field.
247
247
  */
248
- export function setTimeDescription(id, formatter, value) {
248
+ export function setTimeDescription(props) {
249
249
  if (!isBrowser)
250
250
  return;
251
- const valueString = formatter.selectedTime(value);
252
- const el = document.getElementById(id);
251
+ const valueString = props.formatter.selectedTime(props.value);
252
+ const el = props.doc.getElementById(props.id);
253
253
  if (!el) {
254
- const div = document.createElement("div");
254
+ const div = props.doc.createElement("div");
255
255
  div.style.cssText = styleToString({
256
256
  display: "none",
257
257
  });
258
- div.id = id;
258
+ div.id = props.id;
259
259
  div.innerText = `Selected Time: ${valueString}`;
260
- document.body.appendChild(div);
260
+ props.doc.body.appendChild(div);
261
261
  }
262
262
  else {
263
263
  el.innerText = `Selected Time: ${valueString}`;
@@ -268,13 +268,13 @@ export function setTimeDescription(id, formatter, value) {
268
268
  * the provided ID. This function should be called when the
269
269
  * date field is unmounted.
270
270
  */
271
- export function removeTimeDescriptionElement(id) {
271
+ export function removeTimeDescriptionElement(id, doc) {
272
272
  if (!isBrowser)
273
273
  return;
274
- const el = document.getElementById(id);
274
+ const el = doc.getElementById(id);
275
275
  if (!el)
276
276
  return;
277
- document.body.removeChild(el);
277
+ doc.body.removeChild(el);
278
278
  }
279
279
  export function convertTimeValueToDateValue(time) {
280
280
  if (time instanceof Time) {
@@ -1,4 +1,3 @@
1
- export declare function getDocument(element?: Element | null): Document;
2
1
  export declare function activeElement(doc: Document): Element | null;
3
2
  export declare function getFirstNonCommentChild(element: HTMLElement | null): ChildNode | null;
4
3
  /**
@@ -1,6 +1,3 @@
1
- export function getDocument(element) {
2
- return element?.ownerDocument ?? document;
3
- }
4
1
  export function activeElement(doc) {
5
2
  let activeElement = doc.activeElement;
6
3
  while (activeElement?.shadowRoot?.activeElement != null) {
@@ -20,9 +20,9 @@ export declare function focus(element?: FocusableTarget | null, { select }?: {
20
20
  * Attempts to focus the first element in a list of candidates.
21
21
  * Stops when focus is successful.
22
22
  */
23
- export declare function focusFirst(candidates: HTMLElement[], { select }?: {
23
+ export declare function focusFirst(candidates: HTMLElement[], { select }: {
24
24
  select?: boolean | undefined;
25
- }): true | undefined;
25
+ } | undefined, getActiveElement: () => HTMLElement | null): true | undefined;
26
26
  /**
27
27
  * Returns the first visible element in a list.
28
28
  * NOTE: Only checks visibility up to the `container`.
@@ -1,3 +1,4 @@
1
+ import { getDocument, getWindow } from "svelte-toolbelt";
1
2
  import { isBrowser, isElementHidden, isSelectableInput } from "./is.js";
2
3
  /**
3
4
  * Handles `initialFocus` prop behavior for the
@@ -20,12 +21,14 @@ export function handleCalendarInitialFocus(calendar) {
20
21
  * A utility function that focuses an element without scrolling.
21
22
  */
22
23
  export function focusWithoutScroll(element) {
24
+ const doc = getDocument(element);
25
+ const win = getWindow(element);
23
26
  const scrollPosition = {
24
- x: window.pageXOffset || document.documentElement.scrollLeft,
25
- y: window.pageYOffset || document.documentElement.scrollTop,
27
+ x: win.pageXOffset || doc.documentElement.scrollLeft,
28
+ y: win.pageYOffset || doc.documentElement.scrollTop,
26
29
  };
27
30
  element.focus();
28
- window.scrollTo(scrollPosition.x, scrollPosition.y);
31
+ win.scrollTo(scrollPosition.x, scrollPosition.y);
29
32
  }
30
33
  /**
31
34
  * A utility function that focuses an element.
@@ -33,9 +36,10 @@ export function focusWithoutScroll(element) {
33
36
  export function focus(element, { select = false } = {}) {
34
37
  if (!(element && element.focus))
35
38
  return;
36
- if (document.activeElement === element)
39
+ const doc = getDocument(element);
40
+ if (doc.activeElement === element)
37
41
  return;
38
- const previouslyFocusedElement = document.activeElement;
42
+ const previouslyFocusedElement = doc.activeElement;
39
43
  // prevent scroll on focus
40
44
  element.focus({ preventScroll: true });
41
45
  // only elect if its not the same element, it supports selection, and we need to select it
@@ -47,11 +51,11 @@ export function focus(element, { select = false } = {}) {
47
51
  * Attempts to focus the first element in a list of candidates.
48
52
  * Stops when focus is successful.
49
53
  */
50
- export function focusFirst(candidates, { select = false } = {}) {
51
- const previouslyFocusedElement = document.activeElement;
54
+ export function focusFirst(candidates, { select = false } = {}, getActiveElement) {
55
+ const previouslyFocusedElement = getActiveElement();
52
56
  for (const candidate of candidates) {
53
57
  focus(candidate, { select });
54
- if (document.activeElement !== previouslyFocusedElement)
58
+ if (getActiveElement() !== previouslyFocusedElement)
55
59
  return true;
56
60
  }
57
61
  }
@@ -78,7 +82,8 @@ export function findVisible(elements, container) {
78
82
  */
79
83
  export function getTabbableCandidates(container) {
80
84
  const nodes = [];
81
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
85
+ const doc = getDocument(container);
86
+ const walker = doc.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
82
87
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
88
  acceptNode: (node) => {
84
89
  const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
@@ -1,5 +1 @@
1
- /**
2
- * From https://github.com/melt-ui/melt-ui/blob/main/packages/svelte/src/lib/internal/math.ts
3
- */
4
- export declare function snapValueToStep(value: number, min: number, max: number, step: number): number;
5
1
  export declare function linearScale(domain: [number, number], range: [number, number], clamp?: boolean): (x: number) => number;
@@ -1,31 +1,3 @@
1
- /**
2
- * From https://github.com/melt-ui/melt-ui/blob/main/packages/svelte/src/lib/internal/math.ts
3
- */
4
- export function snapValueToStep(value, min, max, step) {
5
- const remainder = (value - (Number.isNaN(min) ? 0 : min)) % step;
6
- let snappedValue = Math.abs(remainder) * 2 >= step
7
- ? value + Math.sign(remainder) * (step - Math.abs(remainder))
8
- : value - remainder;
9
- if (!Number.isNaN(min)) {
10
- if (snappedValue < min) {
11
- snappedValue = min;
12
- }
13
- else if (!Number.isNaN(max) && snappedValue > max) {
14
- snappedValue = min + Math.floor((max - min) / step) * step;
15
- }
16
- }
17
- else if (!Number.isNaN(max) && snappedValue > max) {
18
- snappedValue = Math.floor(max / step) * step;
19
- }
20
- const string = step.toString();
21
- const index = string.indexOf(".");
22
- const precision = index >= 0 ? string.length - index : 0;
23
- if (precision > 0) {
24
- const pow = 10 ** precision;
25
- snappedValue = Math.round(snappedValue * pow) / pow;
26
- }
27
- return snappedValue;
28
- }
29
1
  export function linearScale(domain, range, clamp = true) {
30
2
  const [d0, d1] = domain;
31
3
  const [r0, r1] = range;
@@ -6,5 +6,3 @@ export declare function getTabbableIn(container: HTMLElement, direction: "next"
6
6
  */
7
7
  export declare function getTabbableFrom(currentNode: HTMLElement, direction: "next" | "prev"): import("tabbable").FocusableElement | undefined;
8
8
  export declare function getTabbableFromFocusable(currentNode: HTMLElement, direction: "next" | "prev"): import("tabbable").FocusableElement;
9
- export declare function getNextTabbable(): import("tabbable").FocusableElement | undefined;
10
- export declare function getPreviousTabbable(): import("tabbable").FocusableElement | undefined;
@@ -1,5 +1,6 @@
1
1
  import { focusable, isFocusable, isTabbable, tabbable } from "tabbable";
2
- import { activeElement, getDocument } from "./dom.js";
2
+ import { activeElement } from "./dom.js";
3
+ import { getDocument } from "svelte-toolbelt";
3
4
  function getTabbableOptions() {
4
5
  return {
5
6
  getShadowRoot: true,
@@ -32,35 +33,30 @@ export function getTabbableFrom(currentNode, direction) {
32
33
  if (!isTabbable(currentNode, getTabbableOptions())) {
33
34
  return getTabbableFromFocusable(currentNode, direction);
34
35
  }
35
- const allTabbable = tabbable(getDocument(currentNode).body, getTabbableOptions());
36
+ const doc = getDocument(currentNode);
37
+ const allTabbable = tabbable(doc.body, getTabbableOptions());
36
38
  if (direction === "prev")
37
39
  allTabbable.reverse();
38
40
  const activeIndex = allTabbable.indexOf(currentNode);
39
41
  if (activeIndex === -1)
40
- return document.body;
42
+ return doc.body;
41
43
  const nextTabbableElements = allTabbable.slice(activeIndex + 1);
42
44
  return nextTabbableElements[0];
43
45
  }
44
46
  export function getTabbableFromFocusable(currentNode, direction) {
47
+ const doc = getDocument(currentNode);
45
48
  if (!isFocusable(currentNode, getTabbableOptions()))
46
- return document.body;
49
+ return doc.body;
47
50
  // find all focusable nodes, since some elements may be focusable but not tabbable
48
51
  // such as context menu triggers
49
- const allFocusable = focusable(getDocument(currentNode).body, getTabbableOptions());
52
+ const allFocusable = focusable(doc.body, getTabbableOptions());
50
53
  // find index of current node among focusable siblings
51
54
  if (direction === "prev")
52
55
  allFocusable.reverse();
53
56
  const activeIndex = allFocusable.indexOf(currentNode);
54
57
  if (activeIndex === -1)
55
- return document.body;
58
+ return doc.body;
56
59
  const nextFocusableElements = allFocusable.slice(activeIndex + 1);
57
60
  // find the next focusable node that is also tabbable
58
- return (nextFocusableElements.find((node) => isTabbable(node, getTabbableOptions())) ??
59
- document.body);
60
- }
61
- export function getNextTabbable() {
62
- return getTabbableIn(document.body, "next");
63
- }
64
- export function getPreviousTabbable() {
65
- return getTabbableIn(document.body, "prev");
61
+ return nextFocusableElements.find((node) => isTabbable(node, getTabbableOptions())) ?? doc.body;
66
62
  }
@@ -5,6 +5,7 @@ type UseDataTypeaheadOpts = {
5
5
  getCurrentItem: () => string;
6
6
  candidateValues: Getter<string[]>;
7
7
  enabled: boolean;
8
+ getWindow: () => Window & typeof globalThis;
8
9
  };
9
10
  export declare function useDataTypeahead(opts: UseDataTypeaheadOpts): {
10
11
  search: import("svelte-toolbelt").WritableBox<string>;
@@ -2,7 +2,10 @@ import { getNextMatch } from "./arrays.js";
2
2
  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
- const search = boxAutoReset("", 1000);
5
+ const search = boxAutoReset("", {
6
+ afterMs: 1000,
7
+ getWindow: opts.getWindow,
8
+ });
6
9
  const candidateValues = $derived(opts.candidateValues());
7
10
  function handleTypeaheadSearch(key) {
8
11
  if (!opts.enabled)
@@ -2,8 +2,10 @@ export type DOMTypeahead = ReturnType<typeof useDOMTypeahead>;
2
2
  type UseDOMTypeaheadOpts = {
3
3
  onMatch?: (item: HTMLElement) => void;
4
4
  getCurrentItem?: () => HTMLElement | null;
5
+ getActiveElement: () => HTMLElement | null;
6
+ getWindow: () => Window & typeof globalThis;
5
7
  };
6
- export declare function useDOMTypeahead(opts?: UseDOMTypeaheadOpts): {
8
+ export declare function useDOMTypeahead(opts: UseDOMTypeaheadOpts): {
7
9
  search: import("svelte-toolbelt").WritableBox<string>;
8
10
  handleTypeaheadSearch: (key: string, candidates: HTMLElement[]) => HTMLElement | undefined;
9
11
  resetTypeahead: () => void;
@@ -2,9 +2,12 @@ import { getNextMatch } from "./arrays.js";
2
2
  import { boxAutoReset } from "./box-auto-reset.svelte.js";
3
3
  export function useDOMTypeahead(opts) {
4
4
  // Reset `search` 1 second after it was last updated
5
- const search = boxAutoReset("", 1000);
5
+ const search = boxAutoReset("", {
6
+ afterMs: 1000,
7
+ getWindow: opts.getWindow,
8
+ });
6
9
  const onMatch = opts?.onMatch ?? ((node) => node.focus());
7
- const getCurrentItem = opts?.getCurrentItem ?? (() => document.activeElement);
10
+ const getCurrentItem = opts?.getCurrentItem ?? (() => opts?.getActiveElement());
8
11
  function handleTypeaheadSearch(key, candidates) {
9
12
  if (!candidates.length)
10
13
  return;
@@ -1,14 +1,18 @@
1
- import { executeCallbacks } from "svelte-toolbelt";
1
+ import { executeCallbacks, getWindow } from "svelte-toolbelt";
2
2
  import { on } from "svelte/events";
3
3
  import { watch } from "runed";
4
4
  import { boxAutoReset } from "./box-auto-reset.svelte.js";
5
5
  import { isElement, isHTMLElement } from "./is.js";
6
6
  export function useGraceArea(opts) {
7
7
  const enabled = $derived(opts.enabled());
8
- const isPointerInTransit = boxAutoReset(false, opts.transitTimeout ?? 300, (value) => {
9
- if (enabled) {
10
- opts.setIsPointerInTransit?.(value);
11
- }
8
+ const isPointerInTransit = boxAutoReset(false, {
9
+ afterMs: opts.transitTimeout ?? 300,
10
+ onChange: (value) => {
11
+ if (enabled) {
12
+ opts.setIsPointerInTransit?.(value);
13
+ }
14
+ },
15
+ getWindow: () => getWindow(opts.triggerNode()),
12
16
  });
13
17
  let pointerGraceArea = $state(null);
14
18
  function handleRemoveGraceArea() {