Skip to content

[HDX-4405] fix(app): prevent stranded tooltip in virtualised table rows#2380

Open
alex-fedotyev wants to merge 4 commits into
mainfrom
alex/HDX-4405-fix-stranded-tooltip-in-virtual-table
Open

[HDX-4405] fix(app): prevent stranded tooltip in virtualised table rows#2380
alex-fedotyev wants to merge 4 commits into
mainfrom
alex/HDX-4405-fix-stranded-tooltip-in-virtual-table

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

Summary

After #2321 introduced per-row Tooltip.Floating instances inside a TanStack Virtual list, the tooltip would occasionally stay rendered on screen when the mouse moved rapidly away from a row. The root cause is a race condition: when the mouse exits a row quickly, the virtual row can unmount before onMouseLeave fires, leaving the tooltip's Portal-rendered content stuck as opened: true with no element left to dismiss it.

Fix: Hoist the single Tooltip.Floating up to wrap <tbody> instead of each individual <tr>. Because <tbody> never unmounts, the tooltip's state is never orphaned. Row-level onMouseEnter/onMouseLeave handlers update a shared hoveredRowDescription state; the tooltip's disabled prop gates visibility so rows without a resolved URL (error-toast branch) never show a hint. A <tbody>-level onMouseLeave acts as a safety net to clear the description if a rapid mouse move causes a row to unmount before its own leave handler fires.

Screenshots or video

Before After
Tooltip label stays on screen after fast mouse exit Tooltip dismisses correctly on mouse exit

How to test on Vercel preview

Preview routes: /dashboards

Steps:

  1. Open a dashboard that has a Table tile configured with an onClick action (e.g. "Search Traces" or "Open Dashboard").
  2. Hover over a row to confirm the tooltip hint appears.
  3. Move the mouse rapidly off the row (or out of the table entirely).
  4. Verify the tooltip disappears immediately and does not stay rendered on screen.

References

Per-row Tooltip.Floating instances got stranded in their Portal when a
virtual row unmounted before onMouseLeave fired — a race that occurs
when the mouse moves rapidly across a TanStack Virtual list.

Fix: replace one Tooltip.Floating per virtual row with a single shared
Tooltip.Floating wrapping the whole <tbody>. The floating tooltip now
lives on <tbody>, which never unmounts, so its Portal-rendered content
can never be left open after the triggering element disappears.

Row-level onMouseEnter/onMouseLeave handlers update a shared
hoveredRowDescription state; the tooltip's disabled prop gates
visibility so rows without a resolved URL (error-toast branch) never
show a hint. A tbody-level onMouseLeave acts as a safety net to clear
the description if a rapid mouse move causes a row to unmount before its
own leave handler fires.

Test: adds a regression test that verifies the tooltip disappears on
mouseLeave (the stranded-tooltip scenario).
@alex-fedotyev alex-fedotyev added the ai-generated AI-generated content; review carefully before merging. label May 29, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

⚠️ No Changeset found

Latest commit: 24d8552

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 30, 2026 1:07am
hyperdx-storybook Error Error May 30, 2026 1:07am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

E2E Test Results

All tests passed • 192 passed • 3 skipped • 1312s

Status Count
✅ Passed 192
❌ Failed 0
⚠️ Flaky 4
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

Verifies that the Tooltip.Floating hint appears on row hover and
disappears when the mouse moves away, covering the stranded-tooltip
regression introduced by the per-row Tooltip.Floating in PR #2321.

Changes:
- dashboard-table-linking.spec.ts: new 'Tooltip hint appears on hover
  and disappears on mouse-leave' test. Creates a table tile with a
  Search row-click action, hovers the first row to confirm the hint
  appears, then moves the mouse away to confirm it hides cleanly.
- DashboardPage.ts: adds getFirstTableRow() and
  hoverFirstTableRowAndGetTooltip() page-object helpers.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 1
  • Production lines changed: 132 (+ 139 in test files, excluded from tier calculation)
  • Branch: alex/HDX-4405-fix-stranded-tooltip-in-virtual-table
  • Author: alex-fedotyev

To override this classification, remove the review/tier-2 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

Deep Review

✅ No critical issues found.

🟡 P2 -- recommended

  • packages/app/src/__tests__/HDXMultiSeriesTableChart.test.tsx:145 -- The new HDX-4405 unit test asserts tooltip visibility by walking up from the testid span via closest('[style*="display"]') and reading style.display, coupling the assertion to Mantine's portal-wrapper inline-style choice rather than to user-visible behavior.

    • Fix: Replace the inline-style walk with expect(screen.getByTestId('row-action-hint')).not.toBeVisible() (or use Testing Library text queries) so the assertion is decoupled from Mantine's portal internals.
    • kieran-typescript, testing, maintainability
  • packages/app/src/HDXMultiSeriesTableChart.tsx:326 -- The two defensive branches in hoveredRowDescription (!virtualRow and !row) — the load-bearing protection against the original failure mode where a hovered row leaves the virtualizer's items array via scroll re-virtualization or refetch — are exercised by neither the new unit test (which uses a virtualizer mock that renders every row) nor the new e2e (which only moves the cursor outside the table).

    • Fix: Add a unit test that hovers a row, re-renders with a virtualizer mock that omits hoveredVirtualIndex from items, and asserts the hint is no longer visible.
    • testing, correctness, julik-frontend-races, maintainability
