- Introduced a variable for hovered targets in the Tooltip component to enhance clarity and maintainability of the hover state management. - Updated the cleanup logic to directly reference the hovered targets variable, streamlining the event listener removal process.
122 lines
3.1 KiB
JavaScript
122 lines
3.1 KiB
JavaScript
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 }) => {
|
|
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)
|
|
tooltipContentRef.current = tooltipContent
|
|
|
|
useEffect(() => {
|
|
return () => hideTooltip(id)
|
|
}, [hideTooltip, id])
|
|
|
|
useEffect(() => {
|
|
if (hoveringRef.current) {
|
|
showTooltip(id, tooltipContent)
|
|
}
|
|
}, [id, showTooltip, tooltipContent])
|
|
|
|
const onMouseEnter = (event) => {
|
|
hoveredTargetsRef.current.add(event.currentTarget)
|
|
hoveringRef.current = true
|
|
showTooltip(id, tooltipContentRef.current, event.clientX, event.clientY)
|
|
}
|
|
|
|
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) => (
|
|
<span
|
|
ref={spanRef}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
>
|
|
{node}
|
|
</span>
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
export default Tooltip
|