UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

528 lines (516 loc) 14.9 kB
import { Popper, PopperArrow } from "./2CTBMVJ4.jsx"; import { DismissableLayer } from "./3VFJM5NZ.jsx"; import { createDisclosureState } from "./E53DB7BS.jsx"; import { createRegisterId } from "./JNCCF6MP.jsx"; import { Polymorphic } from "./FLVHQV4A.jsx"; import { __export } from "./5WXHJDCZ.jsx"; // src/tooltip/index.tsx var tooltip_exports = {}; __export(tooltip_exports, { Arrow: () => PopperArrow, Content: () => TooltipContent, Portal: () => TooltipPortal, Root: () => TooltipRoot, Tooltip: () => Tooltip, Trigger: () => TooltipTrigger, useTooltipContext: () => useTooltipContext }); // src/tooltip/tooltip-content.tsx import { mergeDefaultProps, mergeRefs } from "@kobalte/utils"; import { Show, createEffect, onCleanup, splitProps } from "solid-js"; import { combineStyle } from "@solid-primitives/props"; // src/tooltip/tooltip-context.tsx import { createContext, useContext } from "solid-js"; 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 <Show when={context.contentPresent()}><Popper.Positioner><DismissableLayer ref={mergeRefs((el) => { context.setContentRef(el); }, local.ref)} role="tooltip" disableOutsidePointerEvents={false} style={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} /></Popper.Positioner></Show>; } // src/tooltip/tooltip-portal.tsx import { Show as Show2 } from "solid-js"; import { Portal } from "solid-js/web"; function TooltipPortal(props) { const context = useTooltipContext(); return <Show2 when={context.contentPresent()}><Portal {...props} /></Show2>; } // src/tooltip/tooltip-root.tsx import { contains, createGenerateId, getDocument, getEventPoint, getWindow, isPointInPolygon, mergeDefaultProps as mergeDefaultProps2 } from "@kobalte/utils"; import { createEffect as createEffect2, createMemo, createSignal, createUniqueId, onCleanup as onCleanup2, splitProps as splitProps2 } from "solid-js"; import { isServer } from "solid-js/web"; import createPresence from "solid-presence"; // 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 = mergeDefaultProps2( { id: defaultId, openDelay: 700, closeDelay: 300, skipDelayDuration: 300 }, props ); const [local, others] = splitProps2(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(); }; createEffect2(() => { if (isServer) { return; } if (!disclosureState.isOpen()) { return; } const doc = getDocument(); doc.addEventListener("pointermove", onHoverOutside, true); onCleanup2(() => { doc.removeEventListener("pointermove", onHoverOutside, true); }); }); createEffect2(() => { 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 }); onCleanup2(() => { win.removeEventListener("scroll", handleScroll, { capture: true }); }); }); onCleanup2(() => { 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 <TooltipContext.Provider value={context}><Popper anchorRef={triggerRef} contentRef={contentRef} onCurrentPlacementChange={setCurrentPlacement} {...others} /></TooltipContext.Provider>; } // src/tooltip/tooltip-trigger.tsx import { callHandler, getDocument as getDocument2, mergeRefs as mergeRefs2 } from "@kobalte/utils"; import { onCleanup as onCleanup3, splitProps as splitProps3 } from "solid-js"; import { isServer as isServer2 } from "solid-js/web"; function TooltipTrigger(props) { let ref; const context = useTooltipContext(); const [local, others] = splitProps3(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; getDocument2(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); }; onCleanup3(() => { if (isServer2) { return; } getDocument2(ref).removeEventListener("pointerup", handlePointerUp); }); return <Polymorphic as="button" ref={mergeRefs2((el) => { context.setTriggerRef(el); ref = el; }, local.ref)} aria-describedby={context.isOpen() ? context.contentId() : void 0} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave} onPointerDown={onPointerDown} onClick={onClick} onFocus={onFocus} onBlur={onBlur} {...context.dataset()} {...others} />; } // src/tooltip/index.tsx var Tooltip = Object.assign(TooltipRoot, { Arrow: PopperArrow, Content: TooltipContent, Portal: TooltipPortal, Trigger: TooltipTrigger }); export { useTooltipContext, TooltipContent, TooltipPortal, TooltipRoot, TooltipTrigger, Tooltip, tooltip_exports };