@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
470 lines (463 loc) • 14.7 kB
JavaScript
import { PopperArrow, Popper } from './LMWVDFW6.js';
import { DismissableLayer } from './BASJUNIE.js';
import { createDisclosureState } from './7LCANGHD.js';
import { createRegisterId } from './E4R2EMM4.js';
import { Polymorphic } from './6Y7B2NEO.js';
import { __export } from './5ZKAE4VZ.js';
import { createComponent, mergeProps, Portal, isServer, memo } from 'solid-js/web';
import { mergeDefaultProps, mergeRefs, getDocument, getWindow, createGenerateId, contains, isPointInPolygon, getEventPoint, callHandler } from '@kobalte/utils';
import { createContext, useContext, splitProps, createEffect, onCleanup, Show, createUniqueId, createSignal, createMemo } from 'solid-js';
import { combineStyle } from '@solid-primitives/props';
import createPresence from 'solid-presence';
// src/tooltip/index.tsx
var tooltip_exports = {};
__export(tooltip_exports, {
Arrow: () => PopperArrow,
Content: () => TooltipContent,
Portal: () => TooltipPortal,
Root: () => TooltipRoot,
Tooltip: () => Tooltip,
Trigger: () => TooltipTrigger,
useTooltipContext: () => useTooltipContext
});
var TooltipContext = createContext();
function useTooltipContext() {
const context = useContext(TooltipContext);
if (context === void 0) {
throw new Error("[kobalte]: `useTooltipContext` must be used within a `Tooltip` component");
}
return context;
}
// src/tooltip/tooltip-content.tsx
function TooltipContent(props) {
const context = useTooltipContext();
const mergedProps = mergeDefaultProps({
id: context.generateId("content")
}, props);
const [local, others] = splitProps(mergedProps, ["ref", "style"]);
createEffect(() => onCleanup(context.registerContentId(others.id)));
return createComponent(Show, {
get when() {
return context.contentPresent();
},
get children() {
return createComponent(Popper.Positioner, {
get children() {
return createComponent(DismissableLayer, mergeProps({
ref(r$) {
const _ref$ = mergeRefs((el) => {
context.setContentRef(el);
}, local.ref);
typeof _ref$ === "function" && _ref$(r$);
},
role: "tooltip",
disableOutsidePointerEvents: false,
get style() {
return combineStyle({
"--kb-tooltip-content-transform-origin": "var(--kb-popper-content-transform-origin)",
position: "relative"
}, local.style);
},
onFocusOutside: (e) => e.preventDefault(),
onDismiss: () => context.hideTooltip(true)
}, () => context.dataset(), others));
}
});
}
});
}
function TooltipPortal(props) {
const context = useTooltipContext();
return createComponent(Show, {
get when() {
return context.contentPresent();
},
get children() {
return createComponent(Portal, props);
}
});
}
// src/tooltip/utils.ts
function getTooltipSafeArea(placement, anchorEl, floatingEl) {
const basePlacement = placement.split("-")[0];
const anchorRect = anchorEl.getBoundingClientRect();
const floatingRect = floatingEl.getBoundingClientRect();
const polygon = [];
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
switch (basePlacement) {
case "top":
polygon.push([anchorRect.left, anchorCenterY]);
polygon.push([floatingRect.left, floatingRect.bottom]);
polygon.push([floatingRect.left, floatingRect.top]);
polygon.push([floatingRect.right, floatingRect.top]);
polygon.push([floatingRect.right, floatingRect.bottom]);
polygon.push([anchorRect.right, anchorCenterY]);
break;
case "right":
polygon.push([anchorCenterX, anchorRect.top]);
polygon.push([floatingRect.left, floatingRect.top]);
polygon.push([floatingRect.right, floatingRect.top]);
polygon.push([floatingRect.right, floatingRect.bottom]);
polygon.push([floatingRect.left, floatingRect.bottom]);
polygon.push([anchorCenterX, anchorRect.bottom]);
break;
case "bottom":
polygon.push([anchorRect.left, anchorCenterY]);
polygon.push([floatingRect.left, floatingRect.top]);
polygon.push([floatingRect.left, floatingRect.bottom]);
polygon.push([floatingRect.right, floatingRect.bottom]);
polygon.push([floatingRect.right, floatingRect.top]);
polygon.push([anchorRect.right, anchorCenterY]);
break;
case "left":
polygon.push([anchorCenterX, anchorRect.top]);
polygon.push([floatingRect.right, floatingRect.top]);
polygon.push([floatingRect.left, floatingRect.top]);
polygon.push([floatingRect.left, floatingRect.bottom]);
polygon.push([floatingRect.right, floatingRect.bottom]);
polygon.push([anchorCenterX, anchorRect.bottom]);
break;
}
return polygon;
}
// src/tooltip/tooltip-root.tsx
var tooltips = {};
var tooltipsCounter = 0;
var globalWarmedUp = false;
var globalWarmUpTimeout;
var globalCoolDownTimeout;
var globalSkipDelayTimeout;
function TooltipRoot(props) {
const defaultId = `tooltip-${createUniqueId()}`;
const tooltipId = `${++tooltipsCounter}`;
const mergedProps = mergeDefaultProps({
id: defaultId,
openDelay: 700,
closeDelay: 300,
skipDelayDuration: 300
}, props);
const [local, others] = splitProps(mergedProps, ["id", "open", "defaultOpen", "onOpenChange", "disabled", "triggerOnFocusOnly", "openDelay", "closeDelay", "skipDelayDuration", "ignoreSafeArea", "forceMount"]);
let closeTimeoutId;
const [contentId, setContentId] = createSignal();
const [triggerRef, setTriggerRef] = createSignal();
const [contentRef, setContentRef] = createSignal();
const [currentPlacement, setCurrentPlacement] = createSignal(others.placement);
const disclosureState = createDisclosureState({
open: () => local.open,
defaultOpen: () => local.defaultOpen,
onOpenChange: (isOpen) => local.onOpenChange?.(isOpen)
});
const {
present: contentPresent
} = createPresence({
show: () => local.forceMount || disclosureState.isOpen(),
element: () => contentRef() ?? null
});
const ensureTooltipEntry = () => {
tooltips[tooltipId] = hideTooltip;
};
const closeOpenTooltips = () => {
for (const hideTooltipId in tooltips) {
if (hideTooltipId !== tooltipId) {
tooltips[hideTooltipId](true);
delete tooltips[hideTooltipId];
}
}
};
const hideTooltip = (immediate = false) => {
if (isServer) {
return;
}
if (immediate || local.closeDelay && local.closeDelay <= 0) {
window.clearTimeout(closeTimeoutId);
closeTimeoutId = void 0;
disclosureState.close();
} else if (!closeTimeoutId) {
closeTimeoutId = window.setTimeout(() => {
closeTimeoutId = void 0;
disclosureState.close();
}, local.closeDelay);
}
window.clearTimeout(globalWarmUpTimeout);
globalWarmUpTimeout = void 0;
if (local.skipDelayDuration && local.skipDelayDuration >= 0) {
globalSkipDelayTimeout = window.setTimeout(() => {
window.clearTimeout(globalSkipDelayTimeout);
globalSkipDelayTimeout = void 0;
}, local.skipDelayDuration);
}
if (globalWarmedUp) {
window.clearTimeout(globalCoolDownTimeout);
globalCoolDownTimeout = window.setTimeout(() => {
delete tooltips[tooltipId];
globalCoolDownTimeout = void 0;
globalWarmedUp = false;
}, local.closeDelay);
}
};
const showTooltip = () => {
if (isServer) {
return;
}
clearTimeout(closeTimeoutId);
closeTimeoutId = void 0;
closeOpenTooltips();
ensureTooltipEntry();
globalWarmedUp = true;
disclosureState.open();
window.clearTimeout(globalWarmUpTimeout);
globalWarmUpTimeout = void 0;
window.clearTimeout(globalCoolDownTimeout);
globalCoolDownTimeout = void 0;
window.clearTimeout(globalSkipDelayTimeout);
globalSkipDelayTimeout = void 0;
};
const warmupTooltip = () => {
if (isServer) {
return;
}
closeOpenTooltips();
ensureTooltipEntry();
if (!disclosureState.isOpen() && !globalWarmUpTimeout && !globalWarmedUp) {
globalWarmUpTimeout = window.setTimeout(() => {
globalWarmUpTimeout = void 0;
globalWarmedUp = true;
showTooltip();
}, local.openDelay);
} else if (!disclosureState.isOpen()) {
showTooltip();
}
};
const openTooltip = (immediate = false) => {
if (isServer) {
return;
}
if (!immediate && local.openDelay && local.openDelay > 0 && !closeTimeoutId && !globalSkipDelayTimeout) {
warmupTooltip();
} else {
showTooltip();
}
};
const cancelOpening = () => {
if (isServer) {
return;
}
window.clearTimeout(globalWarmUpTimeout);
globalWarmUpTimeout = void 0;
globalWarmedUp = false;
};
const cancelClosing = () => {
if (isServer) {
return;
}
window.clearTimeout(closeTimeoutId);
closeTimeoutId = void 0;
};
const isTargetOnTooltip = (target) => {
return contains(triggerRef(), target) || contains(contentRef(), target);
};
const getPolygonSafeArea = (placement) => {
const triggerEl = triggerRef();
const contentEl = contentRef();
if (!triggerEl || !contentEl) {
return;
}
return getTooltipSafeArea(placement, triggerEl, contentEl);
};
const onHoverOutside = (event) => {
const target = event.target;
if (isTargetOnTooltip(target)) {
cancelClosing();
return;
}
if (!local.ignoreSafeArea) {
const polygon = getPolygonSafeArea(currentPlacement());
if (polygon && isPointInPolygon(getEventPoint(event), polygon)) {
cancelClosing();
return;
}
}
if (closeTimeoutId) {
return;
}
hideTooltip();
};
createEffect(() => {
if (isServer) {
return;
}
if (!disclosureState.isOpen()) {
return;
}
const doc = getDocument();
doc.addEventListener("pointermove", onHoverOutside, true);
onCleanup(() => {
doc.removeEventListener("pointermove", onHoverOutside, true);
});
});
createEffect(() => {
const trigger = triggerRef();
if (!trigger || !disclosureState.isOpen()) {
return;
}
const handleScroll = (event) => {
const target = event.target;
if (contains(target, trigger)) {
hideTooltip(true);
}
};
const win = getWindow();
win.addEventListener("scroll", handleScroll, {
capture: true
});
onCleanup(() => {
win.removeEventListener("scroll", handleScroll, {
capture: true
});
});
});
onCleanup(() => {
clearTimeout(closeTimeoutId);
const tooltip = tooltips[tooltipId];
if (tooltip) {
delete tooltips[tooltipId];
}
});
const dataset = createMemo(() => ({
"data-expanded": disclosureState.isOpen() ? "" : void 0,
"data-closed": !disclosureState.isOpen() ? "" : void 0
}));
const context = {
dataset,
isOpen: disclosureState.isOpen,
isDisabled: () => local.disabled ?? false,
triggerOnFocusOnly: () => local.triggerOnFocusOnly ?? false,
contentId,
contentPresent,
openTooltip,
hideTooltip,
cancelOpening,
generateId: createGenerateId(() => mergedProps.id),
registerContentId: createRegisterId(setContentId),
isTargetOnTooltip,
setTriggerRef,
setContentRef
};
return createComponent(TooltipContext.Provider, {
value: context,
get children() {
return createComponent(Popper, mergeProps({
anchorRef: triggerRef,
contentRef,
onCurrentPlacementChange: setCurrentPlacement
}, others));
}
});
}
function TooltipTrigger(props) {
let ref;
const context = useTooltipContext();
const [local, others] = splitProps(props, ["ref", "onPointerEnter", "onPointerLeave", "onPointerDown", "onClick", "onFocus", "onBlur"]);
let isPointerDown = false;
let isHovered = false;
let isFocused = false;
const handlePointerUp = () => {
isPointerDown = false;
};
const handleShow = () => {
if (!context.isOpen() && (isHovered || isFocused)) {
context.openTooltip(isFocused);
}
};
const handleHide = (immediate) => {
if (context.isOpen() && !isHovered && !isFocused) {
context.hideTooltip(immediate);
}
};
const onPointerEnter = (e) => {
callHandler(e, local.onPointerEnter);
if (e.pointerType === "touch" || context.triggerOnFocusOnly() || context.isDisabled() || e.defaultPrevented) {
return;
}
isHovered = true;
handleShow();
};
const onPointerLeave = (e) => {
callHandler(e, local.onPointerLeave);
if (e.pointerType === "touch") {
return;
}
isHovered = false;
isFocused = false;
if (context.isOpen()) {
handleHide();
} else {
context.cancelOpening();
}
};
const onPointerDown = (e) => {
callHandler(e, local.onPointerDown);
isPointerDown = true;
getDocument(ref).addEventListener("pointerup", handlePointerUp, {
once: true
});
};
const onClick = (e) => {
callHandler(e, local.onClick);
isHovered = false;
isFocused = false;
handleHide(true);
};
const onFocus = (e) => {
callHandler(e, local.onFocus);
if (context.isDisabled() || e.defaultPrevented || isPointerDown) {
return;
}
isFocused = true;
handleShow();
};
const onBlur = (e) => {
callHandler(e, local.onBlur);
const relatedTarget = e.relatedTarget;
if (context.isTargetOnTooltip(relatedTarget)) {
return;
}
isHovered = false;
isFocused = false;
handleHide(true);
};
onCleanup(() => {
if (isServer) {
return;
}
getDocument(ref).removeEventListener("pointerup", handlePointerUp);
});
return createComponent(Polymorphic, mergeProps({
as: "button",
ref(r$) {
const _ref$ = mergeRefs((el) => {
context.setTriggerRef(el);
ref = el;
}, local.ref);
typeof _ref$ === "function" && _ref$(r$);
},
get ["aria-describedby"]() {
return memo(() => !!context.isOpen())() ? context.contentId() : void 0;
},
onPointerEnter,
onPointerLeave,
onPointerDown,
onClick,
onFocus,
onBlur
}, () => context.dataset(), others));
}
// src/tooltip/index.tsx
var Tooltip = Object.assign(TooltipRoot, {
Arrow: PopperArrow,
Content: TooltipContent,
Portal: TooltipPortal,
Trigger: TooltipTrigger
});
export { Tooltip, TooltipContent, TooltipPortal, TooltipRoot, TooltipTrigger, tooltip_exports, useTooltipContext };