lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
320 lines (319 loc) • 13.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
// @ts-nocheck
import * as React from "react";
import { cn } from "../../lib/utils"; // Assuming utils.ts is in lib/
import { motion, AnimatePresence } from "framer-motion";
import ReactDOM from "react-dom";
const TooltipContext = React.createContext(undefined);
// --- Tooltip Component ---
const Tooltip = ({ children, content, defaultOpen = false, open: controlledOpen, onOpenChange, delayDuration = 300, hideDelay = 100, side = "top", align = "center", sideOffset = 8, variant = "default", hideArrow = false, maxWidth = "20rem", asChild = false, disabled = false, }) => {
// Manage uncontrolled open state
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
// Determine if the component is controlled or uncontrolled
const isControlled = controlledOpen !== undefined;
const open = isControlled ? controlledOpen : uncontrolledOpen;
// Ref for the trigger element, passed via context to TooltipContentDisplay
const triggerRef = React.useRef(null);
// Function to update the open state, handling both controlled and uncontrolled modes
const setOpen = React.useCallback((value) => {
if (!isControlled) {
setUncontrolledOpen(value);
}
if (onOpenChange) {
// Calculate the new value if a function is passed
const newValue = typeof value === "function" ? value(open) : value;
onOpenChange(newValue);
}
}, [isControlled, onOpenChange, open]);
// Refs for managing show/hide timeouts
const showTimeoutRef = React.useRef(null);
const hideTimeoutRef = React.useRef(null);
// Memoize configuration object to prevent unnecessary re-renders
const config = React.useMemo(() => ({
side,
align,
sideOffset,
variant,
hideArrow,
maxWidth,
}), [side, align, sideOffset, variant, hideArrow, maxWidth]);
// Clean up any pending timeouts on component unmount
React.useEffect(() => {
return () => {
if (showTimeoutRef.current)
clearTimeout(showTimeoutRef.current);
if (hideTimeoutRef.current)
clearTimeout(hideTimeoutRef.current);
};
}, []);
const handleMouseEnter = React.useCallback(() => {
if (disabled)
return;
// Always clear both timers to prevent race conditions
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
if (showTimeoutRef.current) {
clearTimeout(showTimeoutRef.current);
showTimeoutRef.current = null;
}
showTimeoutRef.current = setTimeout(() => setOpen(true), delayDuration);
}, [disabled, setOpen, delayDuration]);
const handleMouseLeave = React.useCallback(() => {
if (disabled)
return;
// Always clear both timers to prevent race conditions
if (showTimeoutRef.current) {
clearTimeout(showTimeoutRef.current);
showTimeoutRef.current = null;
}
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = null;
}
hideTimeoutRef.current = setTimeout(() => setOpen(false), hideDelay);
}, [disabled, setOpen, hideDelay]);
// Memoize the context value to prevent unnecessary re-renders of consumers
const contextValue = React.useMemo(() => ({
open,
setOpen,
content,
config,
triggerRef,
showTimeoutRef,
hideTimeoutRef,
handleMouseEnter,
handleMouseLeave,
}), [open, setOpen, content, config, triggerRef, handleMouseEnter, handleMouseLeave]);
return (_jsxs(TooltipContext.Provider, { value: contextValue, children: [disabled ? children : (_jsx(TooltipTrigger, { asChild: asChild, triggerRef: triggerRef, children: children })), _jsx(TooltipContentDisplay, {})] }));
};
// --- TooltipTrigger Component ---
const TooltipTrigger = React.forwardRef(({ children, asChild = false, triggerRef }, ref) => {
const context = React.useContext(TooltipContext);
if (!context) {
throw new Error("TooltipTrigger must be used within a Tooltip");
}
// Combined ref to set both the forwarded ref and the internal triggerRef
const combinedRef = React.useCallback((node) => {
if (typeof ref === 'function') {
ref(node);
}
else if (ref) {
ref.current = node;
}
if (triggerRef) {
triggerRef.current = node;
}
}, [ref, triggerRef]);
// Props to be applied to the trigger element
const triggerProps = {
ref: combinedRef, // Use the combined ref
onMouseEnter: context.handleMouseEnter,
onMouseLeave: context.handleMouseLeave,
onFocus: () => context.setOpen(true),
onBlur: () => context.setOpen(false),
};
// If asChild is true, clone props onto the child element
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, triggerProps);
}
// Default rendering if asChild is false
return (_jsx("div", { className: "inline-block relative" // Keeps the trigger as a block for correct positioning
, ...triggerProps, children: children }));
});
TooltipTrigger.displayName = "TooltipTrigger";
// --- TooltipContentDisplay Component ---
// This component renders the actual tooltip content and handles its positioning.
const TooltipContentDisplay = () => {
const context = React.useContext(TooltipContext);
if (!context) {
throw new Error("TooltipContentDisplay must be used within a Tooltip");
}
const { open, content, config, triggerRef } = context;
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const contentRef = React.useRef(null);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
const getAnimationVariants = React.useCallback(() => {
const { side } = config;
const distance = 8;
return {
hidden: {
opacity: 0,
scale: 0.8,
x: side === "left" ? distance : side === "right" ? -distance : 0,
y: side === "top" ? distance : side === "bottom" ? -distance : 0,
},
visible: {
opacity: 1,
scale: 1,
x: 0,
y: 0,
transition: {
type: "spring",
damping: 20,
stiffness: 400,
},
},
exit: {
opacity: 0,
scale: 0.8,
transition: { duration: 0.15, ease: "easeIn" },
pointerEvents: "none",
},
};
}, [config]);
const updatePosition = React.useCallback(() => {
if (!contentRef.current || !triggerRef.current)
return;
const triggerRect = triggerRef.current.getBoundingClientRect();
const contentRect = contentRef.current.getBoundingClientRect();
let x = 0;
let y = 0;
const { side, align, sideOffset } = config;
switch (side) {
case "top":
y = triggerRect.top - contentRect.height - sideOffset;
break;
case "bottom":
y = triggerRect.bottom + sideOffset;
break;
case "left":
x = triggerRect.left - contentRect.width - sideOffset;
break;
case "right":
x = triggerRect.right + sideOffset;
break;
}
if (side === "top" || side === "bottom") {
switch (align) {
case "start":
x = triggerRect.left;
break;
case "end":
x = triggerRect.right - contentRect.width;
break;
default:
x = triggerRect.left + triggerRect.width / 2 - contentRect.width / 2;
break;
}
}
else {
switch (align) {
case "start":
y = triggerRect.top;
break;
case "end":
y = triggerRect.bottom - contentRect.height;
break;
default:
y = triggerRect.top + triggerRect.height / 2 - contentRect.height / 2;
break;
}
}
const padding = 8;
if (x < padding)
x = padding;
if (x + contentRect.width > window.innerWidth - padding)
x = window.innerWidth - contentRect.width - padding;
if (y < padding)
y = padding;
if (y + contentRect.height > window.innerHeight - padding)
y = window.innerHeight - contentRect.height - padding;
setPosition({ x, y });
}, [config, triggerRef]);
React.useEffect(() => {
if (open) {
const id = requestAnimationFrame(updatePosition);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
cancelAnimationFrame(id);
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}
}, [open, updatePosition]);
const getArrowStyle = React.useCallback(() => {
const { side, align } = config;
const arrowSize = 8;
let style = {
position: "absolute",
width: arrowSize,
height: arrowSize,
transform: "rotate(45deg)",
zIndex: -1,
};
switch (side) {
case "top":
style.bottom = -arrowSize / 2;
style.left = align === "center" ? "50%" : align === "start" ? "15%" : undefined;
if (align === "end")
style.right = "15%";
if (align === "center")
style.transform = "translateX(-50%) rotate(45deg)";
break;
case "bottom":
style.top = -arrowSize / 2;
style.left = align === "center" ? "50%" : align === "start" ? "15%" : undefined;
if (align === "end")
style.right = "15%";
if (align === "center")
style.transform = "translateX(-50%) rotate(45deg)";
break;
case "left":
style.right = -arrowSize / 2;
style.top = align === "center" ? "50%" : align === "start" ? "15%" : undefined;
if (align === "end")
style.bottom = "15%";
if (align === "center")
style.transform = "translateY(-50%) rotate(45deg)";
break;
case "right":
style.left = -arrowSize / 2;
style.top = align === "center" ? "50%" : align === "start" ? "15%" : undefined;
if (align === "end")
style.bottom = "15%";
if (align === "center")
style.transform = "translateY(-50%) rotate(45deg)";
break;
}
return style;
}, [config]);
const getVariantClasses = React.useCallback(() => {
const { variant } = config;
switch (variant) {
case "info": return "bg-primarylw text-white border-[color-mix(in_srgb,var(--primarylw)_20%,transparent)]";
case "success": return "bg-emerald-600 text-white border-emerald-400/20";
case "warning": return "bg-amber-500 text-black border-amber-400/20";
case "error": return "bg-rose-600 text-white border-rose-400/20";
default: return "bg-popover/90 text-popover-foreground border border-gray-200 dark:border-gray-800 backdrop-blur-md shadow-xl";
}
}, [config]);
if (!open || !mounted)
return null;
return ReactDOM.createPortal(_jsx(AnimatePresence, { children: _jsxs(motion.div, { ref: contentRef, onMouseEnter: context.handleMouseEnter, onMouseLeave: context.handleMouseLeave, style: {
position: "fixed",
top: position.y,
left: position.x,
maxWidth: config.maxWidth,
zIndex: 9999,
}, initial: "hidden", animate: "visible", exit: "exit", variants: getAnimationVariants(), className: cn("rounded-md px-3 py-1.5 text-xs font-medium", getVariantClasses()), children: [!config.hideArrow && (_jsx("div", { className: cn("absolute w-2 h-2", getVariantClasses(), "border-0"), style: getArrowStyle() })), content] }) }), document.body);
};
// Re-export as named export
export { Tooltip, TooltipTrigger };
// For legacy compatibility (if needed, otherwise remove)
export const Tooltips = Tooltip;
// Deprecated TooltipContent, kept for backward compatibility with a warning
export const TooltipContent = React.forwardRef((props, _ref) => {
console.warn("TooltipContent is deprecated. Use the Tooltip component with content prop instead.");
return _jsx("div", { ...props });
});
TooltipContent.displayName = "TooltipContent";
// Backward compatibility for TooltipProvider (if needed, otherwise remove)
export const TooltipProvider = ({ children }) => {
return _jsx(_Fragment, { children: children });
};