tooltip.tsx raw

   1  import * as React from "react";
   2  import { type PropsWithChildren, createContext, useContext } from "react";
   3  import {
   4    Popover as PopoverPrimitive,
   5    Tooltip as TooltipPrimitive,
   6  } from "radix-ui";
   7  
   8  import { cn } from "src/lib/utils";
   9  
  10  import { Popover, PopoverContent, PopoverTrigger } from "./popover";
  11  
  12  function TooltipProvider({
  13    delayDuration = 0,
  14    ...props
  15  }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
  16    return (
  17      <TooltipPrimitive.Provider
  18        data-slot="tooltip-provider"
  19        delayDuration={delayDuration}
  20        {...props}
  21      />
  22    );
  23  }
  24  
  25  function RadixTooltipRootWithProvider(
  26    props: React.ComponentProps<typeof TooltipPrimitive.Root>
  27  ) {
  28    return (
  29      <TooltipProvider delayDuration={200}>
  30        <TooltipPrimitive.Root data-slot="tooltip" {...props} />
  31      </TooltipProvider>
  32    );
  33  }
  34  
  35  function BaseTooltipContent({
  36    className,
  37    sideOffset = 0,
  38    children,
  39    ...props
  40  }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
  41    return (
  42      <TooltipPrimitive.Portal>
  43        <TooltipPrimitive.Content
  44          data-slot="tooltip-content"
  45          sideOffset={sideOffset}
  46          className={cn(
  47            "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
  48            className
  49          )}
  50          {...props}
  51        >
  52          {children}
  53          <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
  54        </TooltipPrimitive.Content>
  55      </TooltipPrimitive.Portal>
  56    );
  57  }
  58  
  59  // Hybrid tooltip for touch devices
  60  
  61  const TouchContext = createContext<boolean | undefined>(undefined);
  62  
  63  const useTouch = () => useContext(TouchContext);
  64  
  65  function TouchProvider(props: PropsWithChildren) {
  66    const isTouch = React.useMemo(
  67      () => window.matchMedia("(pointer: coarse)").matches,
  68      []
  69    );
  70  
  71    return <TouchContext.Provider value={isTouch} {...props} />;
  72  }
  73  
  74  type HybridTooltipProps = React.ComponentProps<typeof TooltipPrimitive.Root> &
  75    React.ComponentProps<typeof PopoverPrimitive.Root>;
  76  
  77  function HybridTooltip(props: HybridTooltipProps) {
  78    const isTouch = useTouch();
  79  
  80    return isTouch ? (
  81      <Popover {...props} />
  82    ) : (
  83      <RadixTooltipRootWithProvider {...props} />
  84    );
  85  }
  86  
  87  type HybridTooltipTriggerProps = React.ComponentProps<
  88    typeof TooltipPrimitive.Trigger
  89  > &
  90    React.ComponentProps<typeof PopoverPrimitive.Trigger>;
  91  
  92  function HybridTooltipTrigger(props: HybridTooltipTriggerProps) {
  93    const isTouch = useTouch();
  94  
  95    return isTouch ? (
  96      <PopoverTrigger {...props} />
  97    ) : (
  98      <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
  99    );
 100  }
 101  
 102  type HybridTooltipContentProps = React.ComponentProps<
 103    typeof TooltipPrimitive.Content
 104  > &
 105    React.ComponentProps<typeof PopoverPrimitive.Content>;
 106  
 107  function HybridTooltipContent({
 108    className,
 109    ...props
 110  }: HybridTooltipContentProps) {
 111    const isTouch = useTouch();
 112  
 113    return isTouch ? (
 114      <PopoverContent
 115        {...props}
 116        className={cn("bg-foreground text-background text-sm", className)}
 117      />
 118    ) : (
 119      <BaseTooltipContent {...props} className={className} />
 120    );
 121  }
 122  
 123  export {
 124    HybridTooltip as Tooltip,
 125    HybridTooltipContent as TooltipContent,
 126    TooltipProvider,
 127    HybridTooltipTrigger as TooltipTrigger,
 128    TouchProvider,
 129  };
 130