Most of the APIs for Tooltips in React I've seen work something like this:
<Tooltip title="Hello World">
<span>hover me!</span>
</Tooltip>
What that ends up creating is a wrapper around that span, something like this:
<div>
<span>hover me!</span>
</div>
On that div, now, you can add your mouseenter/leave handlers and such, and figure out the rest of the tooltip implementation. I am not concerned with all the other features for implementing a robust React tooltip, but more wondering how from a technical perspective you go about wrapping that <span>, but without adding the extra <div>, I can't see how it can be done in React land (ideally with something like Portals).
If I use React.cloneElement(child, { mouseenter: .... }) you run the risk of overriding the child's mouse handlers, but that is one possible way to do it. (I don't know how to basically do super here, get the passed in mouse handlers, and then wrap them with my own tooltip mouse handler, if they are present, any ideas?).
How would you go about implementing simply the binding part of a tooltip component, where it adds the handlers to the passed in child, using the above sort of API? Maybe you could also cloneElement and add a custom ref or something, but same override problem exists there and not sure how to work around that. Any help would be appreciated. To learn more React technical details, I would like to play around with making a basic tooltip, and see how you should approach this problem in React-land.
I figured it out:
import ReactDOM from 'react-dom'
import React, { useState, useCallback, useEffect } from 'react'
export default function Tooltip({
content,
mouseEnterDelay = 200,
mouseLeaveDelay = 500,
children,
}) {
const [portalContainer, setPortalContainer] = useState(typeof document !== 'undefined' && document.querySelector('#tooltip'))
const [mouseEnterDelayTimer, setMouseEnterDelayTimer] = useState(null)
const [mouseLeaveDelayTimer, setMouseLeaveDelayTimer] = useState(null)
const [isVisible, setIsVisible] = useState(false)
const child = children
const oldMouseEnterHandler = child.props.onMouseEnter
const oldMouseLeaveHandler = child.props.onMouseLeave
const clearMouseLeaveTimer = useCallback(() => {
clearTimeout(mouseLeaveDelayTimer)
setMouseLeaveDelayTimer(null)
}, [mouseLeaveDelayTimer, setMouseLeaveDelayTimer])
const clearMouseEnterTimer = useCallback(() => {
clearTimeout(mouseEnterDelayTimer)
setMouseEnterDelayTimer(null)
}, [mouseEnterDelayTimer, setMouseEnterDelayTimer])
const show = useCallback(() => {
clearMouseEnterTimer()
setIsVisible(true)
}, [setIsVisible, clearMouseEnterTimer])
const hide = useCallback(() => {
clearMouseLeaveTimer()
setIsVisible(false)
}, [setIsVisible, clearMouseLeaveTimer])
const handleMouseEnter = useCallback((e) => {
oldMouseEnterHandler?.(e)
console.log('handleMouseEnter', mouseLeaveDelayTimer)
if (mouseLeaveDelayTimer) {
clearMouseLeaveTimer()
return
}
const timer = setTimeout(show, mouseEnterDelay)
setMouseEnterDelayTimer(timer)
}, [clearMouseLeaveTimer, oldMouseEnterHandler, setMouseEnterDelayTimer, show, mouseEnterDelay, mouseLeaveDelayTimer])
const handleMouseLeave = useCallback((e) => {
console.log('handleMouseLeave', mouseEnterDelayTimer)
oldMouseLeaveHandler?.(e)
if (mouseEnterDelayTimer) {
clearMouseEnterTimer()
return
}
const timer = setTimeout(hide, mouseLeaveDelay)
setMouseLeaveDelayTimer(timer)
}, [clearMouseLeaveTimer, mouseEnterDelayTimer, clearMouseEnterTimer, hide, setMouseLeaveDelayTimer, mouseLeaveDelay])
const newChild = React.cloneElement(child, {
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
})
if (isVisible) {
const portal = ReactDOM.createPortal(
<div>{content}</div>,
portalContainer
)
return (
<>
{portal}
{newChild}
</>
)
}
return newChild
}