import cls from 'classnames'; import React, {useEffect, useRef, useState, CSSProperties, ReactNode, useLayoutEffect} from 'react'; import {fromEvent as rxFromEvent, Subscription, timer, race} from 'rxjs'; import {tap, switchMap} from 'rxjs/operators'; import {clamp, renderForeground} from '@core0/utils'; import cn from './tooltip.module.styl'; interface TooltipProps { enabled?: boolean; duration?: number; type?: 'normal' | 'error'; style?: CSSProperties; className?: string; children?: ReactNode; } /** * Spinner effect used for resources loading */ export const Tooltip = (props: TooltipProps) => { const { enabled = true, duration = 10_000, type = 'normal', className, style, children } = props; const tooltipTrapRef = useRef(null); const tooltipElmRef = useRef(null); const [isTooltipActive, setIsTooltipActive] = useState(false); /* **************************************** * Effect Hooks **************************************** */ useLayoutEffect(() => { const anchorElm = tooltipTrapRef.current?.parentElement; if (!enabled || !anchorElm) return; const sub = subscribeTooltipShowAndDismiss(anchorElm); return () => { sub.unsubscribe(); } }, [enabled, duration]); /* **************************************** * Event Handlers **************************************** */ function subscribeTooltipShowAndDismiss(anchorElm: HTMLElement): Subscription { return rxFromEvent(anchorElm, 'mouseenter') .pipe( tap(() => setIsTooltipActive(true)), tap(() => { requestAnimationFrame(() => { const tooltipElm = tooltipElmRef.current; if (!tooltipElm) return; const { top: anchorElmTop, left: anchorElmLeft, width: anchorElmWidth } = anchorElm.getBoundingClientRect(); const {width: tooltipElmWidth, height: tooltipElmHeight} = tooltipElm.getBoundingClientRect(); const gapBetweenAnchorElm = 5; const tooltipElmTop = anchorElmTop - tooltipElmHeight - gapBetweenAnchorElm; const tooltipElmLeft = anchorElmLeft - (tooltipElmWidth - anchorElmWidth) / 2; const viewportWidth = window.innerWidth ?? document.documentElement.offsetWidth; const viewportHeight = window.innerHeight ?? document.documentElement.offsetHeight; const clampedTooltipElmTop = clamp(0, tooltipElmTop, viewportHeight - tooltipElmHeight); const clampedTooltipElmLeft = clamp(0, tooltipElmLeft, viewportWidth - tooltipElmWidth); tooltipElm.style.top = `${clampedTooltipElmTop}px`; tooltipElm.style.left = `${clampedTooltipElmLeft}px`; tooltipElm.style.opacity = '1'; }); }), switchMap(() => race(rxFromEvent(anchorElm, 'mouseleave'), timer(duration))) ) .subscribe(() => setIsTooltipActive(false)); } return ( <>
{isTooltipActive && renderForeground(
{children}
)} ); };