Skip to content

fix: support anchor elements with dataset capability instead of only HTMLElement#1280

Merged
danielbarion merged 1 commit into
masterfrom
fix/anchor-elements-type
Jun 4, 2026
Merged

fix: support anchor elements with dataset capability instead of only HTMLElement#1280
danielbarion merged 1 commit into
masterfrom
fix/anchor-elements-type

Conversation

@danielbarion
Copy link
Copy Markdown
Member

@danielbarion danielbarion commented Jun 4, 2026

Fix #1279

Summary by CodeRabbit

  • New Features

    • Tooltips can now be triggered from SVG elements and other non-HTML DOM elements.
  • Tests

    • Added test coverage for tooltip functionality on SVG anchors.

@danielbarion danielbarion self-assigned this Jun 4, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR widens tooltip anchor types from HTMLElement to the broader Element type throughout the codebase, enabling tooltips to work with SVG, MathML, and other non-HTMLElement anchors. Type signatures, anchor discovery, event handling, and validation are updated consistently to support delegated hover on SVG elements.

Changes

SVG/non-HTMLElement Anchor Support

Layer / File(s) Summary
Type contract updates for anchors
src/components/Tooltip/TooltipTypes.d.ts, src/components/TooltipController/TooltipControllerTypes.d.ts
TooltipRefProps and ITooltipController type signatures change from HTMLElement | null to Element | null for activeAnchor, setActiveAnchor, and disableTooltip callback parameters.
Anchor discovery and resolution
src/components/Tooltip/anchor-registry.ts, src/utils/resolve-data-tooltip-anchor.ts
Anchor registry and DOM traversal now query and match anchors as generic Element instead of HTMLElement, and mutation processing casts targets to Element when reading data-tooltip-id.
Hook implementations for Element-typed anchors
src/components/Tooltip/use-tooltip-anchors.tsx, src/components/Tooltip/use-tooltip-events.tsx
useTooltipAnchors and useTooltipEvents hooks propagate Element | null anchors through state, subscriptions, and handler callbacks.
Event containment checks with Node guards
src/components/Tooltip/use-tooltip-events.tsx
Event handlers now guard with target instanceof Node and isConnected checks before DOM containment operations, replacing HTMLElement-based casting for hover close, focus close, and click-outside logic.
Component and observer integration
src/components/TooltipController/TooltipController.tsx, src/components/TooltipController/shared-attribute-observer.ts
TooltipController state and shared-attribute-observer callbacks updated to accept Element for anchor types and MutationObserver target processing.
Imperative API and aria helpers
src/components/Tooltip/Tooltip.tsx
getAriaDescribedBy helper and imperative open handler updated to operate on Element | null anchors from document.querySelector.
SVG anchor test coverage
src/test/tooltip-anchor-selection.spec.js
New test verifies tooltip opens on delegated hover over an SVG element with data-tooltip-id.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ReactTooltip/react-tooltip#1278: Both PRs address delegated anchor resolution by modifying use-tooltip-events.tsx and resolve-data-tooltip-anchor.ts to support non-HTMLElement targets as part of the event handling path.
  • ReactTooltip/react-tooltip#1275: Both PRs adjust event handling in use-tooltip-events.tsx, with this PR adding Node instanceof guards for containment checks and the referenced PR changing delegated click listener registration.
  • ReactTooltip/react-tooltip#1211: Both PRs update the disableTooltip callback signature and anchor reference types across the Tooltip/TooltipController API surface.

Suggested reviewers

  • gabrieljablonski

Poem

🐰 A rabbit hops through DOM with glee,
SVGs no longer hide their key—
From HTMLElement's narrow cage,
Element now rules the stage,
MathML too shall have its day,
When tooltips find the broader way! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: supporting anchor elements with dataset capability (SVGElement, MathMLElement) instead of restricting to HTMLElement only.
Linked Issues check ✅ Passed All coding requirements from issue #1279 are met: the PR broadens type handling from HTMLElement to Element across all relevant files, updates resolveDataTooltipAnchor to match elements by dataset capability, adds SVG anchor test case, and preserves the #1277 guard against non-Element nodes.
Out of Scope Changes check ✅ Passed All changes are directly related to the objective of supporting non-HTMLElement anchors (SVG/MathML) that have dataset capability; no extraneous modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/anchor-elements-type

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/components/Tooltip/Tooltip.tsx

Oops! Something went wrong! :(

ESLint: 10.2.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1780584077822" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

src/components/Tooltip/TooltipTypes.d.ts

Oops! Something went wrong! :(

ESLint: 10.2.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1780584077822" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

src/components/Tooltip/anchor-registry.ts

Oops! Something went wrong! :(

ESLint: 10.2.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1780584077822" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

  • 7 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

Beta version released with the last commit 🚀

yarn add react-tooltip@6.0.6-beta.1280.rc.0

or

npm install react-tooltip@6.0.6-beta.1280.rc.0

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/test/tooltip-anchor-selection.spec.js`:
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1dd53806-ac07-4b49-88a8-36af1ccf8075

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc9054 and 53ba78a.

📒 Files selected for processing (10)
  • src/components/Tooltip/Tooltip.tsx
  • src/components/Tooltip/TooltipTypes.d.ts
  • src/components/Tooltip/anchor-registry.ts
  • src/components/Tooltip/use-tooltip-anchors.tsx
  • src/components/Tooltip/use-tooltip-events.tsx
  • src/components/TooltipController/TooltipController.tsx
  • src/components/TooltipController/TooltipControllerTypes.d.ts
  • src/components/TooltipController/shared-attribute-observer.ts
  • src/test/tooltip-anchor-selection.spec.js
  • src/utils/resolve-data-tooltip-anchor.ts

Comment on lines +217 to +231
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')
})
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.

@danielbarion danielbarion merged commit 2dafd9f into master Jun 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant