aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
249 lines (246 loc) • 7.7 kB
JavaScript
'use client';
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
import { forwardRef, useState, useRef, useEffect } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { useA11yId, announceToScreenReader } from '../../utils/a11y.js';
import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
import { cn } from '../../lib/utilsComprehensive.js';
const GlassTooltip = /*#__PURE__*/forwardRef(({
content,
children,
position = "top",
showDelay = 300,
hideDelay = 150,
disabled = false,
className = "",
triggerClassName = "",
maxWidth = "200px",
showArrow = true,
variant = "fade",
respectMotionPreference = true,
"aria-label": ariaLabel,
id,
...props
}, ref) => {
const [isVisible, setIsVisible] = useState(false);
const [actualPosition, setActualPosition] = useState(position);
const triggerRef = useRef(null);
const tooltipRef = useRef(null);
const showTimeoutRef = useRef();
const hideTimeoutRef = useRef();
const {
prefersReducedMotion
} = useMotionPreferenceContext();
// Generate unique ID for accessibility
const tooltipId = id || useA11yId("glass-tooltip");
const shouldAnimate = respectMotionPreference ? !prefersReducedMotion : true;
const calculatePosition = () => {
if (!triggerRef.current || !tooltipRef.current) return position;
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Check if tooltip fits in preferred position
const fitsTop = triggerRect.top > tooltipRect.height + 8;
const fitsBottom = triggerRect.bottom + tooltipRect.height + 8 < viewportHeight;
const fitsLeft = triggerRect.left > tooltipRect.width + 8;
const fitsRight = triggerRect.right + tooltipRect.width + 8 < viewportWidth;
// Auto-positioning logic
if (position === "auto") {
if (fitsTop) return "top";
if (fitsBottom) return "bottom";
if (fitsRight) return "right";
if (fitsLeft) return "left";
return "top"; // fallback
}
return position;
};
const handleMouseEnter = () => {
if (disabled) return;
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
showTimeoutRef.current = setTimeout(() => {
setActualPosition(calculatePosition());
setIsVisible(true);
// Announce tooltip to screen readers
if (content && typeof content === "string") {
announceToScreenReader(content, "polite");
} else if (ariaLabel) {
announceToScreenReader(ariaLabel, "polite");
}
}, showDelay);
};
const handleMouseLeave = () => {
if (showTimeoutRef.current) {
clearTimeout(showTimeoutRef.current);
}
hideTimeoutRef.current = setTimeout(() => {
setIsVisible(false);
}, hideDelay);
};
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
if (showTimeoutRef.current) clearTimeout(showTimeoutRef.current);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
};
}, []);
const getAnimationPreset = () => {
if (!shouldAnimate) return "none";
switch (variant) {
case "scale":
return "scaleIn";
case "slide":
return getSlidePreset();
case "fade":
default:
return "fadeIn";
}
};
const getSlidePreset = () => {
switch (actualPosition) {
case "top":
return "slideUp";
case "bottom":
return "slideDown";
case "left":
return "slideLeft";
case "right":
return "slideRight";
default:
return "fadeIn";
}
};
const getPositionStyles = () => {
if (!triggerRef.current || !tooltipRef.current) return {};
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
switch (actualPosition) {
case "top":
return {
top: triggerRect.top - tooltipRect.height - 8,
left: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2
};
case "bottom":
return {
top: triggerRect.bottom + 8,
left: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2
};
case "left":
return {
top: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
left: triggerRect.left - tooltipRect.width - 8
};
case "right":
return {
top: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
left: triggerRect.right + 8
};
default:
return {};
}
};
const getArrowStyles = () => {
switch (actualPosition) {
case "top":
return {
bottom: "-4px",
left: "50%",
transform: "translateX(-50%)"
};
case "bottom":
return {
top: "-4px",
left: "50%",
transform: "translateX(-50%) rotate(180deg)"
};
case "left":
return {
right: "-4px",
top: "50%",
transform: "translateY(-50%) rotate(90deg)"
};
case "right":
return {
left: "-4px",
top: "50%",
transform: "translateY(-50%) rotate(-90deg)"
};
default:
return {};
}
};
if (disabled) {
return jsx(Fragment, {
children: children
});
}
return jsxs(Fragment, {
children: [jsx("div", {
ref: triggerRef,
className: cn("inline-block", triggerClassName),
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
"aria-describedby": isVisible ? tooltipId : undefined,
...props,
children: children
}), isVisible && jsx(MotionFramer, {
preset: getAnimationPreset(),
duration: shouldAnimate ? 200 : 0,
className: 'fixed z-[9999] pointer-events-none',
style: {
...getPositionStyles(),
maxWidth
},
children: jsx("div", {
ref: ref,
id: tooltipId,
role: "tooltip",
"aria-label": ariaLabel,
className: cn("relative", className),
children: jsxs(OptimizedGlassCore, {
ref: tooltipRef,
intent: "neutral",
elevation: "level4",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
className: 'glass-px-3 glass-py-2 glass-radius-lg glass-text-sm text-primary glass-shadow-lg',
children: [content, showArrow && jsx("div", {
className: 'absolute w-0 h-0 glass-border-l-4 glass-border-r-4 glass-border-b-4 glass-border-transparent glass-border-b-white/20',
style: getArrowStyles(),
"aria-hidden": "true"
})]
})
})
})]
});
});
GlassTooltip.displayName = "GlassTooltip";
// Compound component for tooltip with trigger
const GlassTooltipTrigger = ({
asChild,
children
}) => {
return jsx(Fragment, {
children: children
});
};
const GlassTooltipContent = ({
children
}) => {
return jsx(Fragment, {
children: children
});
};
export { GlassTooltip, GlassTooltipContent, GlassTooltipTrigger };
//# sourceMappingURL=GlassTooltip.js.map