import { useFloating, autoUpdate, offset, flip, shift, useHover, useFocus, useDismiss, useRole, useInteractions, FloatingPortal, useDelayGroup, useDelayGroupContext } from '@floating-ui/react-dom-interactions'; import { motion, AnimatePresence } from 'framer-motion'; import * as React from 'react'; import styled from 'styled-components'; import { TooltipOptions, TooltipPlacement } from './Tooltip.constants'; const TooltipWrap = styled(motion.div)<{ $stateX: number | null; $stateY: number | null }>` width: max-content; max-width: 240px; position: absolute; top: 0; left: 0; background: var(--bg-emphasis); padding: 4px 8px; gap: 4px; border-radius: 8px; pointer-events: none; z-index: 9999999999; font-family: 'Skiff Sans Text', sans-serif !important; font-weight: 470; -webkit-font-smoothing: antialiased !important; font-size: 11px !important; line-height: 12px !important; color: var(--text-always-white) !important; overflow-wrap: break-word; overflow: hidden; text-overflow: ellipsis; white-space: normal; top: ${(props) => props.$stateY ?? 0}px; left: ${(props) => props.$stateX ?? 0}px; visibility: ${(props) => (props.$stateX ? 'visible' : 'hidden')}; `; const StyledTooltipTrigger = styled.span<{ $fullWidth: boolean }>` display: flex; ${(props) => props.$fullWidth && 'width: 100%;'} `; function useTooltip({ initialOpen = false, placement = TooltipPlacement.TOP, open: controlledOpen, onOpenChange: setControlledOpen }: TooltipOptions = {}) { const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen); const open = controlledOpen ?? uncontrolledOpen; const setOpen = setControlledOpen ?? setUncontrolledOpen; const { delay } = useDelayGroupContext(); const data = useFloating({ placement, open, onOpenChange: setOpen, whileElementsMounted: autoUpdate, middleware: [offset(5), flip(), shift()] }); const context = data.context; const hover = useHover(context, { enabled: controlledOpen == null, delay }); const focus = useFocus(context, { enabled: controlledOpen == null }); const dismiss = useDismiss(context, { referencePress: true, outsidePress: true }); const role = useRole(context, { role: 'tooltip' }); const interactions = useInteractions([hover, focus, dismiss, role]); return React.useMemo( () => ({ open, setOpen, ...interactions, ...data }), [open, setOpen, interactions, data] ); } type ContextType = ReturnType | null; const TooltipContext = React.createContext(null); const useTooltipState = () => { const context = React.useContext(TooltipContext); if (context == null) { console.error('Tooltip components must be wrapped in '); } return context; }; export default function Tooltip({ children, ...options }: { children: React.ReactNode } & TooltipOptions) { // This can accept any props as options, e.g. `placement`, // or other positioning options. const tooltip = useTooltip(options); return {children}; } export const TooltipTrigger = React.forwardRef< HTMLElement, React.HTMLProps & { asChild?: boolean; fullWidth?: boolean } >(function TooltipTrigger({ children, asChild = false, fullWidth = false, ...props }, propRef) { const state = useTooltipState(); const childrenRef = (children as React.HTMLProps).ref as React.RefObject; if (!state) return null; // `asChild` allows the user to pass any element as the anchor if (asChild && React.isValidElement(children)) { return React.cloneElement( children, state.getReferenceProps({ ref: childrenRef, ...props, ...children.props, 'data-state': state.open ? 'open' : 'closed' } as React.HTMLProps | undefined) ); } return ( {children} ); }); export const TooltipContent = React.forwardRef>(function TooltipContent( props, propRef ) { const state = useTooltipState(); const { context } = useFloating(); const { delay, setCurrentId } = useDelayGroupContext(); useDelayGroup(state?.context || context, { id: props.children }); // props.children should be primitive (e.g. string) React.useLayoutEffect(() => { if (!state) return; if (state.open) { setCurrentId(props.children); } }, [state?.open, props.children, setCurrentId]); if (!state) return null; if (!props.children) return null; return ( {state.open && ( )} ); });