bits-ui 2.4.0 → 2.4.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.
@@ -76,6 +76,7 @@
76
76
  <EscapeLayer
77
77
  {...mergedProps}
78
78
  enabled={contentState.root.opts.open.current}
79
+ ref={contentState.opts.ref}
79
80
  onEscapeKeydown={(e) => {
80
81
  onEscapeKeydown(e);
81
82
  if (e.defaultPrevented) return;
@@ -69,6 +69,7 @@
69
69
  <EscapeLayer
70
70
  {...mergedProps}
71
71
  enabled={contentState.root.opts.open.current}
72
+ ref={contentState.opts.ref}
72
73
  onEscapeKeydown={(e) => {
73
74
  onEscapeKeydown(e);
74
75
  if (e.defaultPrevented) return;
@@ -73,6 +73,7 @@
73
73
  {#snippet children({ props: dismissibleProps })}
74
74
  <EscapeLayer
75
75
  enabled={true}
76
+ ref={contentImplState.opts.ref}
76
77
  onEscapeKeydown={(e) => {
77
78
  onEscapeKeydown(e);
78
79
  if (e.defaultPrevented) return;
@@ -506,7 +506,7 @@ class ScrollAreaScrollbarSharedState {
506
506
  if (isScrollbarWheel)
507
507
  this.handleWheelScroll(e, maxScrollPos);
508
508
  };
509
- const unsubListener = addEventListener(document, "wheel", handleWheel, {
509
+ const unsubListener = addEventListener(this.root.domContext.getDocument(), "wheel", handleWheel, {
510
510
  passive: false,
511
511
  });
512
512
  return unsubListener;
@@ -1,4 +1,4 @@
1
- import { type Box, type ReadableBox } from "svelte-toolbelt";
1
+ import { type Box, type ReadableBox, DOMContext } from "svelte-toolbelt";
2
2
  import { Context } from "runed";
3
3
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
4
4
  import type { BitsKeyboardEvent, OnChangeFn, WithRefProps } from "../../internal/types.js";
@@ -22,6 +22,7 @@ declare class SliderBaseRootState {
22
22
  isActive: boolean;
23
23
  direction: "rl" | "lr" | "tb" | "bt";
24
24
  normalizedSteps: number[];
25
+ domContext: DOMContext;
25
26
  constructor(opts: SliderBaseRootStateProps);
26
27
  isThumbActive(_index: number): boolean;
27
28
  getAllThumbs: () => HTMLElement[];
@@ -3,7 +3,7 @@
3
3
  * Abdelrahman (https://github.com/abdel-17)
4
4
  */
5
5
  import { untrack } from "svelte";
6
- import { executeCallbacks, onMountEffect, attachRef, } from "svelte-toolbelt";
6
+ import { executeCallbacks, onMountEffect, attachRef, DOMContext, } from "svelte-toolbelt";
7
7
  import { on } from "svelte/events";
8
8
  import { Context, watch } from "runed";
9
9
  import { getRangeStyles, getThumbStyles, getTickStyles, normalizeSteps, snapValueToCustomSteps, getAdjacentStepValue, getTickLabelStyles, getThumbLabelStyles, } from "./helpers.js";
@@ -31,8 +31,10 @@ class SliderBaseRootState {
31
31
  normalizedSteps = $derived.by(() => {
32
32
  return normalizeSteps(this.opts.step.current, this.opts.min.current, this.opts.max.current);
33
33
  });
34
+ domContext;
34
35
  constructor(opts) {
35
36
  this.opts = opts;
37
+ this.domContext = new DOMContext(this.opts.ref);
36
38
  }
37
39
  isThumbActive(_index) {
38
40
  return this.isActive;
@@ -101,7 +103,7 @@ class SliderSingleRootState extends SliderBaseRootState {
101
103
  super(opts);
102
104
  this.opts = opts;
103
105
  onMountEffect(() => {
104
- return executeCallbacks(on(document, "pointerdown", this.handlePointerDown), on(document, "pointerup", this.handlePointerUp), on(document, "pointermove", this.handlePointerMove), on(document, "pointerleave", this.handlePointerUp));
106
+ return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.handlePointerDown), on(this.domContext.getDocument(), "pointerup", this.handlePointerUp), on(this.domContext.getDocument(), "pointermove", this.handlePointerMove), on(this.domContext.getDocument(), "pointerleave", this.handlePointerUp));
105
107
  });
106
108
  watch([
107
109
  () => this.opts.step.current,
@@ -191,7 +193,7 @@ class SliderSingleRootState extends SliderBaseRootState {
191
193
  const closestThumb = this.getAllThumbs()[0];
192
194
  if (!closestThumb || !sliderNode)
193
195
  return;
194
- const target = e.target;
196
+ const target = e.composedPath()[0] ?? e.target;
195
197
  if (!isElementOrSVGElement(target) || !sliderNode.contains(target))
196
198
  return;
197
199
  e.preventDefault();
@@ -286,7 +288,7 @@ class SliderMultiRootState extends SliderBaseRootState {
286
288
  super(opts);
287
289
  this.opts = opts;
288
290
  onMountEffect(() => {
289
- return executeCallbacks(on(document, "pointerdown", this.handlePointerDown), on(document, "pointerup", this.handlePointerUp), on(document, "pointermove", this.handlePointerMove), on(document, "pointerleave", this.handlePointerUp));
291
+ return executeCallbacks(on(this.domContext.getDocument(), "pointerdown", this.handlePointerDown), on(this.domContext.getDocument(), "pointerup", this.handlePointerUp), on(this.domContext.getDocument(), "pointermove", this.handlePointerMove), on(this.domContext.getDocument(), "pointerleave", this.handlePointerUp));
290
292
  });
291
293
  watch([
292
294
  () => this.opts.step.current,
@@ -405,7 +407,7 @@ class SliderMultiRootState extends SliderBaseRootState {
405
407
  const closestThumb = this.#getClosestThumb(e);
406
408
  if (!closestThumb || !sliderNode)
407
409
  return;
408
- const target = e.target;
410
+ const target = e.composedPath()[0] ?? e.target;
409
411
  if (!isElementOrSVGElement(target) || !sliderNode.contains(target))
410
412
  return;
411
413
  e.preventDefault();
@@ -9,12 +9,14 @@
9
9
  onEscapeKeydown = noop,
10
10
  children,
11
11
  enabled,
12
+ ref,
12
13
  }: EscapeLayerImplProps = $props();
13
14
 
14
15
  useEscapeLayer({
15
16
  escapeKeydownBehavior: box.with(() => escapeKeydownBehavior),
16
17
  onEscapeKeydown: box.with(() => onEscapeKeydown),
17
18
  enabled: box.with(() => enabled),
19
+ ref,
18
20
  });
19
21
  </script>
20
22
 
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from "svelte";
2
+ import type { Box } from "svelte-toolbelt";
2
3
  export type EscapeBehaviorType = "close" | "defer-otherwise-close" | "defer-otherwise-ignore" | "ignore";
3
4
  export type EscapeLayerProps = {
4
5
  /**
@@ -24,4 +25,5 @@ export type EscapeLayerImplProps = {
24
25
  */
25
26
  enabled: boolean;
26
27
  children?: Snippet;
28
+ ref: Box<HTMLElement | null>;
27
29
  } & EscapeLayerProps;
@@ -1,9 +1,13 @@
1
+ import { DOMContext, type Box } from "svelte-toolbelt";
1
2
  import type { EscapeLayerImplProps } from "./types.js";
2
3
  import type { ReadableBoxedValues } from "../../../internal/box.svelte.js";
3
- type EscapeLayerStateProps = ReadableBoxedValues<Required<Omit<EscapeLayerImplProps, "children">>>;
4
+ type EscapeLayerStateProps = ReadableBoxedValues<Required<Omit<EscapeLayerImplProps, "children" | "ref">>> & {
5
+ ref: Box<HTMLElement | null>;
6
+ };
4
7
  export declare class EscapeLayerState {
5
8
  #private;
6
9
  readonly opts: EscapeLayerStateProps;
10
+ readonly domContext: DOMContext;
7
11
  constructor(opts: EscapeLayerStateProps);
8
12
  }
9
13
  export declare function useEscapeLayer(props: EscapeLayerStateProps): EscapeLayerState;
@@ -1,3 +1,4 @@
1
+ import { DOMContext } from "svelte-toolbelt";
1
2
  import { watch } from "runed";
2
3
  import { on } from "svelte/events";
3
4
  import { kbd } from "../../../internal/kbd.js";
@@ -5,8 +6,10 @@ import { noop } from "../../../internal/noop.js";
5
6
  globalThis.bitsEscapeLayers ??= new Map();
6
7
  export class EscapeLayerState {
7
8
  opts;
9
+ domContext;
8
10
  constructor(opts) {
9
11
  this.opts = opts;
12
+ this.domContext = new DOMContext(this.opts.ref);
10
13
  let unsubEvents = noop;
11
14
  watch(() => opts.enabled.current, (enabled) => {
12
15
  if (enabled) {
@@ -20,7 +23,7 @@ export class EscapeLayerState {
20
23
  });
21
24
  }
22
25
  #addEventListener = () => {
23
- return on(document, "keydown", this.#onkeydown, { passive: false });
26
+ return on(this.domContext.getDocument(), "keydown", this.#onkeydown, { passive: false });
24
27
  };
25
28
  #onkeydown = (e) => {
26
29
  if (e.key !== kbd.ESCAPE || !isResponsibleEscapeLayer(this))
@@ -183,6 +183,8 @@ class FloatingContentState {
183
183
  return cleanup;
184
184
  },
185
185
  open: () => this.opts.enabled.current,
186
+ sideOffset: () => this.opts.sideOffset.current,
187
+ alignOffset: () => this.opts.alignOffset.current,
186
188
  });
187
189
  $effect(() => {
188
190
  if (!this.floating.isPositioned)
@@ -91,7 +91,7 @@
91
91
  {ref}
92
92
  >
93
93
  {#snippet focusScope({ props: focusScopeProps })}
94
- <EscapeLayer {onEscapeKeydown} {escapeKeydownBehavior} {enabled}>
94
+ <EscapeLayer {onEscapeKeydown} {escapeKeydownBehavior} {enabled} {ref}>
95
95
  <DismissibleLayer
96
96
  {id}
97
97
  {onInteractOutside}
@@ -41,6 +41,18 @@ export type UseFloatingOptions = {
41
41
  * @default undefined
42
42
  */
43
43
  whileElementsMounted?: (reference: ReferenceElement, floating: FloatingElement, update: () => void) => () => void;
44
+ /**
45
+ * The offset from the reference element along the side axis.
46
+ * Used to detect bad coordinates during transitions.
47
+ * @default undefined
48
+ */
49
+ sideOffset?: ValueOrGetValue<number | undefined>;
50
+ /**
51
+ * The offset from the reference element along the alignment axis.
52
+ * Used to detect bad coordinates during transitions.
53
+ * @default undefined
54
+ */
55
+ alignOffset?: ValueOrGetValue<number | undefined>;
44
56
  };
45
57
  export type UseFloatingReturn = {
46
58
  /**
@@ -9,6 +9,8 @@ export function useFloating(options) {
9
9
  const transformOption = $derived(get(options.transform) ?? true);
10
10
  const placementOption = $derived(get(options.placement) ?? "bottom");
11
11
  const strategyOption = $derived(get(options.strategy) ?? "absolute");
12
+ const sideOffsetOption = $derived(get(options.sideOffset) ?? 0);
13
+ const alignOffsetOption = $derived(get(options.alignOffset) ?? 0);
12
14
  const reference = options.reference;
13
15
  /** State */
14
16
  let x = $state(0);
@@ -19,21 +21,17 @@ export function useFloating(options) {
19
21
  let middlewareData = $state({});
20
22
  let isPositioned = $state(false);
21
23
  const floatingStyles = $derived.by(() => {
22
- const initialStyles = {
23
- position: strategy,
24
- left: "0",
25
- top: "0",
26
- };
27
- if (!floating.current) {
28
- return initialStyles;
29
- }
30
- const xVal = roundByDPR(floating.current, x);
31
- const yVal = roundByDPR(floating.current, y);
24
+ // preserve last known position when floating ref is null (during transitions)
25
+ const xVal = floating.current ? roundByDPR(floating.current, x) : x;
26
+ const yVal = floating.current ? roundByDPR(floating.current, y) : y;
32
27
  if (transformOption) {
33
28
  return {
34
- ...initialStyles,
29
+ position: strategy,
30
+ left: "0",
31
+ top: "0",
35
32
  transform: `translate(${xVal}px, ${yVal}px)`,
36
- ...(getDPR(floating.current) >= 1.5 && {
33
+ ...(floating.current &&
34
+ getDPR(floating.current) >= 1.5 && {
37
35
  willChange: "transform",
38
36
  }),
39
37
  };
@@ -54,6 +52,14 @@ export function useFloating(options) {
54
52
  placement: placementOption,
55
53
  strategy: strategyOption,
56
54
  }).then((position) => {
55
+ // ignore bad coordinates that cause jumping during close transitions
56
+ if (!openOption && x !== 0 && y !== 0) {
57
+ // if we had a good position and now getting coordinates near
58
+ // the expected offset bounds during close, ignore it
59
+ const maxExpectedOffset = Math.max(Math.abs(sideOffsetOption), Math.abs(alignOffsetOption), 15);
60
+ if (position.x <= maxExpectedOffset && position.y <= maxExpectedOffset)
61
+ return;
62
+ }
57
63
  x = position.x;
58
64
  y = position.y;
59
65
  strategy = position.strategy;
@@ -1,4 +1,4 @@
1
- import { executeCallbacks, getWindow } from "svelte-toolbelt";
1
+ import { executeCallbacks, getDocument, 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";
@@ -60,7 +60,10 @@ export function useGraceArea(opts) {
60
60
  opts.onPointerExit();
61
61
  }
62
62
  };
63
- return on(document, "pointermove", handleTrackPointerGrace);
63
+ const doc = getDocument(opts.triggerNode() ?? opts.contentNode());
64
+ if (!doc)
65
+ return;
66
+ return on(doc, "pointermove", handleTrackPointerGrace);
64
67
  });
65
68
  return {
66
69
  isPointerInTransit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",