bits-ui 2.2.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.
@@ -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
  }
@@ -46,6 +46,7 @@
46
46
  preventScroll={false}
47
47
  forceMount={true}
48
48
  ref={contentState.opts.ref}
49
+ tooltip={true}
49
50
  >
50
51
  {#snippet popper({ props })}
51
52
  {@const mergedProps = mergeProps(props, {
@@ -64,6 +65,7 @@
64
65
  <PopperLayer
65
66
  {...mergedProps}
66
67
  {...contentState.popperProps}
68
+ tooltip={true}
67
69
  isStatic
68
70
  present={contentState.root.opts.open.current}
69
71
  {id}
@@ -64,6 +64,7 @@
64
64
  preventScroll={false}
65
65
  forceMount={true}
66
66
  ref={contentState.opts.ref}
67
+ tooltip={true}
67
68
  >
68
69
  {#snippet popper({ props, wrapperProps })}
69
70
  {@const mergedProps = mergeProps(props, {
@@ -91,6 +92,7 @@
91
92
  preventScroll={false}
92
93
  forceMount={false}
93
94
  ref={contentState.opts.ref}
95
+ tooltip={true}
94
96
  >
95
97
  {#snippet popper({ props, wrapperProps })}
96
98
  {@const mergedProps = mergeProps(props, {
@@ -29,7 +29,7 @@
29
29
  const mergedProps = $derived(mergeProps(restProps, triggerState.props, { type }));
30
30
  </script>
31
31
 
32
- <FloatingLayerAnchor {id} ref={triggerState.opts.ref}>
32
+ <FloatingLayerAnchor {id} ref={triggerState.opts.ref} tooltip={true}>
33
33
  {#if child}
34
34
  {@render child({ props: mergedProps })}
35
35
  {:else}
@@ -32,6 +32,6 @@
32
32
  });
33
33
  </script>
34
34
 
35
- <FloatingLayer>
35
+ <FloatingLayer tooltip>
36
36
  {@render children?.()}
37
37
  </FloatingLayer>
@@ -1,7 +1,7 @@
1
1
  import { DOMContext } from "svelte-toolbelt";
2
2
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
3
  import type { WithRefProps } from "../../internal/types.js";
4
- import type { PointerEventHandler } from "svelte/elements";
4
+ import type { FocusEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
5
5
  type TooltipProviderStateProps = ReadableBoxedValues<{
6
6
  delayDuration: number;
7
7
  disableHoverableContent: boolean;
@@ -58,23 +58,21 @@ declare class TooltipTriggerState {
58
58
  constructor(opts: TooltipTriggerStateProps, root: TooltipRootState);
59
59
  handlePointerUp: () => void;
60
60
  props: {
61
- id: string;
62
- "aria-describedby": string | undefined;
63
- "data-state": string;
64
- "data-disabled": "" | undefined;
65
- "data-delay-duration": string;
66
- "data-tooltip-trigger": string;
67
- tabindex: number | undefined;
68
- disabled: boolean;
69
- onpointerup: () => void;
70
- onpointerdown: () => void;
71
- onpointermove: PointerEventHandler<HTMLElement>;
72
- onpointerleave: () => void;
73
- onfocus: (e: FocusEvent & {
74
- currentTarget: HTMLElement;
75
- }) => void;
76
- onblur: () => void;
77
- onclick: () => void;
61
+ readonly id: string;
62
+ readonly "aria-describedby": string | undefined;
63
+ readonly "data-state": string;
64
+ readonly "data-disabled": "" | undefined;
65
+ readonly "data-delay-duration": `${number}`;
66
+ readonly "data-tooltip-trigger": "";
67
+ readonly tabindex: 0 | undefined;
68
+ readonly disabled: boolean;
69
+ readonly onpointerup: PointerEventHandler<HTMLElement>;
70
+ readonly onpointerdown: PointerEventHandler<HTMLElement>;
71
+ readonly onpointermove: PointerEventHandler<HTMLElement>;
72
+ readonly onpointerleave: PointerEventHandler<HTMLElement>;
73
+ readonly onfocus: FocusEventHandler<HTMLElement>;
74
+ readonly onblur: FocusEventHandler<HTMLElement>;
75
+ readonly onclick: MouseEventHandler<HTMLElement>;
78
76
  };
79
77
  }
80
78
  type TooltipContentStateProps = WithRefProps & ReadableBoxedValues<{
@@ -194,7 +194,9 @@ class TooltipTriggerState {
194
194
  };
195
195
  props = $derived.by(() => ({
196
196
  id: this.opts.id.current,
197
- "aria-describedby": this.root.opts.open.current ? this.root.contentNode?.id : undefined,
197
+ "aria-describedby": this.root.opts.open.current
198
+ ? this.root.contentNode?.id
199
+ : undefined,
198
200
  "data-state": this.root.stateAttr,
199
201
  "data-disabled": getDataDisabled(this.#isDisabled),
200
202
  "data-delay-duration": `${this.root.delayDuration}`,
@@ -4,13 +4,16 @@
4
4
  import type { AnchorProps } from "./index.js";
5
5
  import type { Measurable } from "../../../../internal/floating-svelte/types.js";
6
6
 
7
- let { id, children, virtualEl, ref }: AnchorProps = $props();
7
+ let { id, children, virtualEl, ref, tooltip = false }: AnchorProps = $props();
8
8
 
9
- useFloatingAnchorState({
10
- id: box.with(() => id),
11
- virtualEl: box.with(() => virtualEl as unknown as Measurable | null),
12
- ref,
13
- });
9
+ useFloatingAnchorState(
10
+ {
11
+ id: box.with(() => id),
12
+ virtualEl: box.with(() => virtualEl as unknown as Measurable | null),
13
+ ref,
14
+ },
15
+ tooltip
16
+ );
14
17
  </script>
15
18
 
16
19
  {@render children?.()}
@@ -25,29 +25,33 @@
25
25
  wrapperId = useId(),
26
26
  customAnchor = null,
27
27
  enabled,
28
+ tooltip = false,
28
29
  }: ContentImplProps = $props();
29
30
 
30
- const contentState = useFloatingContentState({
31
- side: box.with(() => side),
32
- sideOffset: box.with(() => sideOffset),
33
- align: box.with(() => align),
34
- alignOffset: box.with(() => alignOffset),
35
- id: box.with(() => id),
36
- arrowPadding: box.with(() => arrowPadding),
37
- avoidCollisions: box.with(() => avoidCollisions),
38
- collisionBoundary: box.with(() => collisionBoundary),
39
- collisionPadding: box.with(() => collisionPadding),
40
- hideWhenDetached: box.with(() => hideWhenDetached),
41
- onPlaced: box.with(() => onPlaced),
42
- sticky: box.with(() => sticky),
43
- updatePositionStrategy: box.with(() => updatePositionStrategy),
44
- strategy: box.with(() => strategy),
45
- dir: box.with(() => dir),
46
- style: box.with(() => style),
47
- enabled: box.with(() => enabled),
48
- wrapperId: box.with(() => wrapperId),
49
- customAnchor: box.with(() => customAnchor),
50
- });
31
+ const contentState = useFloatingContentState(
32
+ {
33
+ side: box.with(() => side),
34
+ sideOffset: box.with(() => sideOffset),
35
+ align: box.with(() => align),
36
+ alignOffset: box.with(() => alignOffset),
37
+ id: box.with(() => id),
38
+ arrowPadding: box.with(() => arrowPadding),
39
+ avoidCollisions: box.with(() => avoidCollisions),
40
+ collisionBoundary: box.with(() => collisionBoundary),
41
+ collisionPadding: box.with(() => collisionPadding),
42
+ hideWhenDetached: box.with(() => hideWhenDetached),
43
+ onPlaced: box.with(() => onPlaced),
44
+ sticky: box.with(() => sticky),
45
+ updatePositionStrategy: box.with(() => updatePositionStrategy),
46
+ strategy: box.with(() => strategy),
47
+ dir: box.with(() => dir),
48
+ style: box.with(() => style),
49
+ enabled: box.with(() => enabled),
50
+ wrapperId: box.with(() => wrapperId),
51
+ customAnchor: box.with(() => customAnchor),
52
+ },
53
+ tooltip
54
+ );
51
55
 
52
56
  const mergedProps = $derived(
53
57
  mergeProps(contentState.wrapperProps, {
@@ -2,9 +2,9 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import { useFloatingRootState } from "../use-floating-layer.svelte.js";
4
4
 
5
- let { children }: { children?: Snippet } = $props();
5
+ let { children, tooltip = false }: { children?: Snippet; tooltip?: boolean } = $props();
6
6
 
7
- useFloatingRootState();
7
+ useFloatingRootState(tooltip);
8
8
  </script>
9
9
 
10
10
  {@render children?.()}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  type $$ComponentProps = {
3
3
  children?: Snippet;
4
+ tooltip?: boolean;
4
5
  };
5
6
  declare const FloatingLayer: import("svelte").Component<$$ComponentProps, {}, "">;
6
7
  type FloatingLayer = ReturnType<typeof FloatingLayer>;
@@ -107,10 +107,28 @@ export type FloatingLayerContentImplProps = {
107
107
  */
108
108
  onPlaced?: () => void;
109
109
  enabled: boolean;
110
+ /**
111
+ * Tooltips are special in that they are commonly composed
112
+ * with other floating components, where the same trigger is
113
+ * used for both the tooltip and the popover.
114
+ *
115
+ * For situations like this, we need to use a different context
116
+ * symbol so that conflicts don't occur.
117
+ */
118
+ tooltip?: boolean;
110
119
  } & FloatingLayerContentProps;
111
120
  export type FloatingLayerAnchorProps = {
112
121
  id: string;
113
122
  children?: Snippet;
114
123
  virtualEl?: ReadableBox<Measurable | null>;
115
124
  ref: ReadableBox<HTMLElement | null>;
125
+ /**
126
+ * Tooltips are special in that they are commonly composed
127
+ * with other floating components, where the same trigger is
128
+ * used for both the tooltip and the popover.
129
+ *
130
+ * For situations like this, we need to use a different context
131
+ * symbol so that conflicts don't occur.
132
+ */
133
+ tooltip?: boolean;
116
134
  };
@@ -942,10 +942,10 @@ declare class FloatingAnchorState {
942
942
  readonly root: FloatingRootState;
943
943
  constructor(opts: FloatingAnchorStateProps, root: FloatingRootState);
944
944
  }
945
- export declare function useFloatingRootState(): FloatingRootState;
946
- export declare function useFloatingContentState(props: FloatingContentStateProps): FloatingContentState;
945
+ export declare function useFloatingRootState(tooltip?: boolean): FloatingRootState;
946
+ export declare function useFloatingContentState(props: FloatingContentStateProps, tooltip?: boolean): FloatingContentState;
947
947
  export declare function useFloatingArrowState(props: FloatingArrowStateProps): FloatingArrowState;
948
- export declare function useFloatingAnchorState(props: FloatingAnchorStateProps): FloatingAnchorState;
948
+ export declare function useFloatingAnchorState(props: FloatingAnchorStateProps, tooltip?: boolean): FloatingAnchorState;
949
949
  export declare function getSideFromPlacement(placement: Placement): "left" | "right" | "top" | "bottom";
950
950
  export declare function getAlignFromPlacement(placement: Placement): "end" | "center" | "start";
951
951
  export {};
@@ -140,9 +140,6 @@ class FloatingContentState {
140
140
  "data-align": this.placedAlign,
141
141
  style: styleToString({
142
142
  ...this.#transformedStyle,
143
- // if the FloatingContent hasn't been placed yet (not all measurements done)
144
- // we prevent animations so that users's animation don't kick in too early referring wrong sides
145
- // animation: !this.floating.isPositioned ? "none" : undefined,
146
143
  }),
147
144
  ...attachRef(this.contentRef),
148
145
  }));
@@ -233,17 +230,24 @@ class FloatingAnchorState {
233
230
  }
234
231
  const FloatingRootContext = new Context("Floating.Root");
235
232
  const FloatingContentContext = new Context("Floating.Content");
236
- export function useFloatingRootState() {
237
- return FloatingRootContext.set(new FloatingRootState());
233
+ const FloatingTooltipRootContext = new Context("Floating.Root");
234
+ export function useFloatingRootState(tooltip = false) {
235
+ return tooltip
236
+ ? FloatingTooltipRootContext.set(new FloatingRootState())
237
+ : FloatingRootContext.set(new FloatingRootState());
238
238
  }
239
- export function useFloatingContentState(props) {
240
- return FloatingContentContext.set(new FloatingContentState(props, FloatingRootContext.get()));
239
+ export function useFloatingContentState(props, tooltip = false) {
240
+ return tooltip
241
+ ? FloatingContentContext.set(new FloatingContentState(props, FloatingTooltipRootContext.get()))
242
+ : FloatingContentContext.set(new FloatingContentState(props, FloatingRootContext.get()));
241
243
  }
242
244
  export function useFloatingArrowState(props) {
243
245
  return new FloatingArrowState(props, FloatingContentContext.get());
244
246
  }
245
- export function useFloatingAnchorState(props) {
246
- return new FloatingAnchorState(props, FloatingRootContext.get());
247
+ export function useFloatingAnchorState(props, tooltip = false) {
248
+ return tooltip
249
+ ? new FloatingAnchorState(props, FloatingTooltipRootContext.get())
250
+ : new FloatingAnchorState(props, FloatingRootContext.get());
247
251
  }
248
252
  //
249
253
  // HELPERS
@@ -45,6 +45,7 @@
45
45
  isStatic = false,
46
46
  enabled,
47
47
  ref,
48
+ tooltip = false,
48
49
  ...restProps
49
50
  }: Omit<PopperLayerImplProps, "present" | "children"> & {
50
51
  enabled: boolean;
@@ -72,6 +73,7 @@
72
73
  {onPlaced}
73
74
  {customAnchor}
74
75
  {enabled}
76
+ {tooltip}
75
77
  >
76
78
  {#snippet content({ props: floatingProps, wrapperProps })}
77
79
  {#if restProps.forceMount && enabled}
@@ -22,4 +22,13 @@ export type PopperLayerImplProps = Omit<EscapeLayerImplProps & DismissibleLayerI
22
22
  }
23
23
  ]>;
24
24
  isStatic?: boolean;
25
+ /**
26
+ * Tooltips are special in that they are commonly composed
27
+ * with other floating components, where the same trigger is
28
+ * used for both the tooltip and the popover.
29
+ *
30
+ * For situations like this, we need to use a different context
31
+ * symbol so that conflicts don't occur.
32
+ */
33
+ tooltip?: boolean;
25
34
  }, "enabled">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",