Enhance Tooltip Component with Parent Listening Feature

- Updated the Tooltip component to include a new prop, `listenParents`, allowing it to listen for mouse events on parent elements, improving tooltip visibility and interaction.
- Refactored mouse event handling to manage hover states more effectively, ensuring tooltips behave consistently when interacting with parent elements.
- Improved code organization and readability by consolidating event handling logic and utilizing refs for better performance.
This commit is contained in:
Tom Butcher 2026-08-20 21:46:19 +01:00
parent 8ab2e057bb
commit ef57837371
2 changed files with 69 additions and 16 deletions

View File

@ -1,4 +1,10 @@
import { cloneElement, isValidElement, useState, useEffect, useContext } from 'react'
import {
cloneElement,
isValidElement,
useState,
useEffect,
useContext
} from 'react'
import { Layout, Menu, Flex, Button } from 'antd'
import { CaretDownFilled } from '@ant-design/icons'
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
@ -23,7 +29,7 @@ const CollapsedItemIcon = ({ className, icon, title }) => {
: icon
return (
<Tooltip title={title}>
<Tooltip title={title} listenParents={1}>
{iconNode ?? <span className={className} />}
</Tooltip>
)

View File

@ -13,11 +13,15 @@ const mergeHandler = (original, next) => (event) => {
original?.(event)
}
const Tooltip = ({ children, title, content }) => {
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)
@ -30,43 +34,86 @@ const Tooltip = ({ children, title, content }) => {
}, [id, showTooltip, tooltipContent])
const onMouseEnter = (event) => {
hoveredTargetsRef.current.add(event.currentTarget)
hoveringRef.current = true
showTooltip(id, tooltipContent, event.clientX, event.clientY)
showTooltip(id, tooltipContentRef.current, event.clientX, event.clientY)
}
const onMouseLeave = () => {
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 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)
})
hoveredTargetsRef.current.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 (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{trigger}
</span>
)
return wrapWithSpan(trigger)
}
return trigger
}
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{children}
</span>
)
return wrapWithSpan(children)
}
Tooltip.propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node,
content: PropTypes.node
content: PropTypes.node,
listenParents: PropTypes.number
}
export default Tooltip