bits-ui 2.15.7 → 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/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 -5
- package/dist/bits/tooltip/tooltip.svelte.js +387 -58
- package/dist/bits/tooltip/types.d.ts +31 -5
- package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.js +14 -2
- package/dist/internal/floating-svelte/use-floating.svelte.js +39 -1
- package/dist/internal/safe-polygon.svelte.d.ts +2 -0
- package/dist/internal/safe-polygon.svelte.js +50 -15
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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));
|
|
@@ -26,10 +123,26 @@ export class TooltipProviderState {
|
|
|
26
123
|
this.#timerFn = new TimeoutFn(() => {
|
|
27
124
|
this.isOpenDelayed = true;
|
|
28
125
|
}, this.opts.skipDelayDuration.current);
|
|
126
|
+
onMountEffect(() => on(window, "scroll", (e) => {
|
|
127
|
+
const activeTooltip = this.#openTooltip;
|
|
128
|
+
if (!activeTooltip)
|
|
129
|
+
return;
|
|
130
|
+
const triggerNode = activeTooltip.triggerNode;
|
|
131
|
+
if (!triggerNode)
|
|
132
|
+
return;
|
|
133
|
+
const target = e.target;
|
|
134
|
+
if (!(target instanceof Element || target instanceof Document))
|
|
135
|
+
return;
|
|
136
|
+
if (target.contains(triggerNode)) {
|
|
137
|
+
activeTooltip.handleClose();
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
29
140
|
}
|
|
30
141
|
#startTimer = () => {
|
|
31
142
|
const skipDuration = this.opts.skipDelayDuration.current;
|
|
32
143
|
if (skipDuration === 0) {
|
|
144
|
+
// no grace period — reset immediately so next trigger waits the full delay
|
|
145
|
+
this.isOpenDelayed = true;
|
|
33
146
|
return;
|
|
34
147
|
}
|
|
35
148
|
else {
|
|
@@ -71,9 +184,10 @@ export class TooltipRootState {
|
|
|
71
184
|
disabled = $derived.by(() => this.opts.disabled.current ?? this.provider.opts.disabled.current);
|
|
72
185
|
ignoreNonKeyboardFocus = $derived.by(() => this.opts.ignoreNonKeyboardFocus.current ??
|
|
73
186
|
this.provider.opts.ignoreNonKeyboardFocus.current);
|
|
187
|
+
registry;
|
|
188
|
+
tether;
|
|
74
189
|
contentNode = $state(null);
|
|
75
190
|
contentPresence;
|
|
76
|
-
triggerNode = $state(null);
|
|
77
191
|
#wasOpenDelayed = $state(false);
|
|
78
192
|
#timerFn;
|
|
79
193
|
stateAttr = $derived.by(() => {
|
|
@@ -84,10 +198,22 @@ export class TooltipRootState {
|
|
|
84
198
|
constructor(opts, provider) {
|
|
85
199
|
this.opts = opts;
|
|
86
200
|
this.provider = provider;
|
|
201
|
+
this.tether = opts.tether.current?.state ?? null;
|
|
202
|
+
this.registry = this.tether?.registry ?? new TooltipTriggerRegistryState();
|
|
87
203
|
this.#timerFn = new TimeoutFn(() => {
|
|
88
204
|
this.#wasOpenDelayed = true;
|
|
89
205
|
this.opts.open.current = true;
|
|
90
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
|
+
}
|
|
91
217
|
this.contentPresence = new PresenceManager({
|
|
92
218
|
open: this.opts.open,
|
|
93
219
|
ref: boxWith(() => this.contentNode),
|
|
@@ -105,16 +231,28 @@ export class TooltipRootState {
|
|
|
105
231
|
});
|
|
106
232
|
watch(() => this.opts.open.current, (isOpen) => {
|
|
107
233
|
if (isOpen) {
|
|
234
|
+
this.ensureActiveTrigger();
|
|
108
235
|
this.provider.onOpen(this);
|
|
109
236
|
}
|
|
110
237
|
else {
|
|
111
238
|
this.provider.onClose(this);
|
|
112
239
|
}
|
|
113
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
|
+
});
|
|
114
251
|
}
|
|
115
252
|
handleOpen = () => {
|
|
116
253
|
this.#timerFn.stop();
|
|
117
254
|
this.#wasOpenDelayed = false;
|
|
255
|
+
this.ensureActiveTrigger();
|
|
118
256
|
this.opts.open.current = true;
|
|
119
257
|
};
|
|
120
258
|
handleClose = () => {
|
|
@@ -127,8 +265,7 @@ export class TooltipRootState {
|
|
|
127
265
|
const delayDuration = this.delayDuration ?? 0;
|
|
128
266
|
// if no delay needed (either skip delay active or delay is 0), open immediately
|
|
129
267
|
if (shouldSkipDelay || delayDuration === 0) {
|
|
130
|
-
|
|
131
|
-
this.#wasOpenDelayed = delayDuration > 0 && shouldSkipDelay;
|
|
268
|
+
this.#wasOpenDelayed = false;
|
|
132
269
|
this.opts.open.current = true;
|
|
133
270
|
}
|
|
134
271
|
else {
|
|
@@ -136,7 +273,8 @@ export class TooltipRootState {
|
|
|
136
273
|
this.#timerFn.start();
|
|
137
274
|
}
|
|
138
275
|
};
|
|
139
|
-
onTriggerEnter = () => {
|
|
276
|
+
onTriggerEnter = (triggerId) => {
|
|
277
|
+
this.setActiveTrigger(triggerId);
|
|
140
278
|
this.#handleDelayedOpen();
|
|
141
279
|
};
|
|
142
280
|
onTriggerLeave = () => {
|
|
@@ -147,25 +285,161 @@ export class TooltipRootState {
|
|
|
147
285
|
this.#timerFn.stop();
|
|
148
286
|
}
|
|
149
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
|
+
}
|
|
150
339
|
}
|
|
151
340
|
export class TooltipTriggerState {
|
|
152
341
|
static create(opts) {
|
|
153
|
-
|
|
342
|
+
if (opts.tether.current) {
|
|
343
|
+
return new TooltipTriggerState(opts, null, opts.tether.current.state);
|
|
344
|
+
}
|
|
345
|
+
return new TooltipTriggerState(opts, TooltipRootContext.get(), null);
|
|
154
346
|
}
|
|
155
347
|
opts;
|
|
156
348
|
root;
|
|
349
|
+
tether;
|
|
157
350
|
attachment;
|
|
158
351
|
#isPointerDown = simpleBox(false);
|
|
159
352
|
#hasPointerMoveOpened = $state(false);
|
|
160
|
-
#isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled);
|
|
161
353
|
domContext;
|
|
162
354
|
#transitCheckTimeout = null;
|
|
163
|
-
|
|
355
|
+
#mounted = false;
|
|
356
|
+
#lastRegisteredId = null;
|
|
357
|
+
constructor(opts, root, tether) {
|
|
164
358
|
this.opts = opts;
|
|
165
359
|
this.root = root;
|
|
360
|
+
this.tether = tether;
|
|
166
361
|
this.domContext = new DOMContext(opts.ref);
|
|
167
|
-
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
|
+
});
|
|
168
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
|
+
};
|
|
169
443
|
#clearTransitCheck = () => {
|
|
170
444
|
if (this.#transitCheckTimeout !== null) {
|
|
171
445
|
clearTimeout(this.#transitCheckTimeout);
|
|
@@ -176,12 +450,12 @@ export class TooltipTriggerState {
|
|
|
176
450
|
this.#isPointerDown.current = false;
|
|
177
451
|
};
|
|
178
452
|
#onpointerup = () => {
|
|
179
|
-
if (this.#isDisabled)
|
|
453
|
+
if (this.#isDisabled())
|
|
180
454
|
return;
|
|
181
455
|
this.#isPointerDown.current = false;
|
|
182
456
|
};
|
|
183
457
|
#onpointerdown = () => {
|
|
184
|
-
if (this.#isDisabled)
|
|
458
|
+
if (this.#isDisabled())
|
|
185
459
|
return;
|
|
186
460
|
this.#isPointerDown.current = true;
|
|
187
461
|
this.domContext.getDocument().addEventListener("pointerup", () => {
|
|
@@ -189,84 +463,133 @@ export class TooltipTriggerState {
|
|
|
189
463
|
}, { once: true });
|
|
190
464
|
};
|
|
191
465
|
#onpointerenter = (e) => {
|
|
192
|
-
|
|
466
|
+
const root = this.#getRoot();
|
|
467
|
+
if (!root)
|
|
468
|
+
return;
|
|
469
|
+
if (this.#isDisabled()) {
|
|
470
|
+
if (root.opts.open.current) {
|
|
471
|
+
root.handleClose();
|
|
472
|
+
}
|
|
193
473
|
return;
|
|
474
|
+
}
|
|
194
475
|
if (e.pointerType === "touch")
|
|
195
476
|
return;
|
|
196
477
|
// if in transit, wait briefly to see if user is actually heading to old content or staying here
|
|
197
|
-
if (
|
|
478
|
+
if (root.provider.isPointerInTransit.current) {
|
|
198
479
|
this.#clearTransitCheck();
|
|
199
480
|
this.#transitCheckTimeout = window.setTimeout(() => {
|
|
200
481
|
// if still in transit after delay, user is likely staying on this trigger
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
482
|
+
if (root.provider.isPointerInTransit.current) {
|
|
483
|
+
root.provider.isPointerInTransit.current = false;
|
|
484
|
+
root.onTriggerEnter(this.opts.id.current);
|
|
204
485
|
this.#hasPointerMoveOpened = true;
|
|
205
486
|
}
|
|
206
487
|
}, 250);
|
|
207
488
|
return;
|
|
208
489
|
}
|
|
209
|
-
|
|
490
|
+
root.onTriggerEnter(this.opts.id.current);
|
|
210
491
|
this.#hasPointerMoveOpened = true;
|
|
211
492
|
};
|
|
212
493
|
#onpointermove = (e) => {
|
|
213
|
-
|
|
494
|
+
const root = this.#getRoot();
|
|
495
|
+
if (!root)
|
|
496
|
+
return;
|
|
497
|
+
if (this.#isDisabled()) {
|
|
498
|
+
if (root.opts.open.current) {
|
|
499
|
+
root.handleClose();
|
|
500
|
+
}
|
|
214
501
|
return;
|
|
502
|
+
}
|
|
215
503
|
if (e.pointerType === "touch")
|
|
216
504
|
return;
|
|
217
505
|
if (this.#hasPointerMoveOpened)
|
|
218
506
|
return;
|
|
219
507
|
// moving within trigger means we're definitely not in transit anymore
|
|
220
508
|
this.#clearTransitCheck();
|
|
221
|
-
|
|
222
|
-
|
|
509
|
+
root.provider.isPointerInTransit.current = false;
|
|
510
|
+
root.onTriggerEnter(this.opts.id.current);
|
|
223
511
|
this.#hasPointerMoveOpened = true;
|
|
224
512
|
};
|
|
225
|
-
#onpointerleave = () => {
|
|
226
|
-
|
|
513
|
+
#onpointerleave = (e) => {
|
|
514
|
+
const root = this.#getRoot();
|
|
515
|
+
if (!root)
|
|
516
|
+
return;
|
|
517
|
+
if (this.#isDisabled())
|
|
227
518
|
return;
|
|
228
519
|
this.#clearTransitCheck();
|
|
229
|
-
this.
|
|
520
|
+
if (!root.isActiveTrigger(this.opts.id.current)) {
|
|
521
|
+
this.#hasPointerMoveOpened = false;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
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
|
+
}
|
|
536
|
+
}
|
|
537
|
+
root.onTriggerLeave();
|
|
230
538
|
this.#hasPointerMoveOpened = false;
|
|
231
539
|
};
|
|
232
540
|
#onfocus = (e) => {
|
|
233
|
-
|
|
541
|
+
const root = this.#getRoot();
|
|
542
|
+
if (!root)
|
|
234
543
|
return;
|
|
235
|
-
if (this.
|
|
544
|
+
if (this.#isPointerDown.current)
|
|
545
|
+
return;
|
|
546
|
+
if (this.#isDisabled()) {
|
|
547
|
+
if (root.opts.open.current) {
|
|
548
|
+
root.handleClose();
|
|
549
|
+
}
|
|
236
550
|
return;
|
|
237
|
-
|
|
551
|
+
}
|
|
552
|
+
if (root.ignoreNonKeyboardFocus && !isFocusVisible(e.currentTarget))
|
|
553
|
+
return;
|
|
554
|
+
root.setActiveTrigger(this.opts.id.current);
|
|
555
|
+
root.handleOpen();
|
|
238
556
|
};
|
|
239
557
|
#onblur = () => {
|
|
240
|
-
|
|
558
|
+
const root = this.#getRoot();
|
|
559
|
+
if (!root || this.#isDisabled())
|
|
241
560
|
return;
|
|
242
|
-
|
|
561
|
+
root.handleClose();
|
|
243
562
|
};
|
|
244
563
|
#onclick = () => {
|
|
245
|
-
|
|
564
|
+
const root = this.#getRoot();
|
|
565
|
+
if (!root || root.disableCloseOnTriggerClick || this.#isDisabled())
|
|
246
566
|
return;
|
|
247
|
-
|
|
567
|
+
root.handleClose();
|
|
248
568
|
};
|
|
249
|
-
props = $derived.by(() =>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
});
|
|
270
593
|
}
|
|
271
594
|
export class TooltipContentState {
|
|
272
595
|
static create(opts) {
|
|
@@ -283,20 +606,26 @@ export class TooltipContentState {
|
|
|
283
606
|
triggerNode: () => this.root.triggerNode,
|
|
284
607
|
contentNode: () => this.root.contentNode,
|
|
285
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
|
+
},
|
|
286
623
|
onPointerExit: () => {
|
|
287
624
|
if (this.root.provider.isTooltipOpen(this.root)) {
|
|
288
625
|
this.root.handleClose();
|
|
289
626
|
}
|
|
290
627
|
},
|
|
291
628
|
});
|
|
292
|
-
onMountEffect(() => on(window, "scroll", (e) => {
|
|
293
|
-
const target = e.target;
|
|
294
|
-
if (!target)
|
|
295
|
-
return;
|
|
296
|
-
if (target.contains(this.root.triggerNode)) {
|
|
297
|
-
this.root.handleClose();
|
|
298
|
-
}
|
|
299
|
-
}));
|
|
300
629
|
}
|
|
301
630
|
onInteractOutside = (e) => {
|
|
302
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>>;
|
|
@@ -181,6 +181,7 @@ export class FloatingContentState {
|
|
|
181
181
|
constructor(opts, root) {
|
|
182
182
|
this.opts = opts;
|
|
183
183
|
this.root = root;
|
|
184
|
+
this.#updatePositionStrategy = opts.updatePositionStrategy;
|
|
184
185
|
if (opts.customAnchor) {
|
|
185
186
|
this.root.customAnchorNode.current = opts.customAnchor.current;
|
|
186
187
|
}
|
|
@@ -208,10 +209,21 @@ export class FloatingContentState {
|
|
|
208
209
|
this.opts.onPlaced?.current();
|
|
209
210
|
});
|
|
210
211
|
watch(() => this.contentRef.current, (contentNode) => {
|
|
211
|
-
if (!contentNode)
|
|
212
|
+
if (!contentNode || !this.opts.enabled.current)
|
|
212
213
|
return;
|
|
213
214
|
const win = getWindow(contentNode);
|
|
214
|
-
|
|
215
|
+
const rafId = win.requestAnimationFrame(() => {
|
|
216
|
+
// avoid applying stale values when refs change quickly
|
|
217
|
+
if (this.contentRef.current !== contentNode || !this.opts.enabled.current)
|
|
218
|
+
return;
|
|
219
|
+
const zIndex = win.getComputedStyle(contentNode).zIndex;
|
|
220
|
+
if (zIndex !== this.contentZIndex) {
|
|
221
|
+
this.contentZIndex = zIndex;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
win.cancelAnimationFrame(rafId);
|
|
226
|
+
};
|
|
215
227
|
});
|
|
216
228
|
$effect(() => {
|
|
217
229
|
this.floating.floating.current = this.wrapperRef.current;
|
|
@@ -22,6 +22,7 @@ export function useFloating(options) {
|
|
|
22
22
|
let placement = $state(placementOption);
|
|
23
23
|
let middlewareData = $state({});
|
|
24
24
|
let isPositioned = $state(false);
|
|
25
|
+
let hasWhileMountedPosition = false;
|
|
25
26
|
const floatingStyles = $derived.by(() => {
|
|
26
27
|
// preserve last known position when floating ref is null (during transitions)
|
|
27
28
|
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
|
|
@@ -82,6 +83,8 @@ export function useFloating(options) {
|
|
|
82
83
|
update();
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
86
|
+
if (!openOption)
|
|
87
|
+
return;
|
|
85
88
|
if (reference.current === null || floating.current === null)
|
|
86
89
|
return;
|
|
87
90
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
@@ -91,8 +94,43 @@ export function useFloating(options) {
|
|
|
91
94
|
isPositioned = false;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
|
|
97
|
+
function trackWhileMountedDeps() {
|
|
98
|
+
return [
|
|
99
|
+
middlewareOption,
|
|
100
|
+
placementOption,
|
|
101
|
+
strategyOption,
|
|
102
|
+
sideOffsetOption,
|
|
103
|
+
alignOffsetOption,
|
|
104
|
+
openOption,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
$effect(() => {
|
|
108
|
+
if (whileElementsMountedOption !== undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (!openOption)
|
|
111
|
+
return;
|
|
112
|
+
update();
|
|
113
|
+
});
|
|
95
114
|
$effect(attach);
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (whileElementsMountedOption === undefined)
|
|
117
|
+
return;
|
|
118
|
+
trackWhileMountedDeps();
|
|
119
|
+
if (!openOption) {
|
|
120
|
+
hasWhileMountedPosition = false;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!isPositioned) {
|
|
124
|
+
hasWhileMountedPosition = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// skip the first post-position run, since autoUpdate already computed it
|
|
128
|
+
if (!hasWhileMountedPosition) {
|
|
129
|
+
hasWhileMountedPosition = true;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
update();
|
|
133
|
+
});
|
|
96
134
|
$effect(reset);
|
|
97
135
|
$effect(() => cleanup);
|
|
98
136
|
return {
|
|
@@ -5,6 +5,8 @@ export interface SafePolygonOptions {
|
|
|
5
5
|
contentNode: Getter<HTMLElement | null>;
|
|
6
6
|
onPointerExit: () => void;
|
|
7
7
|
buffer?: number;
|
|
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[]>;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Creates a safe polygon area that allows users to move their cursor between
|
|
@@ -46,18 +46,34 @@ 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
|
+
#leaveFallbackRafId = null;
|
|
50
|
+
#cancelLeaveFallback() {
|
|
51
|
+
if (this.#leaveFallbackRafId !== null) {
|
|
52
|
+
cancelAnimationFrame(this.#leaveFallbackRafId);
|
|
53
|
+
this.#leaveFallbackRafId = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
#scheduleLeaveFallback() {
|
|
57
|
+
this.#cancelLeaveFallback();
|
|
58
|
+
this.#leaveFallbackRafId = requestAnimationFrame(() => {
|
|
59
|
+
this.#leaveFallbackRafId = null;
|
|
60
|
+
if (!this.#exitPoint || !this.#exitTarget)
|
|
61
|
+
return;
|
|
62
|
+
this.#clearTracking();
|
|
63
|
+
this.#opts.onPointerExit();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
49
66
|
constructor(opts) {
|
|
50
67
|
this.#opts = opts;
|
|
51
68
|
this.#buffer = opts.buffer ?? 1;
|
|
52
69
|
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
53
70
|
if (!triggerNode || !contentNode || !enabled) {
|
|
54
|
-
this.#
|
|
55
|
-
this.#exitTarget = null;
|
|
71
|
+
this.#clearTracking();
|
|
56
72
|
return;
|
|
57
73
|
}
|
|
58
74
|
const doc = getDocument(triggerNode);
|
|
59
75
|
const handlePointerMove = (e) => {
|
|
60
|
-
this.#onPointerMove(e, triggerNode, contentNode);
|
|
76
|
+
this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
|
|
61
77
|
};
|
|
62
78
|
const handleTriggerLeave = (e) => {
|
|
63
79
|
// when leaving trigger toward content, record exit point
|
|
@@ -66,18 +82,34 @@ export class SafePolygon {
|
|
|
66
82
|
if (isElement(target) && contentNode.contains(target)) {
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
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();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
69
102
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
70
103
|
this.#exitTarget = "content";
|
|
104
|
+
this.#scheduleLeaveFallback();
|
|
71
105
|
};
|
|
72
106
|
const handleTriggerEnter = () => {
|
|
73
107
|
// reached trigger, clear tracking
|
|
74
|
-
this.#
|
|
75
|
-
this.#exitTarget = null;
|
|
108
|
+
this.#clearTracking();
|
|
76
109
|
};
|
|
77
110
|
const handleContentEnter = () => {
|
|
78
111
|
// reached content, clear tracking
|
|
79
|
-
this.#
|
|
80
|
-
this.#exitTarget = null;
|
|
112
|
+
this.#clearTracking();
|
|
81
113
|
};
|
|
82
114
|
const handleContentLeave = (e) => {
|
|
83
115
|
// when leaving content, check if going directly back to trigger
|
|
@@ -86,9 +118,10 @@ export class SafePolygon {
|
|
|
86
118
|
// going directly to trigger, no polygon tracking needed
|
|
87
119
|
return;
|
|
88
120
|
}
|
|
89
|
-
//
|
|
121
|
+
// set up polygon tracking toward trigger — pointermove decides whether to close
|
|
90
122
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
91
123
|
this.#exitTarget = "trigger";
|
|
124
|
+
this.#scheduleLeaveFallback();
|
|
92
125
|
};
|
|
93
126
|
return [
|
|
94
127
|
on(doc, "pointermove", handlePointerMove),
|
|
@@ -102,22 +135,20 @@ export class SafePolygon {
|
|
|
102
135
|
}, () => { });
|
|
103
136
|
});
|
|
104
137
|
}
|
|
105
|
-
#onPointerMove(
|
|
138
|
+
#onPointerMove(clientPoint, triggerNode, contentNode) {
|
|
106
139
|
// if no exit point recorded, nothing to check
|
|
107
140
|
if (!this.#exitPoint || !this.#exitTarget)
|
|
108
141
|
return;
|
|
109
|
-
|
|
142
|
+
this.#cancelLeaveFallback();
|
|
110
143
|
const triggerRect = triggerNode.getBoundingClientRect();
|
|
111
144
|
const contentRect = contentNode.getBoundingClientRect();
|
|
112
145
|
// check if pointer reached the target
|
|
113
146
|
if (this.#exitTarget === "content" && isInsideRect(clientPoint, contentRect)) {
|
|
114
|
-
this.#
|
|
115
|
-
this.#exitTarget = null;
|
|
147
|
+
this.#clearTracking();
|
|
116
148
|
return;
|
|
117
149
|
}
|
|
118
150
|
if (this.#exitTarget === "trigger" && isInsideRect(clientPoint, triggerRect)) {
|
|
119
|
-
this.#
|
|
120
|
-
this.#exitTarget = null;
|
|
151
|
+
this.#clearTracking();
|
|
121
152
|
return;
|
|
122
153
|
}
|
|
123
154
|
// check if pointer is in the rectangular corridor between trigger and content
|
|
@@ -133,9 +164,13 @@ export class SafePolygon {
|
|
|
133
164
|
return;
|
|
134
165
|
}
|
|
135
166
|
// pointer is outside all safe zones - close
|
|
167
|
+
this.#clearTracking();
|
|
168
|
+
this.#opts.onPointerExit();
|
|
169
|
+
}
|
|
170
|
+
#clearTracking() {
|
|
136
171
|
this.#exitPoint = null;
|
|
137
172
|
this.#exitTarget = null;
|
|
138
|
-
this.#
|
|
173
|
+
this.#cancelLeaveFallback();
|
|
139
174
|
}
|
|
140
175
|
/**
|
|
141
176
|
* Creates a rectangular corridor between trigger and content
|