import * as React from "react"; import classNames from "classnames"; import filterInvalidDOMProps from "filter-invalid-dom-props"; import * as uuid from "uuid"; import { Box, Text } from "#gdq/index"; import { animated, SpringConfig, useTransition } from "@react-spring/web"; import { createLayer, removeLayer, useLayerSubscription } from "../Layer/LayersStore"; import { Align, Attach, PositionedLayerProps, usePositionedLayer } from "../Layer/PositionedLayer"; import styles from "./Tooltip.module.css"; export interface TooltipTargetProps { ref: React.RefObject; onMouseEnter: () => unknown; onMouseLeave: () => unknown; onFocus: () => unknown; onBlur: () => unknown; onContextMenu: () => unknown; } export interface TooltipOptions { attach?: Attach; align?: Align; offset?: number; className?: string; } /** * A hook for creating a Tooltip without having to wrap the target element with * another component, and for gaining more granular control over properties, like * imperatively opening/closing the Tooltip. */ export function useTooltip( render: React.ReactNode, options?: TooltipOptions, ): [TooltipTargetProps, () => void, () => void, string] { const attach = options?.attach ?? "top"; const align = options?.align ?? "middle"; const offset = options?.offset ?? 8; const className = options?.className; const targetRef = React.useRef(null); const [name] = React.useState(() => uuid.v4()); const exists = useLayerSubscription(name) != null; const openTooltip = React.useCallback(() => { if (exists) return; const target = targetRef.current; if (target == null) return; const inner = typeof render === "string" ? {render} : render; createLayer({ name, render: () => ( {inner} ), }); }, [exists, render, name, className, attach, align, offset]); const closeTooltip = React.useCallback(() => { if (!exists) return; removeLayer(name); }, [exists, name]); // If the containing component unmounts, ensure that the tooltip goes away // as well. Otherwise they get stuck and are unable to be removed. React.useEffect(() => { return () => removeLayer(name); }, [name]); return [ { ref: targetRef, onMouseEnter: openTooltip, onMouseLeave: closeTooltip, onFocus: openTooltip, onBlur: closeTooltip, onContextMenu: closeTooltip, }, openTooltip, closeTooltip, name, ]; } const TOOLTIP_SPRING_CONFIG: SpringConfig = { tension: 400, friction: 10, clamp: true, }; function TooltipLayer(props: PositionedLayerProps) { const { children, target, attach, align, offset, className, ...passthroughProps } = props; const contentRef = React.useRef(null); const positionStyle = usePositionedLayer({ target, attach, align, offset }, contentRef); const transitions = useTransition(children, { from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: TOOLTIP_SPRING_CONFIG, }); return transitions((style, item) => ( {item} )); } export interface TooltipProps extends TooltipOptions { render: React.ReactNode; children: (props: TooltipTargetProps) => JSX.Element; } /** * Convenience component for rendering a Tooltip in non-hooks contexts. */ export function Tooltip(props: TooltipProps) { const { render, attach, align, offset, children } = props; const [tooltipProps] = useTooltip(render, { attach, align, offset }); return children(tooltipProps); }