All files / web/src/components/ui Tooltip.tsx

72.22% Statements 78/108
100% Branches 0/0
0% Functions 0/2
72.22% Lines 78/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 1091x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                         1x 1x 1x 1x 1x 1x     1x 1x 1x  
'use client'
 
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import type { ReactNode } from 'react'
import { css, cx } from '../../../styled-system/css'
 
export interface TooltipProps {
  /** The element that triggers the tooltip */
  children: ReactNode
  /** The tooltip content - can be a string or rich JSX */
  content: ReactNode
  /** Side of the trigger to render the tooltip */
  side?: 'top' | 'right' | 'bottom' | 'left'
  /** Alignment relative to the trigger */
  align?: 'start' | 'center' | 'end'
  /** Delay before showing (ms) */
  delayDuration?: number
  /** Additional class name for the content */
  contentClassName?: string
  /** Whether the tooltip is open (controlled mode) */
  open?: boolean
  /** Callback when open state changes */
  onOpenChange?: (open: boolean) => void
}
 
const contentStyles = css({
  // Must be above modals (10001) so tooltips inside modals are visible
  zIndex: 15000,
  overflow: 'hidden',
  borderRadius: '8px',
  padding: '0.75rem 1rem',
  fontSize: '0.875rem',
  lineHeight: '1.4',
  maxWidth: '320px',
  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',
  backgroundColor: 'gray.900',
  color: 'gray.100',
  // Animation
  animationDuration: '200ms',
  animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
  willChange: 'transform, opacity',
  '&[data-state="delayed-open"]': {
    '&[data-side="top"]': { animationName: 'slideDownAndFade' },
    '&[data-side="right"]': { animationName: 'slideLeftAndFade' },
    '&[data-side="bottom"]': { animationName: 'slideUpAndFade' },
    '&[data-side="left"]': { animationName: 'slideRightAndFade' },
  },
})
 
const arrowStyles = css({
  fill: 'gray.900',
})
 
/**
 * Tooltip component using Radix UI primitives
 *
 * Usage:
 * ```tsx
 * <Tooltip content="This is a tooltip">
 *   <button>Hover me</button>
 * </Tooltip>
 *
 * // Rich content
 * <Tooltip content={<div><strong>Title</strong><p>Description</p></div>}>
 *   <span>Info</span>
 * </Tooltip>
 * ```
 */
export function Tooltip({
  children,
  content,
  side = 'top',
  align = 'center',
  delayDuration = 200,
  contentClassName,
  open,
  onOpenChange,
}: TooltipProps) {
  return (
    <TooltipPrimitive.Provider>
      <TooltipPrimitive.Root delayDuration={delayDuration} open={open} onOpenChange={onOpenChange}>
        <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
        <TooltipPrimitive.Portal>
          <TooltipPrimitive.Content
            side={side}
            align={align}
            sideOffset={6}
            className={cx(contentStyles, contentClassName)}
          >
            {content}
            <TooltipPrimitive.Arrow className={arrowStyles} width={12} height={6} />
          </TooltipPrimitive.Content>
        </TooltipPrimitive.Portal>
      </TooltipPrimitive.Root>
    </TooltipPrimitive.Provider>
  )
}
 
/**
 * Provider that enables tooltips for all children
 * Wrap your app or a section with this to enable tooltips
 */
export function TooltipProvider({ children }: { children: ReactNode }) {
  return <TooltipPrimitive.Provider>{children}</TooltipPrimitive.Provider>
}
 
// Re-export primitives for advanced use cases
export { TooltipPrimitive }