bits-ui 2.15.8 → 2.16.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.
@@ -260,7 +260,6 @@ export class PopoverContentState {
260
260
  enabled: () => this.root.opts.open.current &&
261
261
  this.root.openedViaHover &&
262
262
  !this.root.hasInteractedWithContent,
263
- throttlePointerMove: true,
264
263
  onPointerExit: () => {
265
264
  this.root.handleDelayedHoverClose();
266
265
  },
@@ -23,6 +23,7 @@
23
23
  sticky = "partial",
24
24
  strategy,
25
25
  hideWhenDetached = false,
26
+ customAnchor,
26
27
  collisionPadding = 0,
27
28
  onInteractOutside = noop,
28
29
  onEscapeKeydown = noop,
@@ -51,6 +52,7 @@
51
52
  hideWhenDetached,
52
53
  collisionPadding,
53
54
  strategy,
55
+ customAnchor: customAnchor ?? contentState.root.triggerNode,
54
56
  });
55
57
 
56
58
  const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props));
@@ -1,9 +1,12 @@
1
- <script lang="ts">
1
+ <script lang="ts" module>
2
+ type T = unknown;
3
+ </script>
4
+
5
+ <script lang="ts" generics="T = never">
2
6
  import { boxWith, mergeProps } from "svelte-toolbelt";
3
7
  import type { TooltipTriggerProps } from "../types.js";
4
8
  import { TooltipTriggerState } from "../tooltip.svelte.js";
5
9
  import { createId } from "../../../internal/create-id.js";
6
- import FloatingLayerAnchor from "../../utilities/floating-layer/components/floating-layer-anchor.svelte";
7
10
 
8
11
  const uid = $props.id();
9
12
 
@@ -12,16 +15,20 @@
12
15
  child,
13
16
  id = createId(uid),
14
17
  disabled = false,
18
+ payload,
19
+ tether,
15
20
  type = "button",
16
21
  tabindex = 0,
17
22
  ref = $bindable(null),
18
23
  ...restProps
19
- }: TooltipTriggerProps = $props();
24
+ }: TooltipTriggerProps<T> = $props();
20
25
 
