Skip to content

fix: keep clickable tooltips open while pointer moves to body#1269

Merged
danielbarion merged 1 commit intoReactTooltip:masterfrom
janpaepke:fix/clickable-mouseover-listener-not-attached
May 7, 2026
Merged

fix: keep clickable tooltips open while pointer moves to body#1269
danielbarion merged 1 commit intoReactTooltip:masterfrom
janpaepke:fix/clickable-mouseover-listener-not-attached

Conversation

@janpaepke
Copy link
Copy Markdown
Contributor

@janpaepke janpaepke commented May 6, 2026

Summary

This fixes a regression bug, which appeared in v6.0.0

Solution: re-introduce rendered dependency on the effect that attaches the tooltip-body mouseover/mouseout listeners. Without it, clickable tooltips close as soon as the pointer leaves the anchor, making any interactive content inside the tooltip unreachable.

I have tested this solution with a local package using npm link and this change fixes the issue.

Reproduction

Live sandbox: https://codesandbox.io/p/devbox/happy-tharp-g39qff

A clickable tooltip with content the user is supposed to interact with (e.g. an upgrade button):

<TooltipController id="test-id" clickable>
  <button>Test</button>
</TooltipController>
<span data-tooltip-id="test-id">Test me</span>

Hover the anchor -> tooltip opens. Move the pointer toward the tooltip -> tooltip closes the moment the pointer leaves the anchor, before the user can reach the button.

Root cause

The effect at use-tooltip-events.tsx attaches two listeners directly to the tooltip wrapper:

tooltipElement?.addEventListener('mouseover', handleMouseOverTooltip)  // sets hoveringTooltip = true
tooltipElement?.addEventListener('mouseout',  handleMouseOutTooltip)   // sets hoveringTooltip = false

Those listeners are how the 100ms hide grace period (the one started when the pointer leaves the anchor) knows to suppress closing while the pointer is on the tooltip body.

The effect's deps array was [actualOpenEvents, actualCloseEvents, float, clickable]. None of those change between the initial mount and the moment the tooltip first becomes rendered. So the effect runs once, on mount, when tooltipRef.current is still null — the tooltip wrapper hasn't been committed to the DOM yet (rendered is false, tooltipNode is null). The optional chaining on tooltipElement?.addEventListener(...) silently no-ops, the listeners are never attached, and the effect never re-runs.

hoveringTooltip.current therefore stays false forever. The 100ms grace period always expires unsuppressed → tooltip closes the moment you leave the anchor.

Fix

Add rendered to the deps array so the effect re-runs after setRendered(true) commits, when tooltipRef.current is now a real element. Listeners get attached. hoveringTooltip flips correctly.

Test

There was already a test for this exact scenario — keeps a clickable tooltip open while the pointer moves into it in tooltip-interaction-behavior.spec.js. It was passing on broken code because of two compounding weaknesses, both of which had to be fixed for the test to be meaningful:

  1. No timer advancement. The assertion fired right after unhoverAnchor, before the 100ms hide grace period could elapse. Whatever the listeners would or wouldn't have done, the timer hadn't fired yet — the test was just observing "tooltip exists immediately after starting close" which is true unconditionally. Now advanceTimers(150) past the hide-delay so the close path actually runs.
  2. Weak assertion. Even after advancing time, expect(...).toBeInTheDocument() would still pass for a tooltip in the closing-but-not-yet-removed state (CSS transition is in progress, unmount happens via onTransitionEnd). To distinguish "still active" from "actively closing" the assertion needs to check for the react-tooltip__show class — switched to that, matching the convention in tooltip-props.spec.js and tooltip-close-and-delay-behavior.spec.js.

Verified that the strengthened test:

  • fails on broken code with Expected the element to have class react-tooltip__show
  • passes with the fix
  • all other 169 tests still pass
  • ESLint passes

Regression history analysis

The dep was inadvertently dropped in db420de9 (Mar 2024, "chore: add hook rules to eslint and refactor relevant code"). That commit added react-hooks/exhaustive-deps and mechanically reorganized the deps array to satisfy it. ESLint can't see that rendered isn't a value used by the effect — it's a deliberate trigger to re-run when tooltipRef.current flips from null to a real element. The pre-existing comment that explained this exact intent...

rendered is also a dependency to ensure anchor observers are re-registered since tooltipRef becomes stale after removing/adding the tooltip to the DOM

...was kept around the now-orphaned location for over a year, then finally cleaned up in a24f389d ("split events and anchors on it's own file") when the file was extracted. By v6 both the comment and the dep were gone.

The other two deps removed in db420de (updateTooltipPosition, hasClickEvent) stay removed — both have compensating refactors that make them redundant:

  • updateTooltipPosition is now consumed via a ref-stable wrapper (updateTooltipPositionRef.current() in Effect 2), so capturing the latest callback doesn't require a re-run.
  • hasClickEvent propagates transitively through actualGlobalCloseEvents's useMemo deps.

rendered was the only dropped dep without compensation.

To prevent this from happening again on the next ESLint cleanup, this PR also adds an inline comment on the deps array explaining why rendered is there, alongside the existing // eslint-disable-next-line react-hooks/exhaustive-deps.

Summary by CodeRabbit

  • Bug Fixes

    • Improved tooltip rendering state management to ensure tooltips properly respond to rendered state changes and pointer interactions.
  • Tests

    • Enhanced tooltip interaction tests for more precise validation of display behavior.

Restore the missing `rendered` dep so tooltip-body mouseover/mouseout
listeners attach after the tooltip mounts.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9d0e798-5fc2-4d0d-8eed-f3f0f251ad26

📥 Commits

Reviewing files that changed from the base of the PR and between c519e94 and 65f0451.

📒 Files selected for processing (3)
  • src/components/Tooltip/Tooltip.tsx
  • src/components/Tooltip/use-tooltip-events.tsx
  • src/test/tooltip-interaction-behavior.spec.js

📝 Walkthrough

Walkthrough

The PR adds a rendered boolean property to the useTooltipEvents hook so it responds to tooltip render state changes. The hook's dependency array is updated accordingly, the Tooltip component passes the rendered state through, and a test is enhanced to more precisely verify tooltip visibility after mouse interactions.

Changes

Tooltip Render State Tracking

Layer / File(s) Summary
Hook Interface
src/components/Tooltip/use-tooltip-events.tsx
Adds rendered: boolean parameter to hook signature and props type definition.
Hook Effect Dependencies
src/components/Tooltip/use-tooltip-events.tsx
Dependency array updated to include rendered, ensuring the hook reinitializes when render state changes.
Component Wiring
src/components/Tooltip/Tooltip.tsx
Tooltip component passes rendered state into the useTooltipEvents hook call.
Test Validation
src/test/tooltip-interaction-behavior.spec.js
Test enhanced to advance timers, flush microtasks, and assert tooltip element presence with the react-tooltip__show class after mouse interaction.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

Bug

Suggested reviewers

  • gabrieljablonski
  • danielbarion

A tooltip now knows when it appears,
Its rendered state held crystal clear,
The hook responds with watchful care,
No flicker lost in thin air! ✨🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: restoring the rendered dependency to fix clickable tooltips closing prematurely when pointer moves away.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@danielbarion danielbarion self-requested a review May 6, 2026 14:38
Copy link
Copy Markdown
Member

@danielbarion danielbarion left a comment

Choose a reason for hiding this comment

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

Looks good to me. Thanks!

@danielbarion danielbarion merged commit 4225197 into ReactTooltip:master May 7, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants