[HDX-4405] fix(app): prevent stranded tooltip in virtualised table rows#2380
[HDX-4405] fix(app): prevent stranded tooltip in virtualised table rows#2380alex-fedotyev wants to merge 6 commits into
Conversation
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).
🦋 Changeset detectedLatest commit: d5d06c7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
E2E Test Results✅ All tests passed • 197 passed • 3 skipped • 1216s
Tests ran across 4 shards in parallel. |
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.
🔵 Tier 2 — Low RiskSmall, isolated change with no API route or data model modifications. Why this tier:
Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns. Stats
|
Deep Review✅ No critical issues found. This is a self-contained UI fix: the cursor-following 🟡 P2 -- recommended
🔵 P3 nitpicks (4)
Reviewers (9): correctness, testing, maintainability, project-standards, kieran-typescript, julik-frontend-races, adversarial, agent-native, learnings-researcher. Testing gaps:
|
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.
MikeShi42
left a comment
There was a problem hiding this comment.
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?
Replace the tbody-level Tooltip.Floating that tracks the cursor
with a hover-revealed trailing chevron icon anchored to the row's
last cell, wrapped in an anchored Mantine Tooltip. Mirrors the
.rowButtons pattern used by Search and Patterns.
Eliminates the stranded-tooltip bug class by construction: the
anchored Tooltip's open state is tied to the icon's lifecycle, so
a virtual row unmounting mid-hover can no longer leave a popup
behind. Addresses Mike's design feedback that the cursor-following
tooltip felt "a bit aggressive".
Drop hoveredVirtualIndex / hoveredRowDescription state, the
tbody-level Tooltip.Floating, the tbody onMouseLeave safety net,
and per-row onMouseEnter / onMouseLeave handlers. Add a per-row
trailing IconChevronRight in the last <td>, wrapped in a Next.js
Link with prefetch=false, tabIndex=-1, and aria-hidden so cmd /
middle / right-click semantics carry over but sequential focus
and screen-reader traversal aren't double-counted.
New HDXMultiSeriesTableChart.module.scss owns .tableRow,
.lastCell, .rowActionHint. The icon is opacity 0 by default and
fades in on .tableRow:hover; the last <td> gets position: relative
so the icon's absolute positioning anchors to the cell, not the
row (which is not a reliable positioning context across browsers).
Tooltip is anchored position=left with the established
DBRowTableIconButton conventions (openDelay 300, closeDelay 100,
withArrow, fz xs).
Suppressed on failure rows (rowAction.url null): the description
("Open in search", "Open dashboard X") would promise a destination
the click cannot deliver.
Tests:
- Unit tests rewritten: trailing chevron rendering, dashboard
variant tooltip, failure-row chevron absent, legacy
getRowSearchLink path chevron absent. Dropped the inline-style
closest() walk flagged in the deep review.
- E2E spec replaces the stranded-tooltip dismiss test with
opacity-gated visibility, anchored tooltip text on hover, and
click-on-chevron navigation.
- DashboardPage hint helper hovers the row to reveal the icon,
then hovers the icon to open the anchored Tooltip.
|
Pivoted the PR per Mike's review feedback. The cursor-following Tooltip.Floating is gone; the hint is now a hover-revealed trailing chevron in the last cell, with an anchored Mantine Tooltip describing the destination on hover. Pushed as Walking through Mike's concern: instead of patching the original surface I replaced it with one that doesn't have the same failure mode. The anchored Tooltip's open state is tied to the icon's lifecycle, so a virtual row unmounting mid-hover can't leave a popup behind by construction. Notes on the design choices, in case they help review:
PR-tier prediction: Tier 2 (132 production lines changed, single layer, no critical-path files). The previous commit on this branch ( Always-visible chevron column (Option A from the chat) is a richer surface and is tracked as a followup. It would add a permanent 32 px column on the right edge of the table, plus action-type-specific icons ( Validation locally:
|
|
@MikeShi42 - what do you think of this? I changed it to use less aggressive UX, while using |
|
@elizabetdev - what do you think about the |
|
@alex-fedotyev I think all the actionable rows should have a hover state And we already using the chevron right for the following (so I think we shouldn't be reuisng the same icon to indicate a different thing:
The solutionSo the solution would be, on hover |
Address elizabetdev review feedback on PR #2380: - Hover state on actionable rows: switched the trailing-icon row from the global `bg-muted-hover` utility (`--color-bg-muted`) to a module-scoped `.actionableRow` class that hovers to `--color-bg-highlighted`. Non-actionable rows (failure or no action configured) keep `bg-muted-hover` as-is, so the visual delta between the two reinforces interactivity before the user sees the icon fade in. - Icon swap: `IconChevronRight` -> `IconArrowUpRight`. The chevron-right is already used elsewhere (sidebar group collapse affordances among others) for an unrelated interaction, so reusing it here was a collision. Arrow-up-right reads as "navigate elsewhere" without that collision. Tests: - Unit: two new positive / negative tests assert the row gains `actionableRow` on a resolved URL and falls back to `bg-muted-hover` otherwise. Existing trailing-icon tests rename to "arrow" wording; the `data-testid="row-action-hint"` selector is unchanged so test infra carries over. - E2E: step labels in `dashboard-table-linking.spec.ts` renamed to reference the arrow icon; selector is unchanged. - Changeset filename renamed `hdx-4405-trailing-chevron-hint.md` -> `hdx-4405-trailing-action-hint.md` and the patch note updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Both fixed in d5d06c7. Icon: swapped Hover state: new Tests:
Vercel preview should update with the new icon + hover behavior shortly. |
|
@elizabetdev - is this how you imagined it? |
…2386) ## Summary This PR adds conditional color rules to number tiles. Define an ordered list of conditions; the last matching rule's color wins, so list rules in ascending priority order with the highest-priority rule last. If no rule matches, the tile falls back to its static color (set previously in #2265), then to the default text color. The schema is intentionally generic at the shared level so a future table-tile slice can attach per-column rules without a schema change. > **Stacking note:** the branch was cut from `alex/HDX-4405-fix-stranded-tooltip-in-virtual-table` (PR #2380), so the file list includes 4 unrelated files from that PR: `HDXMultiSeriesTableChart.tsx`, `HDXMultiSeriesTableChart.test.tsx`, `dashboard-table-linking.spec.ts`, `DashboardPage.ts`. Those files belong to #2380 and will drop out of this diff once #2380 merges. The HDX-4406 review surface is the other 10 files. ### What changed **common-utils (schema)** - Added `ColorConditionSchema` (discriminated union of `gt`, `gte`, `lt`, `lte`, `between`, `eq`, `neq`, `contains`, `startsWith`, `endsWith`, `regex` operators) - Added `colorRules` (optional, max 10) to `SharedChartSettingsSchema` alongside the existing `color` field **app (resolver)** - `evaluateColorCondition`: evaluates a single rule against a runtime value with proper type guards (numeric ops reject strings, string ops reject numbers, bad regex returns false) - `resolveConditionalColor`: iterates rules in order, last match wins, falls back to `fallback` (the static color) when nothing matches - String data values (ClickHouse returns `UInt64` counts as JSON strings) are coerced to numbers before rule evaluation **app (UI)** - New `ColorRulesEditor` component: sortable rule list via `@dnd-kit/sortable`, per-operator value inputs (single number, two-number range for `between`, text-or-number for `eq`/`neq`), `ColorSwatchInput` per rule, add/delete buttons; "Add rule" disables at 10 - `ChartDisplaySettingsDrawer`: added "Conditional colors" section gated on `DisplayType.Number`, placed below the existing static color picker - `EditTimeChartForm`: `colorRules` wired through `useWatch`, `displaySettings` memo, and `handleUpdateDisplaySettings` - `DBNumberChart`: resolves color via `resolveConditionalColor(rawValue, config.colorRules, config.color)` at render time **Tests** - Schema: positive + negative cases for all operators and array length constraints - Resolver: `evaluateColorCondition` per-operator, `resolveConditionalColor` including last-match-wins, string coercion, and the canonical success/warning/error scenario - UI: add/delete/operator shape/color swatch/render cases for `ColorRulesEditor` - Integration: `DBNumberChart` with `color: 'chart-success'` + two rules confirms value 50 -> success, 200 -> warning, 1000 -> error; also covers string UInt64 input No changes to `packages/api` schemas or external API (separate follow-up ticket, mirrors HDX-4378). ### Screenshots or video | Before | After | | :----- | :---- | | Number tile has only a static color picker | Number tile shows a "Conditional colors" section with add/reorder/delete rule controls below the static color picker | ### How to test on Vercel preview **Preview routes:** /dashboards **Steps:** 1. Open or create a dashboard and add a number tile (or edit an existing one). 2. Click the "Display Settings" button to open the drawer. 3. Confirm the "Conditional colors" section appears below the "Color" picker. 4. Click "Add rule" and verify a rule row appears with operator >, a value input, a color swatch, and a delete button. 5. Change the operator to >= and set value to 100; pick the Warning color swatch. 6. Click "Add rule" again; set operator >=, value 500; pick the Error color swatch. 7. Click "Apply". Verify the tile persists the rules (re-open Display Settings and confirm the two rules are still there). 8. With a tile value below 100, confirm it renders in the static (or default) color. 9. With a value between 100 and 499, confirm warning color. 10. With a value >= 500, confirm error color. 11. Click "Add rule" 8 more times to reach 10 rules total. Verify the button becomes disabled. 12. Drag a rule handle to reorder and click Apply; confirm the new order is reflected on re-open. ### References - Linear Issue: https://linear.app/clickhouse/issue/HDX-4406 - Related PRs: #2265 (static number-tile color picker, precedent); #2380 (tooltip fix; this branch is stacked on it)




Summary
Pivot of the original HDX-4405 fix per review feedback.
The first round of this PR fixed the stranded-tooltip bug by hoisting
Tooltip.Floatingto the<tbody>. That dismissed the bug but the resulting surface still felt off: the tooltip followed the cursor too closely. I've replaced it with a hover-revealed trailing chevron icon anchored to the row's last cell, wrapped in an anchored MantineTooltip. Mirrors the.rowButtonspattern from Search and Patterns.This eliminates the stranded-tooltip bug class by construction: the anchored Tooltip's open state is tied to the icon's lifecycle, so a virtual row unmounting mid-hover can no longer leave a popup behind.
What changed (vs the previous commit on this branch)
<tbody>-levelTooltip.Floating, thehoveredVirtualIndex/hoveredRowDescriptionstate, the<tbody>onMouseLeavesafety net, and per-rowonMouseEnter/onMouseLeavehandlers.IconChevronRightin the last<td>of clickable rows, wrapped in a Next.js<Link href={rowAction.url} prefetch={false} tabIndex={-1} aria-hidden="true">. The Link reuses the row's existing URL so cmd-click / middle-click / right-click "Open in New Tab" / status-bar URL preview all work on the icon.tabIndex={-1}andaria-hidden="true"keep sequential focus order and screen-reader traversal anchored to the per-cell Links so the icon doesn't double-count.HDXMultiSeriesTableChart.module.scssowns.tableRow,.lastCell,.rowActionHint. The icon isopacity: 0by default and fades in on.tableRow:hovervia a 0.15s transition..lastCelladdsposition: relativeto the last<td>only (the<tr>does not reliably form a positioning context across browsers).position="left",withArrow,openDelay={300},closeDelay={100},fz="xs"to match the establishedDBRowTableIconButtonconvention.rowAction.url === null) so the icon never promises a destination the click cannot deliver.Tests
HDXMultiSeriesTableChart.test.tsxrewritten: trailing-chevron rendering on success rows, dashboard-variant tooltip text, no chevron on failure rows, no chevron on the legacygetRowSearchLinkpath, no chevron when no action is configured. Dropped the inline-styleclosest('[style*="display"]')walk flagged in the previous deep review.Trailing chevron hint appears on row hover...now asserts opacity-gated visibility, tooltip text on row hover, and click-on-chevron navigation. Replaces the prior stranded-tooltip dismiss test.DashboardPage.tshint helper hovers the row to reveal the icon, then hovers the icon to open the anchored Tooltip. Docstring rewritten.How to test on Vercel preview
Preview route:
/dashboardsonClickaction (Search or Dashboard).Search HyperDX LogsorOpen dashboard "API Latency").onClickwhose template references an unknown column. Hover the resulting row. The chevron is not rendered.References
24d8552c) which fixed the stranding bug via tbody-Tooltip.Floating.