import { cloneElement, isValidElement, useEffect, useId, useRef } from 'react' import PropTypes from 'prop-types' import { useTooltipContext } from '../context/TooltipContext' const mergeHandler = (original, next) => (event) => { next(event) original?.(event) } const Tooltip = ({ children, title, content, listenParents = 0, hideBorder = false }) => { const { showTooltip, hideTooltip } = useTooltipContext() const id = useId() const tooltipContent = title ?? content const spanRef = useRef(null) const hoveredTargetsRef = useRef(new Set()) const hoveringRef = useRef(false) const tooltipContentRef = useRef(tooltipContent) const hideBorderRef = useRef(hideBorder) tooltipContentRef.current = tooltipContent hideBorderRef.current = hideBorder useEffect(() => { return () => hideTooltip(id) }, [hideTooltip, id]) useEffect(() => { if (hoveringRef.current) { showTooltip(id, tooltipContent, undefined, undefined, hideBorder) } }, [id, showTooltip, tooltipContent, hideBorder]) const onMouseEnter = (event) => { hoveredTargetsRef.current.add(event.currentTarget) hoveringRef.current = true showTooltip( id, tooltipContentRef.current, event.clientX, event.clientY, hideBorderRef.current ) } const onMouseLeave = (event) => { hoveredTargetsRef.current.delete(event.currentTarget) if (hoveredTargetsRef.current.size > 0) return hoveringRef.current = false hideTooltip(id) } const onMouseEnterRef = useRef(onMouseEnter) const onMouseLeaveRef = useRef(onMouseLeave) onMouseEnterRef.current = onMouseEnter onMouseLeaveRef.current = onMouseLeave useEffect(() => { const span = spanRef.current if (!span || listenParents < 1) return const hoveredTargets = hoveredTargetsRef.current const handleEnter = (event) => onMouseEnterRef.current(event) const handleLeave = (event) => onMouseLeaveRef.current(event) const parents = [] let current = span.parentElement for (let i = 0; i < listenParents && current; i += 1) { parents.push(current) current = current.parentElement } parents.forEach((parent) => { parent.addEventListener('mouseenter', handleEnter) parent.addEventListener('mouseleave', handleLeave) }) return () => { parents.forEach((parent) => { parent.removeEventListener('mouseenter', handleEnter) parent.removeEventListener('mouseleave', handleLeave) }) hoveredTargets.clear() } }, [listenParents]) const wrapWithSpan = (node) => ( {node} ) if (isValidElement(children)) { const trigger = cloneElement(children, { onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter), onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave) }) if (listenParents > 0) { return wrapWithSpan(children) } if (children.props.disabled) { return wrapWithSpan(trigger) } return trigger } return wrapWithSpan(children) } Tooltip.propTypes = { children: PropTypes.node.isRequired, title: PropTypes.node, content: PropTypes.node, listenParents: PropTypes.number, hideBorder: PropTypes.bool } export default Tooltip