From ef578373710c6e2a74ac8e38dd701914faa5110d Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Thu, 20 Aug 2026 21:46:19 +0100 Subject: [PATCH] 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. --- .../Dashboard/common/DashboardSidebar.jsx | 10 ++- src/components/Dashboard/common/Tooltip.jsx | 75 +++++++++++++++---- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx index 9438d5c9..9cd15116 100644 --- a/src/components/Dashboard/common/DashboardSidebar.jsx +++ b/src/components/Dashboard/common/DashboardSidebar.jsx @@ -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 ( - + {iconNode ?? } ) diff --git a/src/components/Dashboard/common/Tooltip.jsx b/src/components/Dashboard/common/Tooltip.jsx index 95bbf2c2..f9a10d9d 100644 --- a/src/components/Dashboard/common/Tooltip.jsx +++ b/src/components/Dashboard/common/Tooltip.jsx @@ -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) => ( + + {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 ( - - {trigger} - - ) + return wrapWithSpan(trigger) } return trigger } - return ( - - {children} - - ) + return wrapWithSpan(children) } Tooltip.propTypes = { children: PropTypes.node.isRequired, title: PropTypes.node, - content: PropTypes.node + content: PropTypes.node, + listenParents: PropTypes.number } export default Tooltip