🔵 P3 nitpicks (5)
  • packages/app/src/HDXMultiSeriesTableChart.tsx:330 -- The suppression gate moved from rowAction && rowAction.url (any falsy url) to rowAction?.url != null && rowAction.description, so empty-string url now shows the hint and a non-null url paired with empty description now suppresses it; no current producer hits either edge, so there is no concrete failure today, but the implicit RowAction contract is no longer enforced.

    • Fix: Restore truthy semantics with rowAction?.url && rowAction.description, or tighten RowAction into a discriminated union so success vs error variants are type-checked.
    • kieran-typescript, correctness
  • packages/app/src/HDXMultiSeriesTableChart.tsx:323 -- The useMemo lists items as a dependency, but rowVirtualizer.getVirtualItems() returns a fresh array on every render, so the memo invalidates every tick and provides no real caching; the work is cheap but the abstraction misrepresents itself to future maintainers.

    • Fix: Drop the memo (inline the computation) or look the row up via rows[hoveredVirtualIndex] with deps narrowed to [hoveredVirtualIndex, rows, getRowAction].
    • julik-frontend-races, kieran-typescript, maintainability
  • packages/app/tests/e2e/page-objects/DashboardPage.ts:983 -- The new docstring states the helper locates the tooltip via Mantine CSS modules and known hint text patterns ("Open in search", "Search <SourceName>", "Open dashboard"), but the implementation actually queries getByTestId('row-action-hint').

    • Fix: Rewrite the docstring to describe the actual data-testid selection strategy and drop the obsolete text-pattern paragraph.
    • maintainability
  • packages/app/src/HDXMultiSeriesTableChart.tsx:411 -- The <span data-testid="row-action-hint"> label renders even when hoveredRowDescription is null, leaving a permanent test-seam element inside the hidden portal and forcing visibility assertions to climb the DOM to find a meaningful signal.

    • Fix: Render the label conditionally — label={hoveredRowDescription ? <span data-testid="row-action-hint">{hoveredRowDescription}</span> : null} — so queryByTestId directly reflects open/closed state.
    • kieran-typescript, maintainability
  • packages/app/tests/e2e/features/dashboard-table-linking.spec.ts:755 -- The e2e test name and intro comment frame this case as a regression for the row-unmount race, but the body fires a single hover plus page.mouse.move(10, 10), which only exercises the <tbody> onMouseLeave safety net rather than a scenario where a hovered row unmounts during virtualization.

    • Fix: Rename the test to reflect what it actually verifies, or extend it with a scroll-driven case that ejects the hovered row from the virtualizer window.
    • testing

Reviewers (5): correctness, julik-frontend-races, kieran-typescript, testing, maintainability.

Testing gaps:

  • No test re-renders with hoveredVirtualIndex set but its row absent from items[] — the exact path the fix was designed to defend.
  • No test fires mouseLeave on <tbody> while a row is still hovered, so the explicit safety-net cleanup path is unverified.

P2 — correctness:
- Store hoveredVirtualIndex (row index) instead of hoveredRowDescription
  (string) so the label re-derives via useMemo on every render. If the
  virtualiser drops or replaces the hovered row (scroll, auto-refetch,
  rapid cursor movement) the new row's action is shown immediately;
  stale text from the unmounted row can never persist.
- Rows with url:null or empty description now correctly disable the
  tooltip regardless of what the prior hover state was.

P2 — testing:
- Replaced the simple mouseLeave regression test with one that exercises
  the actual race: hover index 0 (URL row), then enter index 1 (no-URL
  row) without firing mouseLeave on index 0. Asserts tooltip hides by
  inspecting the Mantine inline display style on the Portal container.

P3 — maintainability:
- label={hoveredRowDescription} — drop the ?? '' fallback that obscured
  the disabled gating relationship (Mantine accepts null as ReactNode).
- disabled={!hoveredRowDescription} — guards against empty-string
  descriptions that would mount a zero-width floating tooltip.
- Unconditional onMouseEnter/onMouseLeave on each <tr> with a hoisted
  clearHovered useCallback, replacing the conditional handler pattern
  that forked JSX unnecessarily.
- Collapse dual comment blocks into one rationale at the Tooltip.Floating
  call site; the state declaration now has a single-line pointer.
- Add data-testid="row-action-hint" to the Tooltip.Floating label span
  so E2E tests locate the tooltip by stable testid rather than by
  hard-coupled copy strings.
Copy link
Copy Markdown
Contributor

@MikeShi42 MikeShi42 left a comment

Choose a reason for hiding this comment

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

fix makes sense - though i think it's a bit "aggressive" that the tooltip follows the cursor so closely, would it make more sense to have the action on the right side of the table?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated AI-generated content; review carefully before merging. review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants