| import cn from "@utils/classnames.ts"; |
| import { type ReactNode, useRef, useState } from "react"; |
|
|
| type TooltipPosition = "top" | "bottom" | "left" | "right"; |
|
|
| type TextSize = "xs" | "sm" | "md" | "lg" | "xl"; |
|
|
| export interface TooltipProps { |
| children: ReactNode; |
| text: ReactNode; |
| position?: TooltipPosition; |
| className?: string; |
| textSize?: TextSize; |
| } |
|
|
| export default function Tooltip({ |
| children, |
| text, |
| position = "top", |
| className = "", |
| textSize = "md", |
| }: TooltipProps) { |
| const [isVisible, setIsVisible] = useState(false); |
| const timeoutRef = useRef<number>(null); |
|
|
| const positionClasses: Record<TooltipPosition, string> = { |
| top: "bottom-full left-1/2 -translate-x-1/2 mb-2", |
| bottom: "top-full left-1/2 -translate-x-1/2 mt-2", |
| left: "right-full top-1/2 -translate-y-1/2 mr-2", |
| right: "left-full top-1/2 -translate-y-1/2 ml-2", |
| }; |
|
|
| const arrowClasses: Record<TooltipPosition, string> = { |
| top: "top-full left-1/2 -translate-x-1/2 border-l-transparent border-r-transparent border-b-transparent border-t-gray-900 dark:border-t-gray-100", |
| bottom: |
| "bottom-full left-1/2 -translate-x-1/2 border-l-transparent border-r-transparent border-t-transparent border-b-gray-900 dark:border-b-gray-100", |
| left: "left-full top-1/2 -translate-y-1/2 border-t-transparent border-b-transparent border-r-transparent border-l-gray-900 dark:border-l-gray-100", |
| right: |
| "right-full top-1/2 -translate-y-1/2 border-t-transparent border-b-transparent border-l-transparent border-r-gray-900 dark:border-r-gray-100", |
| }; |
|
|
| const textSizeClass: Record<TextSize, string> = { |
| xs: "text-xs", |
| sm: "text-sm", |
| md: "text-base", |
| lg: "text-lg", |
| xl: "text-xl", |
| }; |
|
|
| const showTooltip = () => { |
| if (timeoutRef.current) { |
| clearTimeout(timeoutRef.current); |
| timeoutRef.current = null; |
| } |
| setIsVisible(true); |
| }; |
|
|
| const hideTooltip = () => { |
| timeoutRef.current = window.setTimeout(() => { |
| setIsVisible(false); |
| }, 100); |
| }; |
|
|
| return ( |
| <div |
| className={cn("relative inline-block", className)} |
| onMouseEnter={showTooltip} |
| onMouseLeave={hideTooltip} |
| onFocus={showTooltip} |
| onBlur={hideTooltip} |
| > |
| <span tabIndex={0} className="block"> |
| {children} |
| </span> |
| {isVisible && ( |
| <div |
| className={cn( |
| "absolute z-50", |
| positionClasses[position], |
| "animate-fadeIn" |
| )} |
| role="tooltip" |
| > |
| <div |
| className={cn( |
| textSizeClass[textSize], |
| "rounded bg-gray-900 px-3 py-2 text-sm whitespace-nowrap text-white dark:bg-gray-100 dark:text-gray-900" |
| )} |
| > |
| {text} |
| </div> |
| <div |
| className={cn("absolute h-0 w-0 border-4", arrowClasses[position])} |
| /> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|