Skip to content

fix(SplitTooltip): keep the main pane from collapsing on layout changes - #963

Open
korvin89 wants to merge 5 commits into
mainfrom
t-20261106-split-tooltip-layout-race-remeasure-on-layout-change
Open

korvin89 wants to merge 5 commits into
mainfrom
t-20261106-split-tooltip-layout-race-remeasure-on-layout-change

Conversation

@korvin89

@korvin89 korvin89 commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

Fixes the gravity-charts split tooltip recreating the chart instead of reflowing it when the viewport orientation changes, and the resulting flake of the visual test «preserves tooltip content height and reflows the existing chart across orientation changes» (from #951).

Why

The layout prop flips from the window.resize event (or a parent update), while two inputs of SplitTooltipContent are still stale for that render:

  • the container size, which the container ResizeObserver delivers later in the same frame;
  • the tooltip height, which is measured in a layout effect after commit (and in the previous pane width).

Sizing from stale inputs could give a zero main pane (e.g. 300 − 24 − 400 or 500 − 24 − 600). onMainPaneSizeChange(0) requested a reflow(), and @gravity-ui/charts Chart, on its forced measurement, saw a zero host and unmounted ChartInner, so .gcharts-chart was created anew.

How

  • SplitTooltip applies a new layout prop in a layout effect together with a fresh container measurement, so the content never renders a new layout with the dimensions of the previous one. This covers both the window-resize path and a parent changing the size and the layout in one update. The container is measured once more on the next frame: ResizeObserver reports changes against the size it delivered last, so a size that reverts within the same frame would otherwise never be reported (seen as a CI-only failure of «preserves the mounted main pane…»).
  • SplitTooltipContent keeps the tooltip measurement together with the layout and container width it was taken in (tagged after commit). Until the tooltip is re-measured in the current horizontal layout, the main pane is sized as if the tooltip were empty (containerHeight − 24, never zero) and onMainPaneSizeChange is held back. This also covers a reflow that was already pending before the layout change. enabled={false} and the vertical layout do not depend on the tooltip height and are not gated.

Behavior note: on mount in horizontal layout onMainPaneSizeChange is now called once with the measured size instead of twice (containerHeight − 24 first), which is one reflow() less for withSplitPane.

Small choices made alone: when the container ResizeObserver fires before the resize event (seen in the test environment), one notification with the previous layout and the new container size can still happen; it matches the DOM and is non-zero, so it is accepted and documented in the test. The test defines a local viewport-layout hook instead of importing it from the gravity-charts plugin.

Testing

  • npm run typecheck, npx eslint src --quiet, npm test (138 unit) — green, run before every push.
  • New visual tests in SplitTooltip behavior, both with a wrapping tooltip (12 × 100×100 blocks, 600px tall in a 240px pane, 400px in a 320px pane):
    • «never reports a stale main pane size when the viewport orientation changes»: viewport 600×300 → 320×500 → 600×300. Red on main ([0, 'horizontal']).
    • «never reports a stale main pane size when a parent changes the container and the layout together»: the same sizes through props in one rerender.
      Both assert no zero size and that the new layout is only ever reported with its final size; green 5/5 locally.
  • src/plugins/gravity-charts/__tests__/SplitTooltip.visual.test.tsx 10/10 locally (was failing ~2 of 3 runs before).
  • Full npm run test:visual green locally except the known Indicator «default state» flake, which passes on rerun. SplitTooltip visual tests in the Playwright Linux container (scripts/playwright-docker.sh) 6/6 after the next-frame re-measure (1 of 2 before).
  • Manual step: npm run test:visual on Linux/Docker (npm run test:docker) to confirm the new tests need no screenshot baseline (they have none) and the gravity-charts test is stable there.

Plan

One unit: remeasure-on-layout-change (this PR). Change log: v1 container re-measure only → v2 added the tooltip-height gate and a regression test → v3 gate replaced by a provisional non-zero size, enabled=false bypass, deterministic test content → v4 (review): render-phase container read replaced by applying the layout in a layout effect with a fresh measurement; tooltip measurement tag updated after commit; parent-update test added.

When the layout flips from a window resize event, the container size from
ResizeObserver and the tooltip height measured in the previous pane are
still stale for one render. Sizing from them could report a zero main pane,
which made the chart host unmount and recreate the chart instead of
reflowing it.

Re-measure the container synchronously when the controlled layout changes,
and until the tooltip is measured in the new layout size the main pane as
if the tooltip were empty without notifying consumers.
…kip the measurement gate in vertical layout

The vertical layout does not depend on the tooltip height, so a width
change there no longer renders the content twice. The resize callback
reads the layout and width from a ref, so the ResizeObserver is not
recreated on every container resize. Both dimension writers share one
updater that bails out on equal sizes. The component test no longer
imports the viewport layout hook from the gravity-charts plugin.
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 26.6% 448 / 1684
🔵 Statements 26.68% 464 / 1739
🔵 Functions 20.21% 74 / 366
🔵 Branches 20.23% 263 / 1300
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/SplitTooltip/SplitTooltip.tsx 16.66% 17.97% 17.64% 17.04% 76-81, 155-341
Generated in workflow #378 for commit 7a20d1e by the Vitest Coverage Report Action

@gravity-ui-bot

Copy link
Copy Markdown
Contributor

🎭 Tests Report is ready.
🎭 Coverage Report is ready.

@gravity-ui-bot

Copy link
Copy Markdown
Contributor

Preview is ready.

Addresses: 4494056490
Addresses: 4494064643
Addresses: 4494069336
@korvin89
korvin89 marked this pull request as ready for review November 6, 2026 15:30
@korvin89
korvin89 requested a review from kuzmadom as a code owner November 6, 2026 15:30
onMainPaneSizeChangeRef.current = onMainPaneSizeChange;

const tooltipPaneRef = React.useRef({layout, containerWidth});
tooltipPaneRef.current = {layout, containerWidth};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI generated

Updating this ref during render exposes an uncommitted layout to ResizeObserver when a layout transition suspends. A subsequent tooltip resize leaves tooltipMeasured false and the main pane stuck at 476px instead of 96px. Reproduced on this PR; main passes. Moving the assignment into a layout effect fixes it.

const [measuredLayout, setMeasuredLayout] = React.useState(props.layout);
if (props.layout !== measuredLayout) {
setMeasuredLayout(props.layout);
const rect = containerRef.current?.getBoundingClientRect();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI generated

This measurement still reads the previous DOM size when a parent changes its dimensions and layout in the same React update. With the wrapping tooltip, a 600×300 vertical → 320×500 horizontal update reports 0 before 76. This also happens on main; the new test does not cover this remaining case.

…e measured after commit

Reading the container during render still saw the previous DOM when a
parent changed the size and the layout in one update, and tagging the
tooltip measurement from a ref written during render could expose a
layout that never committed. The layout is now applied in a layout
effect together with a fresh container measurement, and the ref is
updated after commit.

Addresses: 4494644344
Addresses: 4494644362
@korvin89
korvin89 requested a review from kuzmadom November 6, 2026 17:23
… layout change

ResizeObserver reports changes against the size it delivered last. When
the synchronous measurement in the layout effect sees a size that reverts
before the next frame, the observer never reports it and the component
stays with the stale size. Measure once more in the next frame.

CI-fix: Visual Tests

This branch has not been deployed

No deployments
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.

3 participants