analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 10.2 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/Tooltip/Tooltip.tsx"],"sourcesContent":["import {\n ReactNode,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\n\n/**\n * Tooltip position options\n */\ntype TooltipPosition = 'top' | 'bottom' | 'left' | 'right';\n\n/**\n * Tooltip component props interface\n */\nexport interface TooltipProps {\n /** Content to display in the tooltip */\n content: ReactNode;\n /** Element that triggers the tooltip */\n children: ReactNode;\n /** Position of the tooltip relative to the trigger */\n position?: TooltipPosition;\n /** Additional className for the tooltip container */\n className?: string;\n /** Additional className for the tooltip content */\n contentClassName?: string;\n /** Whether the tooltip is disabled */\n disabled?: boolean;\n /**\n * Render the tooltip inside a React Portal attached to document.body.\n * Use this when the trigger lives inside an ancestor with `overflow:hidden`\n * (e.g. scroll containers) that would otherwise clip the tooltip.\n */\n usePortal?: boolean;\n}\n\n/**\n * Position classes for tooltip placement (non-portal mode, CSS-only)\n */\nconst POSITION_CLASSES: Record<TooltipPosition, string> = {\n top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',\n bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',\n left: 'right-full top-1/2 -translate-y-1/2 mr-2',\n right: 'left-full top-1/2 -translate-y-1/2 ml-2',\n};\n\nconst TOOLTIP_GAP_PX = 8;\n\n/**\n * Compute fixed-position coordinates relative to the viewport for a given\n * trigger element and desired tooltip position.\n */\nconst computePortalCoords = (\n triggerRect: DOMRect,\n position: TooltipPosition,\n tooltipRect: { width: number; height: number }\n): { top: number; left: number } => {\n switch (position) {\n case 'bottom':\n return {\n top: triggerRect.bottom + TOOLTIP_GAP_PX,\n left: triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2,\n };\n case 'left':\n return {\n top: triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2,\n left: triggerRect.left - tooltipRect.width - TOOLTIP_GAP_PX,\n };\n case 'right':\n return {\n top: triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2,\n left: triggerRect.right + TOOLTIP_GAP_PX,\n };\n case 'top':\n default:\n return {\n top: triggerRect.top - tooltipRect.height - TOOLTIP_GAP_PX,\n left: triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2,\n };\n }\n};\n\nconst TOOLTIP_CONTENT_CLASSES = cn(\n 'whitespace-nowrap',\n 'px-4 py-2 rounded-lg',\n 'bg-background-dark text-white',\n 'text-sm font-medium',\n 'shadow-[0px_3px_10px_0px_rgba(38,38,38,0.2)]',\n 'transition-opacity duration-150'\n);\n\n/**\n * Tooltip component - Displays contextual information on hover/focus\n *\n * By default uses a CSS-only approach with `group-hover` for performance.\n * When `usePortal` is true, the tooltip is rendered in a React Portal so it\n * escapes ancestors with `overflow:hidden`.\n *\n * @example\n * ```tsx\n * <Tooltip content=\"Desempenho baseado nas atividades\">\n * <Info size={18} weight=\"bold\" className=\"text-text-950 cursor-pointer\" />\n * </Tooltip>\n * ```\n */\nexport function Tooltip({\n content,\n children,\n position = 'top',\n className,\n contentClassName,\n disabled = false,\n usePortal = false,\n}: Readonly<TooltipProps>) {\n const triggerRef = useRef<HTMLSpanElement>(null);\n const tooltipRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n const [coords, setCoords] = useState<{ top: number; left: number }>({\n top: 0,\n left: 0,\n });\n\n const updatePosition = useCallback(() => {\n if (!triggerRef.current || !tooltipRef.current) return;\n const triggerRect = triggerRef.current.getBoundingClientRect();\n const tooltipRect = {\n width: tooltipRef.current.offsetWidth,\n height: tooltipRef.current.offsetHeight,\n };\n setCoords(computePortalCoords(triggerRect, position, tooltipRect));\n }, [position]);\n\n useLayoutEffect(() => {\n if (!usePortal || !open) return;\n updatePosition();\n const onViewportChange = () => updatePosition();\n window.addEventListener('resize', onViewportChange);\n window.addEventListener('scroll', onViewportChange, true);\n return () => {\n window.removeEventListener('resize', onViewportChange);\n window.removeEventListener('scroll', onViewportChange, true);\n };\n }, [usePortal, open, updatePosition, content]);\n\n /**\n * Attach hover/focus listeners via the DOM API (not JSX props) so the\n * wrapper element stays semantically non-interactive. This satisfies the\n * SonarQube `S6848`/`jsx-a11y/no-static-element-interactions` rule which\n * fires on static elements (`<span>`/`<div>`) that carry interactive JSX\n * handlers (`onMouseEnter`/`onFocus`/etc.).\n */\n useEffect(() => {\n if (!usePortal) return;\n const node = triggerRef.current;\n if (!node) return;\n\n const handleOpen = () => setOpen(true);\n const handleClose = () => setOpen(false);\n\n node.addEventListener('mouseenter', handleOpen);\n node.addEventListener('mouseleave', handleClose);\n node.addEventListener('focusin', handleOpen);\n node.addEventListener('focusout', handleClose);\n\n return () => {\n node.removeEventListener('mouseenter', handleOpen);\n node.removeEventListener('mouseleave', handleClose);\n node.removeEventListener('focusin', handleOpen);\n node.removeEventListener('focusout', handleClose);\n };\n }, [usePortal]);\n\n if (disabled) {\n return <>{children}</>;\n }\n\n if (usePortal) {\n return (\n <Text\n as=\"span\"\n ref={triggerRef}\n className={cn('relative inline-flex', className)}\n aria-describedby={open ? 'tooltip-portal' : undefined}\n >\n {children}\n {open &&\n typeof document !== 'undefined' &&\n createPortal(\n <div\n ref={tooltipRef}\n id=\"tooltip-portal\"\n role=\"tooltip\"\n style={{\n position: 'fixed',\n top: coords.top,\n left: coords.left,\n zIndex: 9999,\n }}\n className={cn(TOOLTIP_CONTENT_CLASSES, contentClassName)}\n >\n {content}\n </div>,\n document.body\n )}\n </Text>\n );\n }\n\n return (\n <div className={cn('relative inline-flex group', className)}>\n {children}\n\n {/* Tooltip content - shown on hover/focus via CSS */}\n <div\n role=\"tooltip\"\n className={cn(\n 'absolute z-50',\n TOOLTIP_CONTENT_CLASSES,\n 'opacity-0 invisible',\n 'group-hover:opacity-100 group-hover:visible',\n 'group-focus-within:opacity-100 group-focus-within:visible',\n POSITION_CLASSES[position],\n contentClassName\n )}\n >\n {content}\n </div>\n </div>\n );\n}\n\nexport default Tooltip;\n"],"mappings":"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","names":[]}