All files Tooltip.tsx

100% Statements 74/74
100% Branches 28/28
100% Functions 5/5
100% Lines 74/74

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 109 110 111 1122x 1x 1x 1x     1x 2x               2x 2x   2x 2x 19x 19x 19x   19x 19x 19x     19x   2x 2x 2x 2x       2x 2x 22x 22x 2x 2x 19x 19x 19x 19x 19x 19x 19x   19x     19x                 2x 2x 22x 22x 22x 22x 22x 22x 22x 2x 2x 22x   6x 6x 18x 18x 18x 18x   6x 6x 6x 6x 18x 22x 22x 22x 22x   22x   22x 22x 22x 22x 22x 22x 22x 22x         22x   1x  
import React, {
  createContext,
  useContext,
  useState,
  ReactNode,
  FC,
} from 'react';
import { cn } from '../lib/utils';
 
type TooltipContextType = {
  isVisible: boolean;
  show: () => void;
  hide: () => void;
};
 
// 1. Create context
const TooltipContext = createContext<TooltipContextType | undefined>(undefined);
 
// 2. The context provider—manages the hover state
const TooltipProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [isVisible, setIsVisible] = useState(false);
  const show = () => setIsVisible(true);
  const hide = () => setIsVisible(false);
 
  return (
    <TooltipContext.Provider value={{ isVisible, show, hide }}>
      {children}
    </TooltipContext.Provider>
  );
};
 
// 3. Root wrapper—sets up relative positioning & provider
const Tooltip: FC<{ children: ReactNode }> = ({ children }) => (
  <div className='relative inline-block'>
    <TooltipProvider>{children}</TooltipProvider>
  </div>
);
 
// 4. Trigger—automatically wires up mouse events
const TooltipTrigger: FC<{ children: ReactNode }> = ({ children }) => {
  const ctx = useContext(TooltipContext);
  if (!ctx) {
    throw new Error('TooltipTrigger must be inside <Tooltip>…</Tooltip>');
  }
  return (
    <div
      className='cursor-pointer'
      onMouseEnter={ctx.show}
      onMouseLeave={ctx.hide}
      onFocus={ctx.show}
      onBlur={ctx.hide}
    >
      {children}
    </div>
  );
};
 
interface TooltipContentProps {
  children: ReactNode;
  className?: string;
  side?: 'top' | 'bottom' | 'left' | 'right';
  sideOffset?: number; // in pixels
}
 
// 5. Content—reads state from context, no more required prop
const TooltipContent: FC<TooltipContentProps> = ({
  children,
  className,
  side = 'top',
  sideOffset = 8,
}) => {
  const ctx = useContext(TooltipContext);
  if (!ctx) {
    throw new Error('TooltipContent must be inside <Tooltip>…</Tooltip>');
  }
  if (!ctx.isVisible) return null;
 
  // build a little inline style for the offset
  const offsetStyle: React.CSSProperties = {};
  if (side === 'top') offsetStyle.marginBottom = sideOffset;
  if (side === 'bottom') offsetStyle.marginTop = sideOffset;
  if (side === 'left') offsetStyle.marginRight = sideOffset;
  if (side === 'right') offsetStyle.marginLeft = sideOffset;
 
  return (
    <div
      className={cn(
        'absolute z-50 w-max max-w-xs bg-primary text-primary-foreground text-xs rounded-md px-3 py-1.5 shadow-md animate-fade-in',
        side === 'top' && 'bottom-full left-1/2 transform -translate-x-1/2',
        side === 'bottom' && 'top-full left-1/2 transform -translate-x-1/2',
        side === 'left' && 'right-full top-1/2 transform -translate-y-1/2',
        side === 'right' && 'left-full top-1/2 transform -translate-y-1/2',
        className
      )}
      style={offsetStyle}
    >
      {children}
      <div
        className={cn(
          'absolute h-2 w-2 bg-primary transform rotate-45',
          side === 'top' && 'bottom-[-4px] left-1/2 transform -translate-x-1/2',
          side === 'bottom' && 'top-[-4px] left-1/2 transform -translate-x-1/2',
          side === 'left' && 'right-[-4px] top-1/2 transform -translate-y-1/2',
          side === 'right' && 'left-[-4px] top-1/2 transform -translate-y-1/2'
        )}
      />
    </div>
  );
};
 
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };