Skip to content

Component | Tooltip: Re-land #859's fade-in/follow-cursor fix without breaking Crosshair - #923

Open
Vincentdevreede wants to merge 5 commits into
f5:mainfrom
Vincentdevreede:bugfix/tooltip-relanding-859
Open

Vincentdevreede wants to merge 5 commits into
f5:mainfrom
Vincentdevreede:bugfix/tooltip-relanding-859

Conversation

@Vincentdevreede

Copy link
Copy Markdown
Contributor

Related to

#859
#898
#902

What happened

#859 fixed two real Tooltip bugs: the fade-in animation not having a frame to start from, and the tooltip freezing mid-fade when the mouse crossed a gap between triggers (e.g. an axis, between bars). It was reverted in #902 because it also broke Crosshairs: the pinned tooltip disappeared or keep rerendering the instant the mouse moved anywhere on the page.

The cause: #859 attached its new delegated mousemove/mouseleave listeners to the tooltip's container, which is commonly document.body (Crosshair's default). A container-level listener therefore caught mouse movement anywhere on the page, and its "no trigger matched" fallback kept hiding and re-placing Crosshair's imperatively-controlled tooltip.

What this PR does

Re-lands #859, #898's two commits by @rokotyan and fixed the remaining gap:

  • Scope delegated listeners to the components' owning <svg> not the container. Still catches the "gap between triggers", but page-wide mouse movement never reaches these handlers regardless of what the tooltip's container is set to.
  • A component's own listener only calls stopPropagation() on an actual trigger match, so a chart with no triggers configured at all (exactly the Crosshair-driven case) was running the "no match" fallback twice per mousemove. Once via the component, once via the bubbled-up <svg> listener.
  • Release _isControlledExternally only when the tooltip actually hides (inside _hide(), not hide()) fixing a race. hide() cleared the flag synchronously even though the real hide can be deferred by hideDelay, letting a delegated handler interfere during that grace period.

Vincentdevreede and others added 5 commits September 15, 2026 14:14
…ver trigger gaps

`_display()` removed the `hidden` class and added the `show` class back-to-back in the same tick. With no frame painted in between, the browser could coalesce both class changes into a single style recalc, skipping the "before" state the opacity transition needs to animate from, the tooltip would just pop to full opacity instead of fading in. Forcing a reflow (`getBoundingClientRect()`) between the two class changes guarantees that intermediate frame exists.

With `followCursor` enabled, moving the cursor from one trigger into a gap between triggers (e.g. between bars, covered by an axis or another sibling component's element) never fired a mousemove on any configured trigger, since each one only listens on its own element. The tooltip would freeze at its last position instead of tracking the cursor through the fade-out.
…s from delegated handlers

The delegated mousemove handler hides the tooltip whenever no trigger
matches, and since f5#859 it is also attached to the tooltip container.
When the container is document.body, any mouse movement on the page
hides a tooltip that Crosshair pinned via `forceShowAt`, and the
follow-cursor fallback drags it to the pointer.

Track ownership instead: `show()` marks the tooltip as externally
controlled and `hide()` releases it, so the delegated handlers leave
imperatively shown tooltips alone.

Co-Authored-By: Claude Fable 5 <[email protected]>
…ointer position

With `forceShowAt` set, `shouldShow` still required the pointer to be
within the Y range, so moving the mouse over the axis area (inside the
SVG but outside the plot) hid the pinned crosshair and tooltip, and
leaving the SVG re-showed them — visible as flickering. A pinned
crosshair's visibility should only depend on the forced position being
within the X range.

Co-Authored-By: Claude Fable 5 <[email protected]>
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