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.
- package/dist/bits/popover/popover.svelte.js +0 -1
- package/dist/bits/tooltip/components/tooltip-content.svelte +2 -0
- package/dist/bits/tooltip/components/tooltip-trigger.svelte +17 -12
- package/dist/bits/tooltip/components/tooltip-trigger.svelte.d.ts +16 -2
- package/dist/bits/tooltip/components/tooltip.svelte +21 -4
- package/dist/bits/tooltip/components/tooltip.svelte.d.ts +17 -2
- package/dist/bits/tooltip/exports.d.ts +2 -1
- package/dist/bits/tooltip/exports.js +1 -0
- package/dist/bits/tooltip/tooltip.svelte.d.ts +52 -6
- package/dist/bits/tooltip/tooltip.svelte.js +375 -72
- package/dist/bits/tooltip/types.d.ts +31 -5
- package/dist/internal/safe-polygon.svelte.d.ts +2 -1
- package/dist/internal/safe-polygon.svelte.js +19 -59
- package/package.json +1 -1
|
@@ -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
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
3
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
490
|
+
root.onTriggerEnter(this.opts.id.current);
|
|
224
491
|
this.#hasPointerMoveOpened = true;
|
|
225
492
|
};
|
|
226
493
|
#onpointermove = (e) => {
|
|
227
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
514
|
+
const root = this.#getRoot();
|
|
515
|
+
if (!root)
|
|
516
|
+
return;
|
|
517
|
+
if (this.#isDisabled())
|
|
241
518
|
return;
|
|
242
519
|
this.#clearTransitCheck();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
541
|
+
const root = this.#getRoot();
|
|
542
|
+
if (!root)
|
|
543
|
+
return;
|
|
544
|
+
if (this.#isPointerDown.current)
|
|
258
545
|
return;
|
|
259
|
-
if (this
|
|
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.
|
|
554
|
+
root.setActiveTrigger(this.opts.id.current);
|
|
555
|
+
root.handleOpen();
|
|
262
556
|
};
|
|
263
557
|
#onblur = () => {
|
|
264
|
-
|
|
558
|
+
const root = this.#getRoot();
|
|
559
|
+
if (!root || this.#isDisabled())
|
|
265
560
|
return;
|
|
266
|
-
|
|
561
|
+
root.handleClose();
|
|
267
562
|
};
|
|
268
563
|
#onclick = () => {
|
|
269
|
-
|
|
564
|
+
const root = this.#getRoot();
|
|
565
|
+
if (!root || root.disableCloseOnTriggerClick || this.#isDisabled())
|
|
270
566
|
return;
|
|
271
|
-
|
|
567
|
+
root.handleClose();
|
|
272
568
|
};
|
|
273
|
-
props = $derived.by(() =>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
/**
|