Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ const Tooltip = ({
useEffect(() => {
if (!id) return

function getAriaDescribedBy(element: HTMLElement | null) {
function getAriaDescribedBy(element: Element | null) {
return element?.getAttribute('aria-describedby')?.split(' ') || []
}

function removeAriaDescribedBy(element: HTMLElement | null) {
function removeAriaDescribedBy(element: Element | null) {
const newDescribedBy = getAriaDescribedBy(element).filter((s) => s !== id)
if (newDescribedBy.length) {
element?.setAttribute('aria-describedby', newDescribedBy.join(' '))
Expand Down Expand Up @@ -597,10 +597,10 @@ const Tooltip = ({

useImperativeHandle(forwardRef, () => ({
open: (options) => {
let imperativeAnchor: HTMLElement | null = null
let imperativeAnchor: Element | null = null
if (options?.anchorSelect) {
try {
imperativeAnchor = document.querySelector<HTMLElement>(options.anchorSelect)
imperativeAnchor = document.querySelector(options.anchorSelect)
} catch {
if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') {
console.warn(`[react-tooltip] "${options.anchorSelect}" is not a valid CSS selector`)
Expand Down
10 changes: 5 additions & 5 deletions src/components/Tooltip/TooltipTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export interface TooltipRefProps {
/**
* @readonly
*/
activeAnchor: HTMLElement | null
activeAnchor: Element | null
/**
* @readonly
*/
Expand Down Expand Up @@ -129,10 +129,10 @@ export interface ITooltip {
setIsOpen?: (value: boolean) => void
afterShow?: () => void
afterHide?: () => void
disableTooltip?: (anchorRef: HTMLElement | null) => boolean
previousActiveAnchor: HTMLElement | null
activeAnchor: HTMLElement | null
setActiveAnchor: (anchor: HTMLElement | null) => void
disableTooltip?: (anchorRef: Element | null) => boolean
previousActiveAnchor: Element | null
activeAnchor: Element | null
setActiveAnchor: (anchor: Element | null) => void
border?: CSSProperties['border']
opacity?: CSSProperties['opacity']
arrowColor?: CSSProperties['backgroundColor']
Expand Down
12 changes: 6 additions & 6 deletions src/components/Tooltip/anchor-registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type AnchorRegistrySubscriber = (anchors: HTMLElement[], error: Error | null) => void
type AnchorRegistrySubscriber = (anchors: Element[], error: Error | null) => void

type AnchorRegistryEntry = {
anchors: HTMLElement[]
anchors: Element[]
error: Error | null
subscribers: Set<AnchorRegistrySubscriber>
/**
Expand All @@ -25,7 +25,7 @@ function extractTooltipId(selector: string): string | null {
return match ? match[2].replace(/\\(['"])/g, '$1') : null
}

function areAnchorListsEqual(left: HTMLElement[], right: HTMLElement[]) {
function areAnchorListsEqual(left: Element[], right: Element[]) {
if (left.length !== right.length) {
return false
}
Expand All @@ -36,7 +36,7 @@ function areAnchorListsEqual(left: HTMLElement[], right: HTMLElement[]) {
function readAnchorsForSelector(selector: string) {
try {
return {
anchors: Array.from(document.querySelectorAll<HTMLElement>(selector)),
anchors: Array.from(document.querySelectorAll(selector)),
error: null,
}
} catch (error) {
Expand Down Expand Up @@ -146,7 +146,7 @@ function collectAffectedTooltipIds(records: MutationRecord[]): Set<string> | nul

for (const record of records) {
if (record.type === 'attributes') {
const target = record.target as HTMLElement
const target = record.target as Element
const currentId = target.getAttribute?.('data-tooltip-id')
if (currentId) ids.add(currentId)
if (record.oldValue) ids.add(record.oldValue)
Expand All @@ -158,7 +158,7 @@ function collectAffectedTooltipIds(records: MutationRecord[]): Set<string> | nul
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.nodeType !== Node.ELEMENT_NODE) continue
const el = node as HTMLElement
const el = node as Element
const id = el.getAttribute?.('data-tooltip-id')
if (id) ids.add(id)
// For large subtrees, bail out to full refresh to avoid double-scanning
Expand Down
6 changes: 3 additions & 3 deletions src/components/Tooltip/use-tooltip-anchors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ const useTooltipAnchors = ({
id?: string
anchorSelect?: string
imperativeAnchorSelect?: string
activeAnchor: HTMLElement | null
disableTooltip?: (anchorRef: HTMLElement | null) => boolean
activeAnchor: Element | null
disableTooltip?: (anchorRef: Element | null) => boolean
onActiveAnchorRemoved: () => void
trackAnchors: boolean
}) => {
const [rawAnchorElements, setRawAnchorElements] = useState<HTMLElement[]>([])
const [rawAnchorElements, setRawAnchorElements] = useState<Element[]>([])
const [selectorError, setSelectorError] = useState<Error | null>(null)
const warnedSelectorRef = useRef<string | null>(null)
const selector = useMemo(
Expand Down
42 changes: 20 additions & 22 deletions src/components/Tooltip/use-tooltip-events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ const useTooltipEvents = ({
tooltipShowDelayTimerRef,
updateTooltipPosition,
}: {
activeAnchor: HTMLElement | null
anchorElements: HTMLElement[]
activeAnchor: Element | null
anchorElements: Element[]
anchorSelector: string
clickable: boolean
closeEvents?: AnchorCloseEvents
delayHide: number
delayShow: number
disableTooltip?: (anchorRef: HTMLElement | null) => boolean
disableTooltip?: (anchorRef: Element | null) => boolean
float: boolean
globalCloseEvents?: GlobalCloseEvents
handleHideTooltipDelayed: (delay?: number) => void
Expand All @@ -64,7 +64,7 @@ const useTooltipEvents = ({
openEvents?: AnchorOpenEvents
openOnClick: boolean
rendered: boolean
setActiveAnchor: (anchor: HTMLElement | null) => void
setActiveAnchor: (anchor: Element | null) => void
show: boolean
tooltipHideDelayTimerRef: RefObject<NodeJS.Timeout | null>
tooltipRef: RefObject<HTMLElement | null>
Expand All @@ -73,13 +73,13 @@ const useTooltipEvents = ({
}) => {
// Ref-stable debounced handlers — avoids recreating debounce instances on every effect run
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const debouncedShowRef = useRef(debounce((_anchor: HTMLElement | null) => {}, 50, true))
const debouncedShowRef = useRef(debounce((_anchor: Element | null) => {}, 50, true))
const debouncedHideRef = useRef(debounce(() => {}, 50, true))

// Cache scroll parents — only recompute when the element actually changes
const anchorScrollParentRef = useRef<Element | null>(null)
const tooltipScrollParentRef = useRef<Element | null>(null)
const prevAnchorRef = useRef<HTMLElement | null>(null)
const prevAnchorRef = useRef<Element | null>(null)
const prevTooltipRef = useRef<HTMLElement | null>(null)

if (activeAnchor !== prevAnchorRef.current) {
Expand Down Expand Up @@ -187,10 +187,8 @@ const useTooltipEvents = ({
updateTooltipPositionRef.current = updateTooltipPosition

// --- Handler refs (updated every render, read via ref indirection in effects) ---
const resolveAnchorElementRef = useRef<(target: EventTarget | null) => HTMLElement | null>(
() => null,
)
const handleShowTooltipRef = useRef<(anchor: HTMLElement | null) => void>(() => {})
const resolveAnchorElementRef = useRef<(target: EventTarget | null) => Element | null>(() => null)
const handleShowTooltipRef = useRef<(anchor: Element | null) => void>(() => {})
const handleHideTooltipRef = useRef<() => void>(() => {})

const dataTooltipId = anchorSelector ? parseDataTooltipIdSelector(anchorSelector) : null
Expand All @@ -215,8 +213,8 @@ const useTooltipEvents = ({
? targetElement
: targetElement.closest(anchorSelector)) ?? null

if (matchedAnchor && !disableTooltip?.(matchedAnchor as HTMLElement)) {
return matchedAnchor as HTMLElement
if (matchedAnchor && !disableTooltip?.(matchedAnchor)) {
return matchedAnchor
}
} catch {
return null
Expand All @@ -230,7 +228,7 @@ const useTooltipEvents = ({
)
}

handleShowTooltipRef.current = (anchor: HTMLElement | null) => {
handleShowTooltipRef.current = (anchor: Element | null) => {
if (!anchor) {
return
}
Expand Down Expand Up @@ -283,7 +281,7 @@ const useTooltipEvents = ({
// Update debounced callbacks to always delegate to latest handler refs
const debouncedShow = debouncedShowRef.current
const debouncedHide = debouncedHideRef.current
debouncedShow.setCallback((anchor: HTMLElement | null) => handleShowTooltipRef.current(anchor))
debouncedShow.setCallback((anchor: Element | null) => handleShowTooltipRef.current(anchor))
debouncedHide.setCallback(() => handleHideTooltipRef.current())

// --- Effect 1: Delegated anchor events + tooltip hover ---
Expand All @@ -302,9 +300,9 @@ const useTooltipEvents = ({
}

const activeAnchorContainsTarget = (event?: Event): boolean =>
Boolean(event?.target && activeAnchorRef.current?.contains(event.target as HTMLElement))
Boolean(event?.target instanceof Node && activeAnchorRef.current?.contains(event.target))

const debouncedHandleShowTooltip = (anchor: HTMLElement | null) => {
const debouncedHandleShowTooltip = (anchor: Element | null) => {
debouncedHide.cancel()
debouncedShow(anchor)
}
Expand Down Expand Up @@ -333,9 +331,9 @@ const useTooltipEvents = ({
if (!targetAnchor && !activeAnchorContainsTarget(event)) {
return
}
const relatedTarget = (event as MouseEvent).relatedTarget as HTMLElement | null
const relatedTarget = (event as MouseEvent).relatedTarget
const containerAnchor = targetAnchor || activeAnchorRef.current
if (containerAnchor?.contains(relatedTarget)) {
if (relatedTarget instanceof Node && containerAnchor?.contains(relatedTarget)) {
return
}
debouncedHandleHideTooltip()
Expand Down Expand Up @@ -365,9 +363,9 @@ const useTooltipEvents = ({
if (!targetAnchor && !activeAnchorContainsTarget(event)) {
return
}
const relatedTarget = (event as FocusEvent).relatedTarget as HTMLElement | null
const relatedTarget = (event as FocusEvent).relatedTarget
const containerAnchor = targetAnchor || activeAnchorRef.current
if (containerAnchor?.contains(relatedTarget)) {
if (relatedTarget instanceof Node && containerAnchor?.contains(relatedTarget)) {
return
}
debouncedHandleHideTooltip()
Expand Down Expand Up @@ -512,8 +510,8 @@ const useTooltipEvents = ({
if (!showRef.current) {
return
}
const target = (event as MouseEvent).target as HTMLElement
if (!target?.isConnected) {
const target = (event as MouseEvent).target
if (!(target instanceof Node) || !target.isConnected) {
return
}
if (tooltipRef.current?.contains(target)) {
Expand Down
10 changes: 5 additions & 5 deletions src/components/TooltipController/TooltipController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
}: ITooltipController,
ref,
) => {
const [activeAnchor, setActiveAnchor] = useState<HTMLElement | null>(null)
const [activeAnchor, setActiveAnchor] = useState<Element | null>(null)
const [anchorDataAttributes, setAnchorDataAttributes] = useState<
Partial<Record<DataAttribute, string | null>>
>({})
const previousActiveAnchorRef = useRef<HTMLElement | null>(null)
const previousActiveAnchorRef = useRef<Element | null>(null)
const styleInjectionRef = useRef(disableStyleInjection)

const handleSetActiveAnchor = useCallback((anchor: HTMLElement | null) => {
const handleSetActiveAnchor = useCallback((anchor: Element | null) => {
setActiveAnchor((prev) => {
if (!anchor?.isSameNode(prev)) {
previousActiveAnchorRef.current = prev
Expand All @@ -76,7 +76,7 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
}, [])

/* c8 ignore start */
const getDataAttributesFromAnchorElement = (elementReference: HTMLElement) => {
const getDataAttributesFromAnchorElement = (elementReference: Element) => {
const dataAttributes = elementReference?.getAttributeNames().reduce(
(acc, name) => {
if (name.startsWith('data-tooltip-')) {
Expand Down Expand Up @@ -123,7 +123,7 @@ const TooltipController = React.forwardRef<TooltipRefProps, ITooltipController>(
return () => {}
}

const updateAttributes = (element: HTMLElement) => {
const updateAttributes = (element: Element) => {
const attrs = getDataAttributesFromAnchorElement(element)
setAnchorDataAttributes((prev) => {
const keys = Object.keys(attrs) as DataAttribute[]
Expand Down
4 changes: 2 additions & 2 deletions src/components/TooltipController/TooltipControllerTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ITooltipController {
classNameArrow?: string
content?: ReactNode
portalRoot?: Element | null
render?: (render: { content: ReactNode | null; activeAnchor: HTMLElement | null }) => ReactNode
render?: (render: { content: ReactNode | null; activeAnchor: Element | null }) => ReactNode
place?: PlacesType
offset?: number
id?: string
Expand Down Expand Up @@ -70,7 +70,7 @@ export interface ITooltipController {
setIsOpen?: (value: boolean) => void
afterShow?: () => void
afterHide?: () => void
disableTooltip?: (anchorRef: HTMLElement | null) => boolean
disableTooltip?: (anchorRef: Element | null) => boolean
role?: React.AriaRole
}

Expand Down
11 changes: 4 additions & 7 deletions src/components/TooltipController/shared-attribute-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* all active anchors and dispatches changes to registered callbacks.
*/

type AttributeCallback = (element: HTMLElement) => void
type AttributeCallback = (element: Element) => void

const observedElements = new Map<HTMLElement, Set<AttributeCallback>>()
const observedElements = new Map<Element, Set<AttributeCallback>>()

let sharedObserver: MutationObserver | null = null

Expand All @@ -26,7 +26,7 @@ function getObserver(): MutationObserver {
) {
continue
}
const target = mutation.target as HTMLElement
const target = mutation.target as Element
const callbacks = observedElements.get(target)
if (callbacks) {
callbacks.forEach((cb) => cb(target))
Expand All @@ -37,10 +37,7 @@ function getObserver(): MutationObserver {
return sharedObserver
}

export function observeAnchorAttributes(
element: HTMLElement,
callback: AttributeCallback,
): () => void {
export function observeAnchorAttributes(element: Element, callback: AttributeCallback): () => void {
const observer = getObserver()
let callbacks = observedElements.get(element)
if (!callbacks) {
Expand Down
16 changes: 16 additions & 0 deletions src/test/tooltip-anchor-selection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ describe('tooltip anchor selection', () => {
expect(document.getElementById('delegated-hover-test')).toBeInTheDocument()
})

test('opens for delegated hover on an svg anchor', async () => {
render(
<>
<svg data-tooltip-id="svg-anchor-test" aria-label="SVG anchor" width="16" height="16">
<circle cx="8" cy="8" r="8" />
</svg>
<TooltipController id="svg-anchor-test" content="SVG Anchor Test" />
</>,
)

const anchor = screen.getByLabelText('SVG anchor')

hoverAnchor(anchor, 100)
await waitForTooltip('svg-anchor-test')
})
Comment on lines +217 to +231
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify delegated-hover helpers and event types used by this spec.
rg -n -C2 '\bhoverAnchor\b|fireEvent\.mouseEnter|fireEvent\.mouseOver' src/test/tooltip-anchor-selection.spec.js src/test/test-utils.js

Repository: ReactTooltip/react-tooltip

Length of output: 3830


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show context around delegated-hover-test and the SVG-anchor test
sed -n '180,260p' src/test/tooltip-anchor-selection.spec.js

# Show hover helpers for context
sed -n '1,120p' src/test/test-utils.js

Repository: ReactTooltip/react-tooltip

Length of output: 4670


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' src/test/tooltip-anchor-selection.spec.js
sed -n '120,220p' src/test/tooltip-anchor-selection.spec.js

Repository: ReactTooltip/react-tooltip

Length of output: 6394


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "delayShow|delayHide|showDelay|openDelay" src/components/TooltipController* src/components -S || true
rg -n "setTimeout|jest.useFakeTimers|advanceTimersByTime" src/components/TooltipController* src/components -S | head -n 80

Repository: ReactTooltip/react-tooltip

Length of output: 3644


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "delayShow|delayHide|showDelay|openDelay|closeDelay" src/components -S || true
rg -n "setTimeout\(|clearTimeout\(|schedule|timer" src/components -S | head -n 120
ls -R src/components | head -n 80

Repository: ReactTooltip/react-tooltip

Length of output: 4308


🏁 Script executed:

#!/bin/bash
set -euo pipefail
nl -ba src/components/Tooltip/use-tooltip-events.tsx | sed -n '1,220p'
nl -ba src/components/Tooltip/use-tooltip-events.tsx | sed -n '220,360p'

Repository: ReactTooltip/react-tooltip

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Re-run without `nl`
cat -n src/components/Tooltip/use-tooltip-events.tsx | sed -n '1,240p'
cat -n src/components/Tooltip/use-tooltip-events.tsx | sed -n '240,380p'

Repository: ReactTooltip/react-tooltip

Length of output: 14958


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "function debounce|export function debounce|debounce\\(" src/utils -S
ls src/utils

Repository: ReactTooltip/react-tooltip

Length of output: 507


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/utils/debounce.ts

Repository: ReactTooltip/react-tooltip

Length of output: 1875


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/components/Tooltip/use-tooltip-events.tsx | sed -n '380,620p'

Repository: ReactTooltip/react-tooltip

Length of output: 7208


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "'mouseenter'|mouseenter" src/components/Tooltip/use-tooltip-events.tsx

Repository: ReactTooltip/react-tooltip

Length of output: 308


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "mouseenter|mouseleave|mouseover|mouseout" src/components/Tooltip -S

Repository: ReactTooltip/react-tooltip

Length of output: 2304


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "addEventListener\\(['\"]mouseenter['\"]|onMouseEnter|mouseenter\\b" src/components/Tooltip -S
rg -n "mouseEnter\\b" src -S
rg -n "addEventListener\\(['\"]mouseleave['\"]|onMouseLeave|mouseleave\\b" src/components/Tooltip -S
rg -n "event-delegation|addDelegatedEventListener" src/components/Tooltip -S

Repository: ReactTooltip/react-tooltip

Length of output: 3415


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/components/Tooltip/event-delegation.ts

Repository: ReactTooltip/react-tooltip

Length of output: 2774


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "mouseenter|mouseleave" src/components/Tooltip -S
rg -n "addEventListener\\([^\\)]*['\"]mouseenter['\"]|onMouseEnter" src/components/Tooltip -S || true
sed -n '1,240p' src/components/Tooltip/use-tooltip-anchors.tsx 2>/dev/null || true
sed -n '1,260p' src/components/Tooltip/Tooltip.tsx 2>/dev/null || true

Repository: ReactTooltip/react-tooltip

Length of output: 11408


🌐 Web query:

@testing-library/dom fireEvent.mouseEnter dispatches which native events mouseover mouseenter

💡 Result:

When you call fireEvent.mouseEnter in @testing-library/dom, it dispatches both a mouseenter event and a mouseover event [1]. Historically, this behavior was implemented to ensure that handlers added via both React (which often relies on mouseover for its synthetic mouseEnter events) and native addEventListener methods are triggered correctly [1]. Specifically, the library dispatches both event types to ensure compatibility across different event-handling strategies in the DOM [1]. It is important to note that fireEvent is intended for simple, direct DOM event dispatching and does not simulate full, complex user interactions—such as the exact sequence of events a browser would trigger during a mouse movement [2]. For more realistic simulations of user behavior, the library authors recommend using the @testing-library/user-event package [2].

Citations:


Exercise delegated SVG target resolution by hovering the inner <circle>.

hoverAnchor uses fireEvent.mouseEnter, and RTL dispatches mouseover too, so the delegated hover handler may already run. The missing coverage is when the event target is a nested SVG element (the current test targets the <svg> itself). Dispatch mouseOver from the <circle> and keep timer advancement so the tooltip show delay runs.

Suggested change
   test('opens for delegated hover on an svg anchor', async () => {
     render(
       <>
         <svg data-tooltip-id="svg-anchor-test" aria-label="SVG anchor" width="16" height="16">
           <circle cx="8" cy="8" r="8" />
         </svg>
         <TooltipController id="svg-anchor-test" content="SVG Anchor Test" />
       </>,
     )

     const anchor = screen.getByLabelText('SVG anchor')
+    const circle = anchor.querySelector('circle')

-    hoverAnchor(anchor, 100)
+    fireEvent.mouseOver(circle)
+    advanceTimers(100)
     await waitForTooltip('svg-anchor-test')
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/tooltip-anchor-selection.spec.js` around lines 217 - 231, The test
should exercise delegated SVG target resolution by firing the mouseOver from the
inner <circle> element rather than the <svg>; locate the SVG via
screen.getByLabelText('SVG anchor'), grab the nested circle (e.g. via
querySelector('circle')), then call hoverAnchor or directly dispatch mouseOver
on that circle (preserving the 100ms timer advancement) and finally await
waitForTooltip('svg-anchor-test') so the delegated hover handler path is
covered; update the test named "opens for delegated hover on an svg anchor" and
keep TooltipController and the existing delay logic unchanged.


test('does not close on focus transitions inside the same anchor', async () => {
render(
<>
Expand Down
3 changes: 2 additions & 1 deletion src/utils/resolve-data-tooltip-anchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ function resolveDataTooltipAnchor(targetElement: Element, tooltipId: string) {
let currentElement: Element | null = targetElement

while (currentElement) {
if (currentElement instanceof HTMLElement && currentElement.dataset.tooltipId === tooltipId) {
const dataset = (currentElement as Element & { dataset?: DOMStringMap }).dataset
if (dataset?.tooltipId === tooltipId) {
return currentElement
}
currentElement = currentElement.parentElement
Expand Down