UNPKG

@harvest-profit/npk

Version:
118 lines (103 loc) 4.08 kB
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import ThemeContext from '../ThemeContext'; import classes from './Tooltip.module.css'; import { computePosition, flip, inline, offset, shift } from '@floating-ui/dom'; const HOVER_SHOW_TOOLTIP_AFTER = 400;// ms const CLICK_SHOW_TOOLTIP_AFTER = 1000;// ms const EVENT_OPEN = 'NPK:TooltipOpen'; const EVENT_CLOSE = 'NPK:TooltipClose'; const Tooltip = ({ id = null, targetRef = null, as: Component = 'div', inline: inlineOption = false, // honestly I haven't seen this work yet children }) => { const theme = useContext(ThemeContext); const elementRef = useRef(); const tooltipRef = useRef(); const animationTimeout = useRef(); const tooltipStyleRef = useRef({}); const [isMounted, setIsMounted] = useState(false); const [isOpen, setIsOpen] = useState(false); const openTooltip = useCallback(() => { const event = new CustomEvent(EVENT_OPEN, { bubbles: true }); elementRef.current.dispatchEvent(event); clearTimeout(animationTimeout.current); setIsMounted(true); animationTimeout.current = setTimeout(() => setIsOpen(true), HOVER_SHOW_TOOLTIP_AFTER); }, []); const closeTooltip = useCallback(() => { const event = new CustomEvent(EVENT_CLOSE, { bubbles: true }); elementRef.current.dispatchEvent(event); clearTimeout(animationTimeout.current); setIsOpen(false); animationTimeout.current = setTimeout(() => setIsMounted(false), 100); }, []); const delayOpeningTooltip = useCallback(() => { clearTimeout(animationTimeout.current); animationTimeout.current = setTimeout(() => openTooltip(), CLICK_SHOW_TOOLTIP_AFTER); }, [openTooltip]); const skipOpeningTooltip = useCallback((event) => { if (event.target === elementRef.current) return; if (!elementRef.current.contains(event.target)) return; closeTooltip(); }, [closeTooltip]); const potentiallyOpenTooltip = useCallback((event) => { if (event.target === elementRef.current) return; if (!elementRef.current.contains(event.target)) return; openTooltip(); }, [openTooltip]); useEffect(() => { if (targetRef?.current) { elementRef.current = targetRef.current; } else if (id) { elementRef.current = document.querySelector(`[aria-describedby="${id}"]`); } elementRef.current?.addEventListener("mouseenter", openTooltip); elementRef.current?.addEventListener("mouseleave", closeTooltip); elementRef.current?.addEventListener("click", delayOpeningTooltip); elementRef.current?.addEventListener(EVENT_OPEN, skipOpeningTooltip); elementRef.current?.addEventListener(EVENT_CLOSE, potentiallyOpenTooltip); return () => { elementRef.current?.removeEventListener("mouseenter", openTooltip); elementRef.current?.removeEventListener("mouseleave", closeTooltip); elementRef.current?.removeEventListener("click", delayOpeningTooltip); elementRef.current?.removeEventListener(EVENT_OPEN, skipOpeningTooltip); elementRef.current?.removeEventListener(EVENT_CLOSE, potentiallyOpenTooltip); } }, [targetRef?.current, id, openTooltip, closeTooltip, delayOpeningTooltip, skipOpeningTooltip, potentiallyOpenTooltip]); const setTooltipRef = (ref) => { tooltipRef.current = ref; if (tooltipRef.current) { computePosition( elementRef.current, tooltipRef.current, { middleware: [ inlineOption && inline(), offset(4), flip(), shift({ padding: 5 }), ] } ).then(({ x, y }) => { tooltipStyleRef.current = { left: x, top: y } }); } } if (!isMounted) return null; return ReactDOM.createPortal( ( <Component ref={setTooltipRef} className={classes.Tooltip} role="tooltip" data-visibility={isOpen} style={tooltipStyleRef.current}> {children} </Component> ), document.getElementById(theme.appendRootId), ); } export default Tooltip;