21
26
  const triggerState = TooltipTriggerState.create({
22
27
  id: boxWith(() => id),
23
28
  disabled: boxWith(() => disabled ?? false),
24
29
  tabindex: boxWith(() => tabindex ?? 0),
30
+ payload: boxWith(() => payload),
31
+ tether: boxWith(() => tether),
25
32
  ref: boxWith(
26
33
  () => ref,
27
34
  (v) => (ref = v)
@@ -31,12 +38,10 @@
31
38
  const mergedProps = $derived(mergeProps(restProps, triggerState.props, { type }));
32
39
  </script>
33
40
 
34
- <FloatingLayerAnchor {id} ref={triggerState.opts.ref} tooltip={true}>
35
- {#if child}
36
- {@render child({ props: mergedProps })}
37
- {:else}
38
- <button {...mergedProps}>
39
- {@render children?.()}
40
- </button>
41
- {/if}
42
- </FloatingLayerAnchor>
41
+ {#if child}
42
+ {@render child({ props: mergedProps })}
43
+ {:else}
44
+ <button {...mergedProps}>
45
+ {@render children?.()}
46
+ </button>
47
+ {/if}
@@ -1,4 +1,18 @@
1
1
  import type { TooltipTriggerProps } from "../types.js";
2
- declare const TooltipTrigger: import("svelte").Component<TooltipTriggerProps, {}, "ref">;
3
- type TooltipTrigger = ReturnType<typeof TooltipTrigger>;
2
+ declare class __sveltets_Render<T = never> {
3
+ props(): TooltipTriggerProps<T>;
4
+ events(): {};
5
+ slots(): {};
6
+ bindings(): "ref";
7
+ exports(): {};
8
+ }
9
+ interface $$IsomorphicComponent {
10
+ new <T = never>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
11
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
12
+ } & ReturnType<__sveltets_Render<T>['exports']>;
13
+ <T = never>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
14
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
15
+ }
16
+ declare const TooltipTrigger: $$IsomorphicComponent;
17
+ type TooltipTrigger<T = never> = InstanceType<typeof TooltipTrigger<T>>;
4
18
  export default TooltipTrigger;
@@ -1,4 +1,8 @@
1
- <script lang="ts">
1
+ <script lang="ts" module>
2
+ type T = unknown;
3
+ </script>
4
+
5
+ <script lang="ts" generics="T = never">
2
6
  import { boxWith } from "svelte-toolbelt";
3
7
  import type { TooltipRootProps } from "../types.js";
4
8
  import { TooltipRootState } from "../tooltip.svelte.js";
@@ -7,6 +11,7 @@
7
11
 
8
12
  let {
9
13
  open = $bindable(false),
14
+ triggerId = $bindable<string | null>(null),
10
15
  onOpenChange = noop,
11
16
  onOpenChangeComplete = noop,
12
17
  disabled,
@@ -14,10 +19,11 @@
14
19
  disableCloseOnTriggerClick,
15
20
  disableHoverableContent,
16
21
  ignoreNonKeyboardFocus,
22
+ tether,
17
23
  children,
18
- }: TooltipRootProps = $props();
24
+ }: TooltipRootProps<T> = $props();
19
25
 
20
- TooltipRootState.create({
26
+ const rootState = TooltipRootState.create({
21
27
  open: boxWith(
22
28
  () => open,
23
29
  (v) => {
@@ -25,15 +31,26 @@
25
31
  onOpenChange(v);
26
32
  }
27
33
  ),
34
+ triggerId: boxWith(
35
+ () => triggerId,
36
+ (v) => {
37
+ triggerId = v;
38
+ }
39
+ ),
28
40
  delayDuration: boxWith(() => delayDuration),
29
41
  disableCloseOnTriggerClick: boxWith(() => disableCloseOnTriggerClick),
30
42
  disableHoverableContent: boxWith(() => disableHoverableContent),
31
43
  ignoreNonKeyboardFocus: boxWith(() => ignoreNonKeyboardFocus),
32
44
  disabled: boxWith(() => disabled),
33
45
  onOpenChangeComplete: boxWith(() => onOpenChangeComplete),
46
+ tether: boxWith(() => tether),
34
47
  });
35
48
  </script>
36
49
 
37
50
  <FloatingLayer tooltip>
38
- {@render children?.()}
51
+ {@render children?.({
52
+ open: rootState.opts.open.current,
53
+ triggerId: rootState.activeTriggerId,
54
+ payload: rootState.activePayload as [T] extends [never] ? null : T | null,
55
+ })}
39
56
  </FloatingLayer>
@@ -1,3 +1,18 @@
1
- declare const Tooltip: import("svelte").Component<import("../types.js").TooltipRootPropsWithoutHTML, {}, "open">;
2
- type Tooltip = ReturnType<typeof Tooltip>;
1
+ import type { TooltipRootProps } from "../types.js";
2
+ declare class __sveltets_Render<T = never> {
3
+ props(): TooltipRootProps<T>;
4
+ events(): {};
5
+ slots(): {};
6
+ bindings(): "open" | "triggerId";
7
+ exports(): {};
8
+ }
9
+ interface $$IsomorphicComponent {
10
+ new <T = never>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
11
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
12
+ } & ReturnType<__sveltets_Render<T>['exports']>;
13
+ <T = never>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
14
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
15
+ }
16
+ declare const Tooltip: $$IsomorphicComponent;
17
+ type Tooltip<T = never> = InstanceType<typeof Tooltip<T>>;
3
18
  export default Tooltip;
@@ -5,5 +5,6 @@ export { default as Trigger } from "./components/tooltip-trigger.svelte";
5
5
  export { default as Arrow } from "./components/tooltip-arrow.svelte";
6
6
  export { default as Provider } from "./components/tooltip-provider.svelte";
7
7
  export { default as Portal } from "../utilities/portal/portal.svelte";
8
- export type { TooltipProviderPropsWithoutHTML as ProviderProps, TooltipRootPropsWithoutHTML as RootProps, TooltipArrowProps as ArrowProps, TooltipContentProps as ContentProps, TooltipContentStaticProps as ContentStaticProps, TooltipTriggerProps as TriggerProps, } from "./types.js";
8
+ export { createTooltipTether as createTether } from "./tooltip.svelte.js";
9
+ export type { TooltipProviderPropsWithoutHTML as ProviderProps, TooltipRootPropsWithoutHTML as RootProps, TooltipArrowProps as ArrowProps, TooltipContentProps as ContentProps, TooltipContentStaticProps as ContentStaticProps, TooltipTriggerProps as TriggerProps, TooltipTether as Tether, TooltipRootSnippetProps as RootSnippetProps, } from "./types.js";
9
10
  export type { PortalProps } from "../utilities/portal/types.js";
@@ -5,3 +5,4 @@ export { default as Trigger } from "./components/tooltip-trigger.svelte";
5
5
  export { default as Arrow } from "./components/tooltip-arrow.svelte";
6
6
  export { default as Provider } from "./components/tooltip-provider.svelte";
7
7
  export { default as Portal } from "../utilities/portal/portal.svelte";
8
+ export { createTooltipTether as createTether } from "./tooltip.svelte.js";
@@ -3,6 +3,38 @@ import type { OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/type
3
3
  import type { FocusEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
4
4
  import { PresenceManager } from "../../internal/presence-manager.svelte.js";
5
5
  export declare const tooltipAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["content", "trigger"]>;
6
+ type TooltipTriggerRecord = {
7
+ id: string;
8
+ node: HTMLElement | null;
9
+ payload: unknown;
10
+ disabled: boolean;
11
+ };
12
+ declare class TooltipTriggerRegistryState {
13
+ #private;
14
+ triggers: Map<string, TooltipTriggerRecord>;
15
+ activeTriggerId: string | null;
16
+ activeTriggerNode: HTMLElement | null;
17
+ activePayload: {} | null;
18
+ register: (record: TooltipTriggerRecord) => void;
19
+ update: (record: TooltipTriggerRecord) => void;
20
+ unregister: (id: string) => void;
21
+ setActiveTrigger: (id: string | null) => void;
22
+ get: (id: string) => TooltipTriggerRecord | undefined;
23
+ has: (id: string) => boolean;
24
+ getFirstTriggerId: () => string | null;
25
+ }
26
+ declare class TooltipTetherState {
27
+ readonly registry: TooltipTriggerRegistryState;
28
+ root: TooltipRootState | null;
29
+ }
30
+ export declare class TooltipTether<Payload = never> {
31
+ #private;
32
+ get state(): TooltipTetherState;
33
+ open(triggerId: string): void;
34
+ close(): void;
35
+ get isOpen(): boolean;
36
+ }
37
+ export declare function createTooltipTether<Payload = never>(): TooltipTether<Payload>;
6
38
  interface TooltipProviderStateOpts extends ReadableBoxedValues<{
7
39
  delayDuration: number;
8
40
  disableHoverableContent: boolean;
@@ -30,8 +62,10 @@ interface TooltipRootStateOpts extends ReadableBoxedValues<{
30
62
  disabled: boolean | undefined;
31
63
  ignoreNonKeyboardFocus: boolean | undefined;
32
64
  onOpenChangeComplete: OnChangeFn<boolean>;
65
+ tether: TooltipTether<unknown> | undefined;
33
66
  }>, WritableBoxedValues<{
34
67
  open: boolean;
68
+ triggerId: string | null;
35
69
  }> {
36
70
  }
37
71
  export declare class TooltipRootState {
@@ -44,34 +78,47 @@ export declare class TooltipRootState {
44
78
  readonly disableCloseOnTriggerClick: boolean;
45
79
  readonly disabled: boolean;
46
80
  readonly ignoreNonKeyboardFocus: boolean;
81
+ readonly registry: TooltipTriggerRegistryState;
82
+ readonly tether: TooltipTetherState | null;
47
83
  contentNode: HTMLElement | null;
48
84
  contentPresence: PresenceManager;
49
- triggerNode: HTMLElement | null;
50
85
  readonly stateAttr: string;
51
86
  constructor(opts: TooltipRootStateOpts, provider: TooltipProviderState);
52
87
  handleOpen: () => void;
53
88
  handleClose: () => void;
54
- onTriggerEnter: () => void;
89
+ onTriggerEnter: (triggerId: string) => void;
55
90
  onTriggerLeave: () => void;
91
+ ensureActiveTrigger: () => void;
92
+ setActiveTrigger: (triggerId: string | null) => void;
93
+ registerTrigger: (trigger: TooltipTriggerRecord) => void;
94
+ updateTrigger: (trigger: TooltipTriggerRecord) => void;
95
+ unregisterTrigger: (id: string) => void;
96
+ isActiveTrigger: (triggerId: string) => boolean;
97
+ get triggerNode(): HTMLElement | null;
98
+ get activePayload(): {} | null;
99
+ get activeTriggerId(): string | null;
56
100
  }
57
101
  interface TooltipTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
58
102
  disabled: boolean;
59
103
  tabindex: number;
104
+ payload: unknown;
105
+ tether: TooltipTether<unknown> | undefined;
60
106
  }> {
61
107
  }
62
108
  export declare class TooltipTriggerState {
63
109
  #private;
64
110
  static create(opts: TooltipTriggerStateOpts): TooltipTriggerState;
65
111
  readonly opts: TooltipTriggerStateOpts;
66
- readonly root: TooltipRootState;
112
+ readonly root: TooltipRootState | null;
113
+ readonly tether: TooltipTetherState | null;
67
114
  readonly attachment: RefAttachment;
68
115
  domContext: DOMContext;
69
- constructor(opts: TooltipTriggerStateOpts, root: TooltipRootState);
116
+ constructor(opts: TooltipTriggerStateOpts, root: TooltipRootState | null, tether: TooltipTetherState | null);
70
117
  handlePointerUp: () => void;
71
118
  readonly props: {
72
119
  readonly id: string;
73
120
  readonly "aria-describedby": string | undefined;
74
- readonly "data-state": "closed" | "delayed-open" | "instant-open";
121
+ readonly "data-state": "closed" | "delayed-open" | "instant-open" | undefined;
75
122
  readonly "data-disabled": "" | undefined;
76
123
  readonly "data-delay-duration": `${number}`;
77
124
  readonly tabindex: number | undefined;
@@ -92,7 +139,6 @@ interface TooltipContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
92
139
  }> {
93
140
  }
94
141
  export declare class TooltipContentState {
95
- #private;
96
142
  static create(opts: TooltipContentStateOpts): TooltipContentState;
97
143
  readonly opts: TooltipContentStateOpts;
98
144
  readonly root: TooltipRootState;
@@ -12,6 +12,103 @@ export const tooltipAttrs = createBitsAttrs({
12
12
  });
13
13
  const TooltipProviderContext = new Context("Tooltip.Provider");
14
14
  const TooltipRootContext = new Context("Tooltip.Root");
15
+ class TooltipTriggerRegistryState {
16
+ triggers = $state(new Map());
17
+ activeTriggerId = $state(null);
18
+ activeTriggerNode = $derived.by(() => {
19
+ const activeTriggerId = this.activeTriggerId;
20
+ if (activeTriggerId === null)
21
+ return null;
22
+ return this.triggers.get(activeTriggerId)?.node ?? null;
23
+ });
24
+ activePayload = $derived.by(() => {
25
+ const activeTriggerId = this.activeTriggerId;
26
+ if (activeTriggerId === null)
27
+ return null;
28
+ return this.triggers.get(activeTriggerId)?.payload ?? null;
29
+ });
30
+ register = (record) => {
31
+ const next = new Map(this.triggers);
32
+ next.set(record.id, record);
33
+ this.triggers = next;
34
+ this.#coerceActiveTrigger();
35
+ };
36
+ update = (record) => {
37
+ const next = new Map(this.triggers);
38
+ next.set(record.id, record);
39
+ this.triggers = next;
40
+ this.#coerceActiveTrigger();
41
+ };
42
+ unregister = (id) => {
43
+ if (!this.triggers.has(id))
44
+ return;
45
+ const next = new Map(this.triggers);
46
+ next.delete(id);
47
+ this.triggers = next;
48
+ if (this.activeTriggerId === id) {
49
+ this.activeTriggerId = null;
50
+ }
51
+ };
52
+ setActiveTrigger = (id) => {
53
+ if (id === null) {
54
+ this.activeTriggerId = null;
55
+ return;
56
+ }
57
+ if (!this.triggers.has(id)) {
58
+ this.activeTriggerId = null;
59
+ return;
60
+ }
61
+ this.activeTriggerId = id;
62
+ };
63
+ get = (id) => {
64
+ return this.triggers.get(id);
65
+ };
66
+ has = (id) => {
67
+ return this.triggers.has(id);
68
+ };
69
+ getFirstTriggerId = () => {
70
+ const firstEntry = this.triggers.entries().next();
71
+ if (firstEntry.done)
72
+ return null;
73
+ return firstEntry.value[0];
74
+ };
75
+ #coerceActiveTrigger = () => {
76
+ const activeTriggerId = this.activeTriggerId;
77
+ if (activeTriggerId === null)
78
+ return;
79
+ if (!this.triggers.has(activeTriggerId)) {
80
+ this.activeTriggerId = null;
81
+ }
82
+ };
83
+ }
84
+ class TooltipTetherState {
85
+ registry = new TooltipTriggerRegistryState();
86
+ root = $state(null);
87
+ }
88
+ // oxlint-disable-next-line no-unused-vars
89
+ export class TooltipTether {
90
+ #state = new TooltipTetherState();
91
+ get state() {
92
+ return this.#state;
93
+ }
94
+ open(triggerId) {
95
+ if (!this.#state.registry.has(triggerId)) {
96
+ return;
97
+ }
98
+ this.#state.registry.setActiveTrigger(triggerId);
99
+ this.#state.root?.setActiveTrigger(triggerId);
100
+ this.#state.root?.handleOpen();
101
+ }
102
+ close() {
103
+ this.#state.root?.handleClose();
104
+ }
105
+ get isOpen() {
106
+ return this.#state.root?.opts.open.current ?? false;
107
+ }
108
+ }
109
+ export function createTooltipTether() {
110
+ return new TooltipTether();
111
+ }
15
112
  export class TooltipProviderState {
16
113
  static create(opts) {
17
114
  return TooltipProviderContext.set(new TooltipProviderState(opts));
@@ -44,6 +141,8 @@ export class TooltipProviderState {
44
141
  #startTimer = () => {
45
142
  const skipDuration = this.opts.skipDelayDuration.current;
46
143
  if (skipDuration === 0) {
144
+ // no grace period — reset immediately so next trigger waits the full delay
145
+ this.isOpenDelayed = true;
47
146
  return;
48
147
  }
49
148
  else {
@@ -85,9 +184,10 @@ export class TooltipRootState {
85
184
  disabled = $derived.by(() => this.opts.disabled.current ?? this.provider.opts.disabled.current);
86
185
  ignoreNonKeyboardFocus = $derived.by(() => this.opts.ignoreNonKeyboardFocus.current ??
87
186
  this.provider.opts.ignoreNonKeyboardFocus.current);
187
+ registry;
188
+ tether;
88
189
  contentNode = $state(null);
89
190
  contentPresence;
90
- triggerNode = $state(null);
91
191
  #wasOpenDelayed = $state(false);
92
192
  #timerFn;
93
193
  stateAttr = $derived.by(() => {
@@ -98,10 +198,22 @@ export class TooltipRootState {
98
198
  constructor(opts, provider) {
99
199
  this.opts = opts;
100
200
  this.provider = provider;
201
+ this.tether = opts.tether.current?.state ?? null;
202
+ this.registry = this.tether?.registry ?? new TooltipTriggerRegistryState();
101
203
  this.#timerFn = new TimeoutFn(() => {
102
204
  this.#wasOpenDelayed = true;
103
205
  this.opts.open.current = true;
104
206
  }, this.delayDuration ?? 0);
207
+ if (this.tether) {
208
+ this.tether.root = this;
209
+ onMountEffect(() => {
210
+ return () => {
211
+ if (this.tether?.root === this) {
212
+ this.tether.root = null;
213
+ }
214
+ };
215
+ });
216
+ }
105
217
  this.contentPresence = new PresenceManager({
106
218
  open: this.opts.open,
107
219
  ref: boxWith(() => this.contentNode),
@@ -119,16 +231,28 @@ export class TooltipRootState {
119
231
  });
120
232
  watch(() => this.opts.open.current, (isOpen) => {
121
233
  if (isOpen) {
234
+ this.ensureActiveTrigger();
122
235
  this.provider.onOpen(this);
123
236
  }
124
237
  else {
125
238
  this.provider.onClose(this);
126
239
  }
127
240
  }, { lazy: true });
241
+ watch(() => this.opts.triggerId.current, (triggerId) => {
242
+ if (triggerId === this.registry.activeTriggerId)
243
+ return;
244
+ this.registry.setActiveTrigger(triggerId);
245
+ });
246
+ watch(() => this.registry.activeTriggerId, (activeTriggerId) => {
247
+ if (this.opts.triggerId.current === activeTriggerId)
248
+ return;
249
+ this.opts.triggerId.current = activeTriggerId;
250
+ });
128
251
  }
129
252
  handleOpen = () => {
130
253
  this.#timerFn.stop();
131
254
  this.#wasOpenDelayed = false;
255
+ this.ensureActiveTrigger();
132
256
  this.opts.open.current = true;
133
257
  };
134
258
  handleClose = () => {
@@ -141,8 +265,7 @@ export class TooltipRootState {
141
265
  const delayDuration = this.delayDuration ?? 0;
142
266
  // if no delay needed (either skip delay active or delay is 0), open immediately
143
267
  if (shouldSkipDelay || delayDuration === 0) {
144
- // set wasOpenDelayed based on whether we actually had a delay
145
- this.#wasOpenDelayed = delayDuration > 0 && shouldSkipDelay;
268
+ this.#wasOpenDelayed = false;
146
269
  this.opts.open.current = true;
147
270
  }
148
271
  else {
@@ -150,7 +273,8 @@ export class TooltipRootState {
150
273
  this.#timerFn.start();
151
274
  }
152
275
  };
153
- onTriggerEnter = () => {
276
+ onTriggerEnter = (triggerId) => {
277
+ this.setActiveTrigger(triggerId);
154
278
  this.#handleDelayedOpen();
155
279
  };
156
280
  onTriggerLeave = () => {
@@ -161,25 +285,161 @@ export class TooltipRootState {
161
285
  this.#timerFn.stop();
162
286
  }
163
287
  };
288
+ ensureActiveTrigger = () => {
289
+ if (this.registry.activeTriggerId !== null &&
290
+ this.registry.has(this.registry.activeTriggerId)) {
291
+ return;
292
+ }
293
+ if (this.opts.triggerId.current !== null &&
294
+ this.registry.has(this.opts.triggerId.current)) {
295
+ this.registry.setActiveTrigger(this.opts.triggerId.current);
296
+ return;
297
+ }
298
+ const firstTriggerId = this.registry.getFirstTriggerId();
299
+ this.registry.setActiveTrigger(firstTriggerId);
300
+ };
301
+ setActiveTrigger = (triggerId) => {
302
+ this.registry.setActiveTrigger(triggerId);
303
+ };
304
+ registerTrigger = (trigger) => {
305
+ this.registry.register(trigger);
306
+ if (trigger.disabled &&
307
+ this.registry.activeTriggerId === trigger.id &&
308
+ this.opts.open.current) {
309
+ this.handleClose();
310
+ }
311
+ };
312
+ updateTrigger = (trigger) => {
313
+ this.registry.update(trigger);
314
+ if (trigger.disabled &&
315
+ this.registry.activeTriggerId === trigger.id &&
316
+ this.opts.open.current) {
317
+ this.handleClose();
318
+ }
319
+ };
320
+ unregisterTrigger = (id) => {
321
+ const isActive = this.registry.activeTriggerId === id;
322
+ this.registry.unregister(id);
323
+ if (isActive && this.opts.open.current) {
324
+ this.handleClose();
325
+ }
326
+ };
327
+ isActiveTrigger = (triggerId) => {
328
+ return this.registry.activeTriggerId === triggerId;
329
+ };
330
+ get triggerNode() {
331
+ return this.registry.activeTriggerNode;
332
+ }
333
+ get activePayload() {
334
+ return this.registry.activePayload;
335
+ }
336
+ get activeTriggerId() {
337
+ return this.registry.activeTriggerId;
338
+ }
164
339
  }
165
340
  export class TooltipTriggerState {
166
341
  static create(opts) {
167
- return new TooltipTriggerState(opts, TooltipRootContext.get());
342
+ if (opts.tether.current) {
343
+ return new TooltipTriggerState(opts, null, opts.tether.current.state);
344
+ }
345
+ return new TooltipTriggerState(opts, TooltipRootContext.get(), null);
168
346
  }
169
347
  opts;
170
348
  root;
349
+ tether;
171
350
  attachment;
172
351
  #isPointerDown = simpleBox(false);
173
352
  #hasPointerMoveOpened = $state(false);
174
- #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled);
175
353
  domContext;
176
354
  #transitCheckTimeout = null;
177
- constructor(opts, root) {
355
+ #mounted = false;
356
+ #lastRegisteredId = null;
357
+ constructor(opts, root, tether) {
178
358
  this.opts = opts;
179
359
  this.root = root;
360
+ this.tether = tether;
180
361
  this.domContext = new DOMContext(opts.ref);
181
- this.attachment = attachRef(this.opts.ref, (v) => (this.root.triggerNode = v));
362
+ this.attachment = attachRef(this.opts.ref, (v) => this.#register(v));
363
+ watch(() => this.opts.id.current, () => {
364
+ this.#register(this.opts.ref.current);
365
+ });
366
+ watch(() => this.opts.payload.current, () => {
367
+ this.#register(this.opts.ref.current);
368
+ });
369
+ watch(() => this.opts.disabled.current, () => {
370
+ this.#register(this.opts.ref.current);
371
+ });
372
+ onMountEffect(() => {
373
+ this.#mounted = true;
374
+ this.#register(this.opts.ref.current);
375
+ return () => {
376
+ const root = this.#getRoot();
377
+ const id = this.#lastRegisteredId;
378
+ if (id) {
379
+ if (this.tether) {
380
+ this.tether.registry.unregister(id);
381
+ }
382
+ else {
383
+ root?.unregisterTrigger(id);
384
+ }
385
+ }
386
+ this.#lastRegisteredId = null;
387
+ this.#mounted = false;
388
+ };
389
+ });
182
390
  }
391
+ #getRoot = () => {
392
+ return this.tether?.root ?? this.root;
393
+ };
394
+ #isDisabled = () => {
395
+ const root = this.#getRoot();
396
+ return this.opts.disabled.current || Boolean(root?.disabled);
397
+ };
398
+ #register = (node) => {
399
+ if (!this.#mounted)
400
+ return;
401
+ const id = this.opts.id.current;
402
+ const payload = this.opts.payload.current;
403
+ const disabled = this.opts.disabled.current;
404
+ if (this.#lastRegisteredId && this.#lastRegisteredId !== id) {
405
+ const root = this.#getRoot();
406
+ if (this.tether) {
407
+ this.tether.registry.unregister(this.#lastRegisteredId);
408
+ }
409
+ else {
410
+ root?.unregisterTrigger(this.#lastRegisteredId);
411
+ }
412
+ }
413
+ const triggerRecord = {
414
+ id,
415
+ node,
416
+ payload,
417
+ disabled,
418
+ };
419
+ const root = this.#getRoot();
420
+ if (this.tether) {
421
+ if (this.tether.registry.has(id)) {
422
+ this.tether.registry.update(triggerRecord);
423
+ }
424
+ else {
425
+ this.tether.registry.register(triggerRecord);
426
+ }
427
+ if (disabled &&
428
+ this.tether.registry.activeTriggerId === id &&
429
+ root?.opts.open.current) {
430
+ root.handleClose();
431
+ }
432
+ }
433
+ else {
434
+ if (root?.registry.has(id)) {
435
+ root.updateTrigger(triggerRecord);
436
+ }
437
+ else {
438
+ root?.registerTrigger(triggerRecord);
439
+ }
440
+ }
441
+ this.#lastRegisteredId = id;
442
+ };
183
443
  #clearTransitCheck = () => {
184
444
  if (this.#transitCheckTimeout !== null) {
185
445
  clearTimeout(this.#transitCheckTimeout);
@@ -190,12 +450,12 @@ export class TooltipTriggerState {
190
450
  this.#isPointerDown.current = false;
191
451
  };
192
452
  #onpointerup = () => {
193
- if (this.#isDisabled)
453
+ if (this.#isDisabled())
194
454
  return;
195
455
  this.#isPointerDown.current = false;
196
456
  };
197
457
  #onpointerdown = () => {
198
- if (this.#isDisabled)
458
+ if (this.#isDisabled())
199
459
  return;
200
460
  this.#isPointerDown.current = true;
201
461
  this.domContext.getDocument().addEventListener("pointerup", () => {
@@ -203,94 +463,133 @@ export class TooltipTriggerState {
203
463
  }, { once: true });
204
464
  };
205
465
  #onpointerenter = (e) => {
206
- if (this.#isDisabled)
466
+ const root = this.#getRoot();
467
+ if (!root)
207
468
  return;
469
+ if (this.#isDisabled()) {
470
+ if (root.opts.open.current) {
471
+ root.handleClose();
472
+ }
473
+ return;
474
+ }
208
475
  if (e.pointerType === "touch")
209
476
  return;
210
477
  // if in transit, wait briefly to see if user is actually heading to old content or staying here
211
- if (this.root.provider.isPointerInTransit.current) {
478
+ if (root.provider.isPointerInTransit.current) {
212
479
  this.#clearTransitCheck();
213
480
  this.#transitCheckTimeout = window.setTimeout(() => {
214
481
  // if still in transit after delay, user is likely staying on this trigger
215
- if (this.root.provider.isPointerInTransit.current) {
216
- this.root.provider.isPointerInTransit.current = false;
217
- this.root.onTriggerEnter();
482
+ if (root.provider.isPointerInTransit.current) {
483
+ root.provider.isPointerInTransit.current = false;
484
+ root.onTriggerEnter(this.opts.id.current);
218
485
  this.#hasPointerMoveOpened = true;
219
486
  }
220
487
  }, 250);
221
488
  return;
222
489
  }
223
- this.root.onTriggerEnter();
490
+ root.onTriggerEnter(this.opts.id.current);
224
491
  this.#hasPointerMoveOpened = true;
225
492
  };
226
493
  #onpointermove = (e) => {
227
- if (this.#isDisabled)
494
+ const root = this.#getRoot();
495
+ if (!root)
228
496
  return;
497
+ if (this.#isDisabled()) {
498
+ if (root.opts.open.current) {
499
+ root.handleClose();
500
+ }
501
+ return;
502
+ }
229
503
  if (e.pointerType === "touch")
230
504
  return;
231
505
  if (this.#hasPointerMoveOpened)
232
506
  return;
233
507
  // moving within trigger means we're definitely not in transit anymore
234
508
  this.#clearTransitCheck();
235
- this.root.provider.isPointerInTransit.current = false;
236
- this.root.onTriggerEnter();
509
+ root.provider.isPointerInTransit.current = false;
510
+ root.onTriggerEnter(this.opts.id.current);
237
511
  this.#hasPointerMoveOpened = true;
238
512
  };
239
513
  #onpointerleave = (e) => {
240
- if (this.#isDisabled)
514
+ const root = this.#getRoot();
515
+ if (!root)
516
+ return;
517
+ if (this.#isDisabled())
241
518
  return;
242
519
  this.#clearTransitCheck();
243
- const relatedTarget = e.relatedTarget;
244
- if (!this.root.disableHoverableContent &&
245
- this.root.opts.open.current &&
246
- isElement(relatedTarget) &&
247
- this.root.contentNode &&
248
- !this.root.contentNode.contains(relatedTarget)) {
249
- this.root.handleClose();
520
+ if (!root.isActiveTrigger(this.opts.id.current)) {
521
+ this.#hasPointerMoveOpened = false;
522
+ return;
250
523
  }
251
- else {
252
- this.root.onTriggerLeave();
524
+ const relatedTarget = e.relatedTarget;
525
+ // when moving to a sibling trigger and skip delay is active, don't close —
526
+ // the sibling's enter handler will switch the active trigger instantly.
527
+ // if skipDelayDuration is 0 there's no grace period, so let the tooltip
528
+ // close and make the sibling wait through the full delay (and re-animate).
529
+ if (isElement(relatedTarget) && root.provider.opts.skipDelayDuration.current > 0) {
530
+ for (const record of root.registry.triggers.values()) {
531
+ if (record.node === relatedTarget) {
532
+ this.#hasPointerMoveOpened = false;
533
+ return;
534
+ }
535
+ }
253
536
  }
537
+ root.onTriggerLeave();
254
538
  this.#hasPointerMoveOpened = false;
255
539
  };
256
540
  #onfocus = (e) => {
257
- if (this.#isPointerDown.current || this.#isDisabled)
541
+ const root = this.#getRoot();
542
+ if (!root)
543
+ return;
544
+ if (this.#isPointerDown.current)
258
545
  return;
259
- if (this.root.ignoreNonKeyboardFocus && !isFocusVisible(e.currentTarget))
546
+ if (this.#isDisabled()) {
547
+ if (root.opts.open.current) {
548
+ root.handleClose();
549
+ }
550
+ return;
551
+ }
552
+ if (root.ignoreNonKeyboardFocus && !isFocusVisible(e.currentTarget))
260
553
  return;
261
- this.root.handleOpen();
554
+ root.setActiveTrigger(this.opts.id.current);
555
+ root.handleOpen();
262
556
  };
263
557
  #onblur = () => {
264
- if (this.#isDisabled)
558
+ const root = this.#getRoot();
559
+ if (!root || this.#isDisabled())
265
560
  return;
266
- this.root.handleClose();
561
+ root.handleClose();
267
562
  };
268
563
  #onclick = () => {
269
- if (this.root.disableCloseOnTriggerClick || this.#isDisabled)
564
+ const root = this.#getRoot();
565
+ if (!root || root.disableCloseOnTriggerClick || this.#isDisabled())
270
566
  return;
271
- this.root.handleClose();
567
+ root.handleClose();
272
568
  };
273
- props = $derived.by(() => ({
274
- id: this.opts.id.current,
275
- "aria-describedby": this.root.opts.open.current
276
- ? this.root.contentNode?.id
277
- : undefined,
278
- "data-state": this.root.stateAttr,
279
- "data-disabled": boolToEmptyStrOrUndef(this.#isDisabled),
280
- "data-delay-duration": `${this.root.delayDuration}`,
281
- [tooltipAttrs.trigger]: "",
282
- tabindex: this.#isDisabled ? undefined : this.opts.tabindex.current,
283
- disabled: this.opts.disabled.current,
284
- onpointerup: this.#onpointerup,
285
- onpointerdown: this.#onpointerdown,
286
- onpointerenter: this.#onpointerenter,
287
- onpointermove: this.#onpointermove,
288
- onpointerleave: this.#onpointerleave,
289
- onfocus: this.#onfocus,
290
- onblur: this.#onblur,
291
- onclick: this.#onclick,
292
- ...this.attachment,
293
- }));
569
+ props = $derived.by(() => {
570
+ const root = this.#getRoot();
571
+ const isOpenForTrigger = Boolean(root?.opts.open.current && root.isActiveTrigger(this.opts.id.current));
572
+ const isDisabled = this.#isDisabled();
573
+ return {
574
+ id: this.opts.id.current,
575
+ "aria-describedby": isOpenForTrigger ? root?.contentNode?.id : undefined,
576
+ "data-state": isOpenForTrigger ? root?.stateAttr : "closed",
577
+ "data-disabled": boolToEmptyStrOrUndef(isDisabled),
578
+ "data-delay-duration": `${root?.delayDuration ?? 0}`,
579
+ [tooltipAttrs.trigger]: "",
580
+ tabindex: isDisabled ? undefined : this.opts.tabindex.current,
581
+ disabled: this.opts.disabled.current,
582
+ onpointerup: this.#onpointerup,
583
+ onpointerdown: this.#onpointerdown,
584
+ onpointerenter: this.#onpointerenter,
585
+ onpointermove: this.#onpointermove,
586
+ onpointerleave: this.#onpointerleave,
587
+ onfocus: this.#onfocus,
588
+ onblur: this.#onblur,
589
+ onclick: this.#onclick,
590
+ ...this.attachment,
591
+ };
592
+ });
294
593
  }
295
594
  export class TooltipContentState {
296
595
  static create(opts) {
@@ -299,30 +598,34 @@ export class TooltipContentState {
299
598
  opts;
300
599
  root;
301
600
  attachment;
302
- #safePolygon = null;
303
- #createSafePolygon = () => {
304
- if (this.#safePolygon)
305
- return;
306
- this.#safePolygon = new SafePolygon({
601
+ constructor(opts, root) {
602
+ this.opts = opts;
603
+ this.root = root;
604
+ this.attachment = attachRef(this.opts.ref, (v) => (this.root.contentNode = v));
605
+ new SafePolygon({
307
606
  triggerNode: () => this.root.triggerNode,
308
607
  contentNode: () => this.root.contentNode,
309
608
  enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
609
+ ignoredTargets: () => {
610
+ // only skip closing for sibling triggers when there's a skip-delay grace period;
611
+ // with skipDelayDuration=0 the close+reopen is intentional (full delay + re-animation)
612
+ if (this.root.provider.opts.skipDelayDuration.current === 0)
613
+ return [];
614
+ const nodes = [];
615
+ const activeTriggerNode = this.root.triggerNode;
616
+ for (const record of this.root.registry.triggers.values()) {
617
+ if (record.node && record.node !== activeTriggerNode) {
618
+ nodes.push(record.node);
619
+ }
620
+ }
621
+ return nodes;
622
+ },
310
623
  onPointerExit: () => {
311
624
  if (this.root.provider.isTooltipOpen(this.root)) {
312
625
  this.root.handleClose();
313
626
  }
314
627
  },
315
628
  });
316
- };
317
- constructor(opts, root) {
318
- this.opts = opts;
319
- this.root = root;
320
- this.attachment = attachRef(this.opts.ref, (v) => (this.root.contentNode = v));
321
- watch(() => this.root.opts.open.current && !this.root.disableHoverableContent, (shouldTrackSafePolygon) => {
322
- if (!shouldTrackSafePolygon)
323
- return;
324
- this.#createSafePolygon();
325
- }, { lazy: true });
326
629
  }
327
630
  onInteractOutside = (e) => {
328
631
  if (isElement(e.target) &&
@@ -2,10 +2,18 @@ import type { FloatingLayerContentProps } from "../utilities/floating-layer/type
2
2
  import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
3
3
  import type { DismissibleLayerProps } from "../utilities/dismissible-layer/types.js";
4
4
  import type { EscapeLayerProps } from "../utilities/escape-layer/types.js";
5
+ import type { Snippet } from "svelte";
5
6
  import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, WithChildren, Without } from "../../internal/types.js";
6
7
  import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
7
8
  import type { PortalProps } from "../utilities/portal/types.js";
8
9
  import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "../../shared/types.js";
10
+ import type { TooltipTether as TooltipTetherImpl } from "./tooltip.svelte.js";
11
+ export type TooltipTether<Payload = never> = TooltipTetherImpl<Payload>;
12
+ export type TooltipRootSnippetProps<Payload = never> = {
13
+ open: boolean;
14
+ triggerId: string | null;
15
+ payload: [Payload] extends [never] ? null : Payload | null;
16
+ };
9
17
  export type TooltipProviderPropsWithoutHTML = WithChildren<{
10
18
  /**
11
19
  * The delay in milliseconds before the tooltip opens.
@@ -46,7 +54,7 @@ export type TooltipProviderPropsWithoutHTML = WithChildren<{
46
54
  ignoreNonKeyboardFocus?: boolean;
47
55
  }>;
48
56
  export type TooltipProviderProps = TooltipProviderPropsWithoutHTML;
49
- export type TooltipRootPropsWithoutHTML = WithChildren<{
57
+ export type TooltipRootPropsWithoutHTML<Payload = never> = Omit<WithChildren<{
50
58
  /**
51
59
  * The open state of the tooltip.
52
60
  *
@@ -92,8 +100,18 @@ export type TooltipRootPropsWithoutHTML = WithChildren<{
92
100
  * @defaultValue false
93
101
  */
94
102
  ignoreNonKeyboardFocus?: boolean;
95
- }>;
96
- export type TooltipRootProps = TooltipRootPropsWithoutHTML;
103
+ /**
104
+ * The active trigger id for controlled single tooltip mode.
105
+ */
106
+ triggerId?: string | null;
107
+ /**
108
+ * Shared tether used to connect detached triggers and infer payload types.
109
+ */
110
+ tether?: TooltipTether<Payload> | undefined;
111
+ }>, "children"> & {
112
+ children?: Snippet | Snippet<[TooltipRootSnippetProps<Payload>]>;
113
+ };
114
+ export type TooltipRootProps<Payload = never> = TooltipRootPropsWithoutHTML<Payload>;
97
115
  export type TooltipContentPropsWithoutHTML = WithChildNoChildrenSnippetProps<Omit<FloatingLayerContentProps, "content" | "preventScroll"> & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & EscapeLayerProps & {
98
116
  /**
99
117
  * When `true`, the tooltip will be forced to mount in the DOM.
@@ -116,12 +134,20 @@ export type TooltipArrowPropsWithoutHTML = ArrowPropsWithoutHTML;
116
134
  export type TooltipArrowProps = ArrowProps;
117
135
  export type TooltipPortalPropsWithoutHTML = PortalProps;
118
136
  export type TooltipPortalProps = PortalProps;
119
- export type TooltipTriggerPropsWithoutHTML = WithChild<{
137
+ export type TooltipTriggerPropsWithoutHTML<Payload = never> = WithChild<{
120
138
  /**
121
139
  * Whether the tooltip trigger is disabled or not.
122
140
  *
123
141
  * @defaultValue false
124
142
  */
125
143
  disabled?: boolean | null | undefined;
144
+ /**
145
+ * Payload for the trigger used by singleton tooltip root snippets.
146
+ */
147
+ payload?: [Payload] extends [never] ? unknown : Payload;
148
+ /**
149
+ * Shared tether used to connect detached triggers and infer payload types.
150
+ */
151
+ tether?: TooltipTether<Payload> | undefined;
126
152
  }>;
127
- export type TooltipTriggerProps = TooltipTriggerPropsWithoutHTML & Without<BitsPrimitiveButtonAttributes, TooltipTriggerPropsWithoutHTML>;
153
+ export type TooltipTriggerProps<Payload = never> = TooltipTriggerPropsWithoutHTML<Payload> & Without<BitsPrimitiveButtonAttributes, TooltipTriggerPropsWithoutHTML<Payload>>;
@@ -5,7 +5,8 @@ export interface SafePolygonOptions {
5
5
  contentNode: Getter<HTMLElement | null>;
6
6
  onPointerExit: () => void;
7
7
  buffer?: number;
8
- throttlePointerMove?: boolean;
8
+ /** nodes that should not trigger a close when they become the relatedTarget on trigger leave (e.g. sibling triggers in singleton mode) */
9
+ ignoredTargets?: Getter<HTMLElement[]>;
9
10
  }
10
11
  /**
11
12
  * Creates a safe polygon area that allows users to move their cursor between
@@ -46,10 +46,6 @@ export class SafePolygon {
46
46
  #exitPoint = null;
47
47
  // tracks what we're moving toward: "content" when leaving trigger, "trigger" when leaving content
48
48
  #exitTarget = null;
49
- #triggerRect = null;
50
- #contentRect = null;
51
- #pointerMoveRafId = null;
52
- #pendingClientPoint = null;
53
49
  #leaveFallbackRafId = null;
54
50
  #cancelLeaveFallback() {
55
51
  if (this.#leaveFallbackRafId !== null) {
@@ -67,15 +63,6 @@ export class SafePolygon {
67
63
  this.#opts.onPointerExit();
68
64
  });
69
65
  }
70
- #closeIfPointerEnteredOutside(target, triggerNode, contentNode) {
71
- if (!isElement(target))
72
- return false;
73
- if (triggerNode.contains(target) || contentNode.contains(target))
74
- return false;
75
- this.#clearTracking();
76
- this.#opts.onPointerExit();
77
- return true;
78
- }
79
66
  constructor(opts) {
80
67
  this.#opts = opts;
81
68
  this.#buffer = opts.buffer ?? 1;
@@ -86,12 +73,7 @@ export class SafePolygon {
86
73
  }
87
74
  const doc = getDocument(triggerNode);
88
75
  const handlePointerMove = (e) => {
89
- if (this.#opts.throttlePointerMove) {
90
- this.#onPointerMoveThrottled(e, triggerNode, contentNode);
91
- }
92
- else {
93
- this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
94
- }
76
+ this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
95
77
  };
96
78
  const handleTriggerLeave = (e) => {
97
79
  // when leaving trigger toward content, record exit point
@@ -100,12 +82,25 @@ export class SafePolygon {
100
82
  if (isElement(target) && contentNode.contains(target)) {
101
83
  return;
102
84
  }
103
- if (this.#closeIfPointerEnteredOutside(target, triggerNode, contentNode)) {
85
+ // if moving to an ignored target (e.g. a sibling trigger), don't close —
86
+ // the sibling's enter handler will take over
87
+ const ignoredTargets = this.#opts.ignoredTargets?.() ?? [];
88
+ if (isElement(target) &&
89
+ ignoredTargets.some((n) => n === target || n.contains(target))) {
90
+ return;
91
+ }
92
+ // if relatedTarget is completely unrelated to the floating tree
93
+ // (not an ancestor of content, not inside content/trigger), close now
94
+ if (isElement(target) &&
95
+ !triggerNode.contains(target) &&
96
+ !contentNode.contains(target) &&
97
+ !target.contains(contentNode)) {
98
+ this.#clearTracking();
99
+ this.#opts.onPointerExit();
104
100
  return;
105
101
  }
106
102
  this.#exitPoint = [e.clientX, e.clientY];
107
103
  this.#exitTarget = "content";
108
- this.#captureRects(triggerNode, contentNode);
109
104
  this.#scheduleLeaveFallback();
110
105
  };
111
106
  const handleTriggerEnter = () => {
@@ -123,13 +118,9 @@ export class SafePolygon {
123
118
  // going directly to trigger, no polygon tracking needed
124
119
  return;
125
120
  }
126
- if (this.#closeIfPointerEnteredOutside(target, triggerNode, contentNode)) {
127
- return;
128
- }
129
- // might be traversing gap back to trigger, set up polygon tracking
121
+ // set up polygon tracking toward trigger — pointermove decides whether to close
130
122
  this.#exitPoint = [e.clientX, e.clientY];
131
123
  this.#exitTarget = "trigger";
132
- this.#captureRects(triggerNode, contentNode);
133
124
  this.#scheduleLeaveFallback();
134
125
  };
135
126
  return [
@@ -144,33 +135,13 @@ export class SafePolygon {
144
135
  }, () => { });
145
136
  });
146
137
  }
147
- #onPointerMoveThrottled(e, triggerNode, contentNode) {
148
- if (this.#pointerMoveRafId === null) {
149
- // handle the first move in the frame immediately so close checks
150
- // are not deferred when only a single pointermove fires.
151
- this.#pointerMoveRafId = requestAnimationFrame(() => {
152
- this.#pointerMoveRafId = null;
153
- const point = this.#pendingClientPoint;
154
- this.#pendingClientPoint = null;
155
- if (!point)
156
- return;
157
- this.#onPointerMove(point, triggerNode, contentNode);
158
- });
159
- this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
160
- return;
161
- }
162
- this.#pendingClientPoint = [e.clientX, e.clientY];
163
- }
164
138
  #onPointerMove(clientPoint, triggerNode, contentNode) {
165
139
  // if no exit point recorded, nothing to check
166
140
  if (!this.#exitPoint || !this.#exitTarget)
167
141
  return;
168
142
  this.#cancelLeaveFallback();
169
- if (!this.#triggerRect || !this.#contentRect) {
170
- this.#captureRects(triggerNode, contentNode);
171
- }
172
- const triggerRect = this.#triggerRect ?? triggerNode.getBoundingClientRect();
173
- const contentRect = this.#contentRect ?? contentNode.getBoundingClientRect();
143
+ const triggerRect = triggerNode.getBoundingClientRect();
144
+ const contentRect = contentNode.getBoundingClientRect();
174
145
  // check if pointer reached the target
175
146
  if (this.#exitTarget === "content" && isInsideRect(clientPoint, contentRect)) {
176
147
  this.#clearTracking();
@@ -196,20 +167,9 @@ export class SafePolygon {
196
167
  this.#clearTracking();
197
168
  this.#opts.onPointerExit();
198
169
  }
199
- #captureRects(triggerNode, contentNode) {
200
- this.#triggerRect = triggerNode.getBoundingClientRect();
201
- this.#contentRect = contentNode.getBoundingClientRect();
202
- }
203
170
  #clearTracking() {
204
171
  this.#exitPoint = null;
205
172
  this.#exitTarget = null;
206
- this.#triggerRect = null;
207
- this.#contentRect = null;
208
- this.#pendingClientPoint = null;
209
- if (this.#pointerMoveRafId !== null) {
210
- cancelAnimationFrame(this.#pointerMoveRafId);
211
- this.#pointerMoveRafId = null;
212
- }
213
173
  this.#cancelLeaveFallback();
214
174
  }
215
175
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.15.8",
3
+ "version": "2.16.0",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",