INIT
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m11s

This commit is contained in:
eewing
2026-02-18 15:17:47 -06:00
parent e74c106f85
commit ec317eb17c
11532 changed files with 1631690 additions and 1 deletions
@@ -0,0 +1,8 @@
<script lang="ts">
import type { TooltipArrowProps } from "../types.js";
import FloatingLayerArrow from "../../utilities/floating-layer/components/floating-layer-arrow.svelte";
let { ref = $bindable(null), ...restProps }: TooltipArrowProps = $props();
</script>
<FloatingLayerArrow bind:ref {...restProps} />
@@ -0,0 +1,3 @@
declare const TooltipArrow: import("svelte").Component<import("../../utilities/arrow/types.js").ArrowProps, {}, "ref">;
type TooltipArrow = ReturnType<typeof TooltipArrow>;
export default TooltipArrow;
@@ -0,0 +1,98 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { TooltipContentStaticProps } from "../types.js";
import { TooltipContentState } from "../tooltip.svelte.js";
import { createId } from "../../../internal/create-id.js";
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
import { noop } from "../../../internal/noop.js";
const uid = $props.id();
let {
children,
child,
id = createId(uid),
ref = $bindable(null),
onInteractOutside = noop,
onEscapeKeydown = noop,
forceMount = false,
style,
...restProps
}: TooltipContentStaticProps = $props();
const contentState = TooltipContentState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
onInteractOutside: boxWith(() => onInteractOutside),
onEscapeKeydown: boxWith(() => onEscapeKeydown),
});
const mergedProps = $derived(mergeProps(restProps, contentState.props));
</script>
{#if forceMount}
<PopperLayerForceMount
{...mergedProps}
{...contentState.popperProps}
isStatic
enabled={contentState.root.opts.open.current}
{id}
trapFocus={false}
loop={false}
preventScroll={false}
forceMount={true}
ref={contentState.opts.ref}
tooltip={true}
shouldRender={contentState.shouldRender}
>
{#snippet popper({ props })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("tooltip") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, ...contentState.snippetProps })}
{:else}
<div {...finalProps}>
{@render children?.()}
</div>
{/if}
{/snippet}
</PopperLayerForceMount>
{:else if !forceMount}
<PopperLayer
{...mergedProps}
{...contentState.popperProps}
tooltip={true}
isStatic
open={contentState.root.opts.open.current}
{id}
trapFocus={false}
loop={false}
preventScroll={false}
forceMount={false}
ref={contentState.opts.ref}
shouldRender={contentState.shouldRender}
>
{#snippet popper({ props })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("tooltip") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, ...contentState.snippetProps })}
{:else}
<div {...finalProps}>
{@render children?.()}
</div>
{/if}
{/snippet}
</PopperLayer>
{/if}
@@ -0,0 +1,4 @@
import type { TooltipContentStaticProps } from "../types.js";
declare const TooltipContentStatic: import("svelte").Component<TooltipContentStaticProps, {}, "ref">;
type TooltipContentStatic = ReturnType<typeof TooltipContentStatic>;
export default TooltipContentStatic;
@@ -0,0 +1,123 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { TooltipContentProps } from "../types.js";
import { TooltipContentState } from "../tooltip.svelte.js";
import { createId } from "../../../internal/create-id.js";
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
import { noop } from "../../../internal/noop.js";
const uid = $props.id();
let {
children,
child,
id = createId(uid),
ref = $bindable(null),
side = "top",
sideOffset = 0,
align = "center",
avoidCollisions = true,
arrowPadding = 0,
sticky = "partial",
strategy,
hideWhenDetached = false,
collisionPadding = 0,
onInteractOutside = noop,
onEscapeKeydown = noop,
forceMount = false,
style,
...restProps
}: TooltipContentProps = $props();
const contentState = TooltipContentState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
onInteractOutside: boxWith(() => onInteractOutside),
onEscapeKeydown: boxWith(() => onEscapeKeydown),
});
const floatingProps = $derived({
side,
sideOffset,
align,
avoidCollisions,
arrowPadding,
sticky,
hideWhenDetached,
collisionPadding,
strategy,
});
const mergedProps = $derived(mergeProps(restProps, floatingProps, contentState.props));
</script>
{#if forceMount}
<PopperLayerForceMount
{...mergedProps}
{...contentState.popperProps}
enabled={contentState.root.opts.open.current}
{id}
trapFocus={false}
loop={false}
preventScroll={false}
forceMount={true}
ref={contentState.opts.ref}
tooltip={true}
shouldRender={contentState.shouldRender}
contentPointerEvents={contentState.root.disableHoverableContent ? "none" : "auto"}
>
{#snippet popper({ props, wrapperProps })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("tooltip") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
{:else}
<div {...wrapperProps}>
<div {...finalProps}>
{@render children?.()}
</div>
</div>
{/if}
{/snippet}
</PopperLayerForceMount>
{:else if !forceMount}
<PopperLayer
{...mergedProps}
{...contentState.popperProps}
open={contentState.root.opts.open.current}
{id}
trapFocus={false}
loop={false}
preventScroll={false}
forceMount={false}
ref={contentState.opts.ref}
tooltip={true}
shouldRender={contentState.shouldRender}
contentPointerEvents={contentState.root.disableHoverableContent ? "none" : "auto"}
>
{#snippet popper({ props, wrapperProps })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("tooltip") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
{:else}
<div {...wrapperProps}>
<div {...finalProps}>
{@render children?.()}
</div>
</div>
{/if}
{/snippet}
</PopperLayer>
{/if}
@@ -0,0 +1,4 @@
import type { TooltipContentProps } from "../types.js";
declare const TooltipContent: import("svelte").Component<TooltipContentProps, {}, "ref">;
type TooltipContent = ReturnType<typeof TooltipContent>;
export default TooltipContent;
@@ -0,0 +1,26 @@
<script lang="ts">
import { boxWith } from "svelte-toolbelt";
import type { TooltipProviderProps } from "../types.js";
import { TooltipProviderState } from "../tooltip.svelte.js";
let {
children,
delayDuration = 700,
disableCloseOnTriggerClick = false,
disableHoverableContent = false,
disabled = false,
ignoreNonKeyboardFocus = false,
skipDelayDuration = 300,
}: TooltipProviderProps = $props();
TooltipProviderState.create({
delayDuration: boxWith(() => delayDuration),
disableCloseOnTriggerClick: boxWith(() => disableCloseOnTriggerClick),
disableHoverableContent: boxWith(() => disableHoverableContent),
disabled: boxWith(() => disabled),
ignoreNonKeyboardFocus: boxWith(() => ignoreNonKeyboardFocus),
skipDelayDuration: boxWith(() => skipDelayDuration),
});
</script>
{@render children?.()}
@@ -0,0 +1,3 @@
declare const TooltipProvider: import("svelte").Component<import("../types.js").TooltipProviderPropsWithoutHTML, {}, "">;
type TooltipProvider = ReturnType<typeof TooltipProvider>;
export default TooltipProvider;
@@ -0,0 +1,42 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { TooltipTriggerProps } from "../types.js";
import { TooltipTriggerState } from "../tooltip.svelte.js";
import { createId } from "../../../internal/create-id.js";
import FloatingLayerAnchor from "../../utilities/floating-layer/components/floating-layer-anchor.svelte";
const uid = $props.id();
let {
children,
child,
id = createId(uid),
disabled = false,
type = "button",
tabindex = 0,
ref = $bindable(null),
...restProps
}: TooltipTriggerProps = $props();
const triggerState = TooltipTriggerState.create({
id: boxWith(() => id),
disabled: boxWith(() => disabled ?? false),
tabindex: boxWith(() => tabindex ?? 0),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(mergeProps(restProps, triggerState.props, { type }));
</script>
<FloatingLayerAnchor {id} ref={triggerState.opts.ref} tooltip={true}>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}
</FloatingLayerAnchor>
@@ -0,0 +1,4 @@
import type { TooltipTriggerProps } from "../types.js";
declare const TooltipTrigger: import("svelte").Component<TooltipTriggerProps, {}, "ref">;
type TooltipTrigger = ReturnType<typeof TooltipTrigger>;
export default TooltipTrigger;
+39
View File
@@ -0,0 +1,39 @@
<script lang="ts">
import { boxWith } from "svelte-toolbelt";
import type { TooltipRootProps } from "../types.js";
import { TooltipRootState } from "../tooltip.svelte.js";
import FloatingLayer from "../../utilities/floating-layer/components/floating-layer.svelte";
import { noop } from "../../../internal/noop.js";
let {
open = $bindable(false),
onOpenChange = noop,
onOpenChangeComplete = noop,
disabled,
delayDuration,
disableCloseOnTriggerClick,
disableHoverableContent,
ignoreNonKeyboardFocus,
children,
}: TooltipRootProps = $props();
TooltipRootState.create({
open: boxWith(
() => open,
(v) => {
open = v;
onOpenChange(v);
}
),
delayDuration: boxWith(() => delayDuration),
disableCloseOnTriggerClick: boxWith(() => disableCloseOnTriggerClick),
disableHoverableContent: boxWith(() => disableHoverableContent),
ignoreNonKeyboardFocus: boxWith(() => ignoreNonKeyboardFocus),
disabled: boxWith(() => disabled),
onOpenChangeComplete: boxWith(() => onOpenChangeComplete),
});
</script>
<FloatingLayer tooltip>
{@render children?.()}
</FloatingLayer>
@@ -0,0 +1,3 @@
declare const Tooltip: import("svelte").Component<import("../types.js").TooltipRootPropsWithoutHTML, {}, "open">;
type Tooltip = ReturnType<typeof Tooltip>;
export default Tooltip;
+9
View File
@@ -0,0 +1,9 @@
export { default as Root } from "./components/tooltip.svelte";
export { default as Content } from "./components/tooltip-content.svelte";
export { default as ContentStatic } from "./components/tooltip-content-static.svelte";
export { default as Trigger } from "./components/tooltip-trigger.svelte";
export { default as Arrow } from "./components/tooltip-arrow.svelte";
export { default as Provider } from "./components/tooltip-provider.svelte";
export { default as Portal } from "../utilities/portal/portal.svelte";
export type { TooltipProviderPropsWithoutHTML as ProviderProps, TooltipRootPropsWithoutHTML as RootProps, TooltipArrowProps as ArrowProps, TooltipContentProps as ContentProps, TooltipContentStaticProps as ContentStaticProps, TooltipTriggerProps as TriggerProps, } from "./types.js";
export type { PortalProps } from "../utilities/portal/types.js";
+7
View File
@@ -0,0 +1,7 @@
export { default as Root } from "./components/tooltip.svelte";
export { default as Content } from "./components/tooltip-content.svelte";
export { default as ContentStatic } from "./components/tooltip-content-static.svelte";
export { default as Trigger } from "./components/tooltip-trigger.svelte";
export { default as Arrow } from "./components/tooltip-arrow.svelte";
export { default as Provider } from "./components/tooltip-provider.svelte";
export { default as Portal } from "../utilities/portal/portal.svelte";
+1
View File
@@ -0,0 +1 @@
export * as Tooltip from "./exports.js";
+1
View File
@@ -0,0 +1 @@
export * as Tooltip from "./exports.js";
+124
View File
@@ -0,0 +1,124 @@
import { DOMContext, type WritableBoxedValues, type ReadableBoxedValues } from "svelte-toolbelt";
import type { OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
import type { FocusEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
export declare const tooltipAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["content", "trigger"]>;
interface TooltipProviderStateOpts extends ReadableBoxedValues<{
delayDuration: number;
disableHoverableContent: boolean;
disableCloseOnTriggerClick: boolean;
disabled: boolean;
ignoreNonKeyboardFocus: boolean;
skipDelayDuration: number;
}> {
}
export declare class TooltipProviderState {
#private;
static create(opts: TooltipProviderStateOpts): TooltipProviderState;
readonly opts: TooltipProviderStateOpts;
isOpenDelayed: boolean;
isPointerInTransit: import("svelte-toolbelt").WritableBox<boolean>;
constructor(opts: TooltipProviderStateOpts);
onOpen: (tooltip: TooltipRootState) => void;
onClose: (tooltip: TooltipRootState) => void;
isTooltipOpen: (tooltip: TooltipRootState) => boolean;
}
interface TooltipRootStateOpts extends ReadableBoxedValues<{
delayDuration: number | undefined;
disableHoverableContent: boolean | undefined;
disableCloseOnTriggerClick: boolean | undefined;
disabled: boolean | undefined;
ignoreNonKeyboardFocus: boolean | undefined;
onOpenChangeComplete: OnChangeFn<boolean>;
}>, WritableBoxedValues<{
open: boolean;
}> {
}
export declare class TooltipRootState {
#private;
static create(opts: TooltipRootStateOpts): TooltipRootState;
readonly opts: TooltipRootStateOpts;
readonly provider: TooltipProviderState;
readonly delayDuration: number;
readonly disableHoverableContent: boolean;
readonly disableCloseOnTriggerClick: boolean;
readonly disabled: boolean;
readonly ignoreNonKeyboardFocus: boolean;
contentNode: HTMLElement | null;
contentPresence: PresenceManager;
triggerNode: HTMLElement | null;
readonly stateAttr: string;
constructor(opts: TooltipRootStateOpts, provider: TooltipProviderState);
handleOpen: () => void;
handleClose: () => void;
onTriggerEnter: () => void;
onTriggerLeave: () => void;
}
interface TooltipTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
disabled: boolean;
tabindex: number;
}> {
}
export declare class TooltipTriggerState {
#private;
static create(opts: TooltipTriggerStateOpts): TooltipTriggerState;
readonly opts: TooltipTriggerStateOpts;
readonly root: TooltipRootState;
readonly attachment: RefAttachment;
domContext: DOMContext;
constructor(opts: TooltipTriggerStateOpts, root: TooltipRootState);
handlePointerUp: () => void;
readonly props: {
readonly id: string;
readonly "aria-describedby": string | undefined;
readonly "data-state": "closed" | "delayed-open" | "instant-open";
readonly "data-disabled": "" | undefined;
readonly "data-delay-duration": `${number}`;
readonly tabindex: number | undefined;
readonly disabled: boolean;
readonly onpointerup: PointerEventHandler<HTMLElement>;
readonly onpointerdown: PointerEventHandler<HTMLElement>;
readonly onpointerenter: PointerEventHandler<HTMLElement>;
readonly onpointermove: PointerEventHandler<HTMLElement>;
readonly onpointerleave: PointerEventHandler<HTMLElement>;
readonly onfocus: FocusEventHandler<HTMLElement>;
readonly onblur: FocusEventHandler<HTMLElement>;
readonly onclick: MouseEventHandler<HTMLElement>;
};
}
interface TooltipContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
onInteractOutside: (e: PointerEvent) => void;
onEscapeKeydown: (e: KeyboardEvent) => void;
}> {
}
export declare class TooltipContentState {
#private;
static create(opts: TooltipContentStateOpts): TooltipContentState;
readonly opts: TooltipContentStateOpts;
readonly root: TooltipRootState;
readonly attachment: RefAttachment;
constructor(opts: TooltipContentStateOpts, root: TooltipRootState);
onInteractOutside: (e: PointerEvent) => void;
onEscapeKeydown: (e: KeyboardEvent) => void;
onOpenAutoFocus: (e: Event) => void;
onCloseAutoFocus: (e: Event) => void;
get shouldRender(): boolean;
readonly snippetProps: {
open: boolean;
};
readonly props: {
readonly id: string;
readonly "data-state": "closed" | "delayed-open" | "instant-open";
readonly "data-disabled": "" | undefined;
readonly style: {
readonly outline: "none";
};
};
readonly popperProps: {
onInteractOutside: (e: PointerEvent) => void;
onEscapeKeydown: (e: KeyboardEvent) => void;
onOpenAutoFocus: (e: Event) => void;
onCloseAutoFocus: (e: Event) => void;
};
}
export {};
+371
View File
@@ -0,0 +1,371 @@
import { onMountEffect, attachRef, DOMContext, simpleBox, boxWith, } from "svelte-toolbelt";
import { on } from "svelte/events";
import { Context, watch } from "runed";
import { isElement, isFocusVisible } from "../../internal/is.js";
import { createBitsAttrs, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
import { TimeoutFn } from "../../internal/timeout-fn.js";
import { SafePolygon } from "../../internal/safe-polygon.svelte.js";
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
export const tooltipAttrs = createBitsAttrs({
component: "tooltip",
parts: ["content", "trigger"],
});
const TooltipProviderContext = new Context("Tooltip.Provider");
const TooltipRootContext = new Context("Tooltip.Root");
export class TooltipProviderState {
static create(opts) {
return TooltipProviderContext.set(new TooltipProviderState(opts));
}
opts;
isOpenDelayed = $state(true);
isPointerInTransit = simpleBox(false);
#timerFn;
#openTooltip = $state(null);
constructor(opts) {
this.opts = opts;
this.#timerFn = new TimeoutFn(() => {
this.isOpenDelayed = true;
}, this.opts.skipDelayDuration.current);
onMountEffect(() => on(window, "scroll", (e) => {
const activeTooltip = this.#openTooltip;
if (!activeTooltip)
return;
const triggerNode = activeTooltip.triggerNode;
if (!triggerNode)
return;
const target = e.target;
if (!(target instanceof Element || target instanceof Document))
return;
if (target.contains(triggerNode)) {
activeTooltip.handleClose();
}
}));
}
#startTimer = () => {
const skipDuration = this.opts.skipDelayDuration.current;
if (skipDuration === 0) {
return;
}
else {
this.#timerFn.start();
}
};
#clearTimer = () => {
this.#timerFn.stop();
};
onOpen = (tooltip) => {
if (this.#openTooltip && this.#openTooltip !== tooltip) {
this.#openTooltip.handleClose();
}
this.#clearTimer();
this.isOpenDelayed = false;
this.#openTooltip = tooltip;
};
onClose = (tooltip) => {
if (this.#openTooltip === tooltip) {
this.#openTooltip = null;
}
this.#startTimer();
};
isTooltipOpen = (tooltip) => {
return this.#openTooltip === tooltip;
};
}
export class TooltipRootState {
static create(opts) {
return TooltipRootContext.set(new TooltipRootState(opts, TooltipProviderContext.get()));
}
opts;
provider;
delayDuration = $derived.by(() => this.opts.delayDuration.current ?? this.provider.opts.delayDuration.current);
disableHoverableContent = $derived.by(() => this.opts.disableHoverableContent.current ??
this.provider.opts.disableHoverableContent.current);
disableCloseOnTriggerClick = $derived.by(() => this.opts.disableCloseOnTriggerClick.current ??
this.provider.opts.disableCloseOnTriggerClick.current);
disabled = $derived.by(() => this.opts.disabled.current ?? this.provider.opts.disabled.current);
ignoreNonKeyboardFocus = $derived.by(() => this.opts.ignoreNonKeyboardFocus.current ??
this.provider.opts.ignoreNonKeyboardFocus.current);
contentNode = $state(null);
contentPresence;
triggerNode = $state(null);
#wasOpenDelayed = $state(false);
#timerFn;
stateAttr = $derived.by(() => {
if (!this.opts.open.current)
return "closed";
return this.#wasOpenDelayed ? "delayed-open" : "instant-open";
});
constructor(opts, provider) {
this.opts = opts;
this.provider = provider;
this.#timerFn = new TimeoutFn(() => {
this.#wasOpenDelayed = true;
this.opts.open.current = true;
}, this.delayDuration ?? 0);
this.contentPresence = new PresenceManager({
open: this.opts.open,
ref: boxWith(() => this.contentNode),
onComplete: () => {
this.opts.onOpenChangeComplete.current(this.opts.open.current);
},
});
watch(() => this.delayDuration, () => {
if (this.delayDuration === undefined)
return;
this.#timerFn = new TimeoutFn(() => {
this.#wasOpenDelayed = true;
this.opts.open.current = true;
}, this.delayDuration);
});
watch(() => this.opts.open.current, (isOpen) => {
if (isOpen) {
this.provider.onOpen(this);
}
else {
this.provider.onClose(this);
}
}, { lazy: true });
}
handleOpen = () => {
this.#timerFn.stop();
this.#wasOpenDelayed = false;
this.opts.open.current = true;
};
handleClose = () => {
this.#timerFn.stop();
this.opts.open.current = false;
};
#handleDelayedOpen = () => {
this.#timerFn.stop();
const shouldSkipDelay = !this.provider.isOpenDelayed;
const delayDuration = this.delayDuration ?? 0;
// if no delay needed (either skip delay active or delay is 0), open immediately
if (shouldSkipDelay || delayDuration === 0) {
// set wasOpenDelayed based on whether we actually had a delay
this.#wasOpenDelayed = delayDuration > 0 && shouldSkipDelay;
this.opts.open.current = true;
}
else {
// use timer for actual delays
this.#timerFn.start();
}
};
onTriggerEnter = () => {
this.#handleDelayedOpen();
};
onTriggerLeave = () => {
if (this.disableHoverableContent) {
this.handleClose();
}
else {
this.#timerFn.stop();
}
};
}
export class TooltipTriggerState {
static create(opts) {
return new TooltipTriggerState(opts, TooltipRootContext.get());
}
opts;
root;
attachment;
#isPointerDown = simpleBox(false);
#hasPointerMoveOpened = $state(false);
#isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled);
domContext;
#transitCheckTimeout = null;
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.domContext = new DOMContext(opts.ref);
this.attachment = attachRef(this.opts.ref, (v) => (this.root.triggerNode = v));
}
#clearTransitCheck = () => {
if (this.#transitCheckTimeout !== null) {
clearTimeout(this.#transitCheckTimeout);
this.#transitCheckTimeout = null;
}
};
handlePointerUp = () => {
this.#isPointerDown.current = false;
};
#onpointerup = () => {
if (this.#isDisabled)
return;
this.#isPointerDown.current = false;
};
#onpointerdown = () => {
if (this.#isDisabled)
return;
this.#isPointerDown.current = true;
this.domContext.getDocument().addEventListener("pointerup", () => {
this.handlePointerUp();
}, { once: true });
};
#onpointerenter = (e) => {
if (this.#isDisabled)
return;
if (e.pointerType === "touch")
return;
// if in transit, wait briefly to see if user is actually heading to old content or staying here
if (this.root.provider.isPointerInTransit.current) {
this.#clearTransitCheck();
this.#transitCheckTimeout = window.setTimeout(() => {
// if still in transit after delay, user is likely staying on this trigger
if (this.root.provider.isPointerInTransit.current) {
this.root.provider.isPointerInTransit.current = false;
this.root.onTriggerEnter();
this.#hasPointerMoveOpened = true;
}
}, 250);
return;
}
this.root.onTriggerEnter();
this.#hasPointerMoveOpened = true;
};
#onpointermove = (e) => {
if (this.#isDisabled)
return;
if (e.pointerType === "touch")
return;
if (this.#hasPointerMoveOpened)
return;
// moving within trigger means we're definitely not in transit anymore
this.#clearTransitCheck();
this.root.provider.isPointerInTransit.current = false;
this.root.onTriggerEnter();
this.#hasPointerMoveOpened = true;
};
#onpointerleave = (e) => {
if (this.#isDisabled)
return;
this.#clearTransitCheck();
const relatedTarget = e.relatedTarget;
if (!this.root.disableHoverableContent &&
this.root.opts.open.current &&
isElement(relatedTarget) &&
this.root.contentNode &&
!this.root.contentNode.contains(relatedTarget)) {
this.root.handleClose();
}
else {
this.root.onTriggerLeave();
}
this.#hasPointerMoveOpened = false;
};
#onfocus = (e) => {
if (this.#isPointerDown.current || this.#isDisabled)
return;
if (this.root.ignoreNonKeyboardFocus && !isFocusVisible(e.currentTarget))
return;
this.root.handleOpen();
};
#onblur = () => {
if (this.#isDisabled)
return;
this.root.handleClose();
};
#onclick = () => {
if (this.root.disableCloseOnTriggerClick || this.#isDisabled)
return;
this.root.handleClose();
};
props = $derived.by(() => ({
id: this.opts.id.current,
"aria-describedby": this.root.opts.open.current
? this.root.contentNode?.id
: undefined,
"data-state": this.root.stateAttr,
"data-disabled": boolToEmptyStrOrUndef(this.#isDisabled),
"data-delay-duration": `${this.root.delayDuration}`,
[tooltipAttrs.trigger]: "",
tabindex: this.#isDisabled ? undefined : this.opts.tabindex.current,
disabled: this.opts.disabled.current,
onpointerup: this.#onpointerup,
onpointerdown: this.#onpointerdown,
onpointerenter: this.#onpointerenter,
onpointermove: this.#onpointermove,
onpointerleave: this.#onpointerleave,
onfocus: this.#onfocus,
onblur: this.#onblur,
onclick: this.#onclick,
...this.attachment,
}));
}
export class TooltipContentState {
static create(opts) {
return new TooltipContentState(opts, TooltipRootContext.get());
}
opts;
root;
attachment;
#safePolygon = null;
#createSafePolygon = () => {
if (this.#safePolygon)
return;
this.#safePolygon = new SafePolygon({
triggerNode: () => this.root.triggerNode,
contentNode: () => this.root.contentNode,
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
onPointerExit: () => {
if (this.root.provider.isTooltipOpen(this.root)) {
this.root.handleClose();
}
},
});
};
constructor(opts, root) {
this.opts = opts;
this.root = root;
this.attachment = attachRef(this.opts.ref, (v) => (this.root.contentNode = v));
watch(() => this.root.opts.open.current && !this.root.disableHoverableContent, (shouldTrackSafePolygon) => {
if (!shouldTrackSafePolygon)
return;
this.#createSafePolygon();
}, { lazy: true });
}
onInteractOutside = (e) => {
if (isElement(e.target) &&
this.root.triggerNode?.contains(e.target) &&
this.root.disableCloseOnTriggerClick) {
e.preventDefault();
return;
}
this.opts.onInteractOutside.current(e);
if (e.defaultPrevented)
return;
this.root.handleClose();
};
onEscapeKeydown = (e) => {
this.opts.onEscapeKeydown.current?.(e);
if (e.defaultPrevented)
return;
this.root.handleClose();
};
onOpenAutoFocus = (e) => {
e.preventDefault();
};
onCloseAutoFocus = (e) => {
e.preventDefault();
};
get shouldRender() {
return this.root.contentPresence.shouldRender;
}
snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
props = $derived.by(() => ({
id: this.opts.id.current,
"data-state": this.root.stateAttr,
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled),
style: {
outline: "none",
},
[tooltipAttrs.content]: "",
...this.attachment,
}));
popperProps = {
onInteractOutside: this.onInteractOutside,
onEscapeKeydown: this.onEscapeKeydown,
onOpenAutoFocus: this.onOpenAutoFocus,
onCloseAutoFocus: this.onCloseAutoFocus,
};
}
+127
View File
@@ -0,0 +1,127 @@
import type { FloatingLayerContentProps } from "../utilities/floating-layer/types.js";
import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
import type { DismissibleLayerProps } from "../utilities/dismissible-layer/types.js";
import type { EscapeLayerProps } from "../utilities/escape-layer/types.js";
import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, WithChildren, Without } from "../../internal/types.js";
import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
import type { PortalProps } from "../utilities/portal/types.js";
import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "../../shared/types.js";
export type TooltipProviderPropsWithoutHTML = WithChildren<{
/**
* The delay in milliseconds before the tooltip opens.
*
* @defaultValue 700
*/
delayDuration?: number;
/**
* How much time a user has to enter another trigger without
* incurring a delay again.
* @defaultValue 300
*/
skipDelayDuration?: number;
/**
* Prevents tooltip from remaining open when hovering over the content.
*
* @defaultValue false
*/
disableHoverableContent?: boolean;
/**
* When `true`, the tooltip will not close when you click on the trigger.
*
* @defaultValue false
*/
disableCloseOnTriggerClick?: boolean;
/**
* When `true`, the tooltip will be disabled and will not open.
*
* @defaultValue false
*/
disabled?: boolean;
/**
* Prevent the tooltip from opening if the focus did not come using
* the keyboard.
*
* @defaultValue false
*/
ignoreNonKeyboardFocus?: boolean;
}>;
export type TooltipProviderProps = TooltipProviderPropsWithoutHTML;
export type TooltipRootPropsWithoutHTML = WithChildren<{
/**
* The open state of the tooltip.
*
* @defaultValue false
*/
open?: boolean;
/**
* A callback that will be called when the tooltip is opened or closed.
*/
onOpenChange?: OnChangeFn<boolean>;
/**
* A callback that will be called when the tooltip is opened or closed.
*/
onOpenChangeComplete?: OnChangeFn<boolean>;
/**
* The delay in milliseconds before the tooltip opens.
*
* @defaultValue 700
*/
delayDuration?: number;
/**
* Prevents tooltip from remaining open when hovering over the content.
*
* @defaultValue false
*/
disableHoverableContent?: boolean;
/**
* When `true`, the tooltip will not close when you click on the trigger.
*
* @defaultValue false
*/
disableCloseOnTriggerClick?: boolean;
/**
* When `true`, the tooltip will be disabled and will not open.
*
* @defaultValue false
*/
disabled?: boolean;
/**
* Prevent the tooltip from opening if the focus did not come using
* the keyboard.
*
* @defaultValue false
*/
ignoreNonKeyboardFocus?: boolean;
}>;
export type TooltipRootProps = TooltipRootPropsWithoutHTML;
export type TooltipContentPropsWithoutHTML = WithChildNoChildrenSnippetProps<Omit<FloatingLayerContentProps, "content" | "preventScroll"> & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & EscapeLayerProps & {
/**
* When `true`, the tooltip will be forced to mount in the DOM.
*
* Useful for more control over the transition behavior.
*/
forceMount?: boolean;
}, FloatingContentSnippetProps>;
export type TooltipContentProps = TooltipContentPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, TooltipContentPropsWithoutHTML>;
export type TooltipContentStaticPropsWithoutHTML = WithChildNoChildrenSnippetProps<Pick<FloatingLayerContentProps, "dir"> & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & EscapeLayerProps & {
/**
* When `true`, the tooltip will be forced to mount in the DOM.
*
* Useful for more control over the transition behavior.
*/
forceMount?: boolean;
}, StaticContentSnippetProps>;
export type TooltipContentStaticProps = TooltipContentStaticPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, TooltipContentStaticPropsWithoutHTML>;
export type TooltipArrowPropsWithoutHTML = ArrowPropsWithoutHTML;
export type TooltipArrowProps = ArrowProps;
export type TooltipPortalPropsWithoutHTML = PortalProps;
export type TooltipPortalProps = PortalProps;
export type TooltipTriggerPropsWithoutHTML = WithChild<{
/**
* Whether the tooltip trigger is disabled or not.
*
* @defaultValue false
*/
disabled?: boolean | null | undefined;
}>;
export type TooltipTriggerProps = TooltipTriggerPropsWithoutHTML & Without<BitsPrimitiveButtonAttributes, TooltipTriggerPropsWithoutHTML>;
+1
View File
@@ -0,0 +1 @@
export {};
+1
View File
@@ -0,0 +1 @@
export declare const TOOLTIP_OPEN_EVENT = "tooltip.open";
+1
View File
@@ -0,0 +1 @@
export const TOOLTIP_OPEN_EVENT = "tooltip.open";