Skip to content

feat(interaction): let brushX and zoomX pass idle hover to point focus - #162

Open
greglum wants to merge 11 commits into
TanStack:mainfrom
greglum:greg/control-pointer-passthrough
Open

greglum wants to merge 11 commits into
TanStack:mainfrom
greglum:greg/control-pointer-passthrough

Conversation

@greglum

@greglum greglum commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

With brushX or zoomX on a plot, hovering that plot shows no point focus or tooltip. The controls cover the plot and capture every pointer event, and the host clears focus whenever the pointer is inside a control (renderer.ts). We hit this in a production app and had to move brush and zoom into a separate overview chart to keep point tooltips on the main plot.

This adds one option shared by both controls: hover?: 'capture' | 'passthrough' (ChartControlHover). The default, 'capture', keeps today's behavior.

With 'passthrough':

  • Idle mouse or pen hover over the zoom surface or the empty brush area focuses the nearest point and shows its tooltip.
  • The brush selection and its handles still capture hover.
  • During an active pan, brush drag or touch gesture, focus clears. It returns on the next idle hover.
  • Presses, clicks, wheel and keyboard stay with the control, so one click never both selects a point and starts a brush.
  • After a wheel zoom, focus re-resolves at the last pointer position, including when the zoom surface took focus from a click.

It works through the existing control-containment check, via an internal passesHover(target) on control instances. There is no CSS change, and the Canvas host shares the same renderer path.

Verification:

  • New brush and zoom tests cover the default, capture, passthrough, drag suppression, focus returning after a drag, brush selection capture, and re-resolve after click then wheel.
  • typecheck, docs check, bundle check and benchmark check pass.
  • The interactions guide gains a runnable zoom-plus-tooltip example.

Bundle cost:

  • DOM host: about +34 B raw.
  • Brush and zoom: about +0.3 kB raw / +0.1 kB gzip each.
  • Two size limits rise by 0.01 to 0.02 kB.

Not tested: real browsers and real touch or pen devices; everything above ran under jsdom. package:check fails locally at the React 18 packed-consumer step with ERR_PNPM_PEER_DEP_ISSUES, and main fails the same way here, so this looks environmental.

Includes a minor changeset and API-FRICTION entry F-307. That ID will need renumbering if another friction entry lands first.

Summary by CodeRabbit

  • New Features
    • Brush and zoom controls now support a passthrough hover option. While the pointer is idle over eligible plot areas, nearby points can receive focus and display tooltips; the default capture behavior is unchanged.
    • Point focus clears during active gestures and resumes on idle hover. Brush and zoom controls retain presses, clicks, wheel input, and keyboard input; focus is refreshed after accepted wheel zoom.
  • Documentation
    • Added guidance and examples covering hover modes and interaction behavior during gestures.

Add a shared hover: 'capture' | 'passthrough' option to brushX and zoomX.
With passthrough, idle hover over the zoom surface or the blank brush area
resolves chart point focus and tooltips through the host's control
containment check. Brush selections and handles, active gestures, clicks,
wheel, and keyboard input stay with the control. The default keeps the
current behavior.
…ocus

A passthrough zoom surface needs focus for default wheel activation. Taking
that focus no longer clears the resting pointer focus, so an accepted wheel
zoom re-resolves the tooltip at the last pointer position. Cache the brush
overlay lookup and cover focus returning after a brush drag ends.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0b10179c-8722-4304-bc7a-d5c0869eff73
📥 Commits

Reviewing files that changed from the base of the PR and between 313107c and 92b48a9.

📒 Files selected for processing: 24
  • .changeset/control-hover-passthrough.md
  • API-FRICTION.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/comparison/bundle-baseline.json
  • benchmarks/conformance/previews/manifest.json
  • docs/comparison.md
  • docs/guides/interactions-and-selections.md
  • docs/reference/focus-and-interaction.md
  • docs/reference/index.md
  • docs/reference/types.md
  • packages/charts-core/docs/comparison.md
  • packages/charts-core/docs/guides/interactions-and-selections.md
  • packages/charts-core/docs/reference/focus-and-interaction.md
  • packages/charts-core/docs/reference/index.md
  • packages/charts-core/docs/reference/types.md
  • packages/charts-core/src/dom-types.ts
  • packages/charts-core/src/interaction-brush.test.ts
  • packages/charts-core/src/interaction-brush.ts
  • packages/charts-core/src/interaction-range-internal.ts
  • packages/charts-core/src/interaction-zoom.test.ts
  • packages/charts-core/src/interaction-zoom.ts
  • packages/charts-core/src/renderer.ts
  • scripts/measure-bundles.mjs
  • scripts/public-callback-contract.mjs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9c7390a5-fac9-43c8-9d51-02bfebf1d322
📥 Commits

Reviewing files that changed from the base of the PR and between b32fb23 and 313107c.

📒 Files selected for processing: 17
  • API-FRICTION.md
  • benchmarks/bundle-size/universal-baseline.json
  • benchmarks/comparison/bundle-baseline.json
  • benchmarks/conformance/previews/manifest.json
  • docs/comparison.md
  • docs/reference/focus-and-interaction.md
  • docs/reference/types.md
  • packages/charts-core/docs/comparison.md
  • packages/charts-core/docs/reference/focus-and-interaction.md
  • packages/charts-core/docs/reference/types.md
  • packages/charts-core/src/dom-types.ts
  • packages/charts-core/src/interaction-brush.test.ts
  • packages/charts-core/src/interaction-brush.ts
  • packages/charts-core/src/interaction-zoom.test.ts
  • packages/charts-core/src/renderer.ts
  • scripts/measure-bundles.mjs
  • scripts/public-callback-contract.mjs
🚧 Files skipped from review as they are similar to previous changes: 5
  • benchmarks/comparison/bundle-baseline.json
  • benchmarks/bundle-size/universal-baseline.json
  • docs/comparison.md
  • packages/charts-core/docs/comparison.md
  • packages/charts-core/docs/reference/focus-and-interaction.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

📝 Walkthrough

brushX and zoomX now share a hover option. The default capture mode keeps hover with the control. The passthrough mode forwards eligible idle hover to chart point focus while control gestures and other inputs remain control-owned.

Changes: Brush and zoom hover passthrough

Layer / File(s) Summary
Hover contract and renderer handling
packages/charts-core/src/interaction-range-internal.ts, packages/charts-core/src/dom-types.ts, packages/charts-core/src/renderer.ts, scripts/public-callback-contract.mjs
Adds ChartControlHover and a resolver that defaults to capture and rejects other values. Adds passesHover to host controls. The renderer honors that callback for pointer movement and focus-in events.
Brush and zoom hover behavior
packages/charts-core/src/interaction-brush.ts, packages/charts-core/src/interaction-zoom.ts, packages/charts-core/src/interaction-brush.test.ts, packages/charts-core/src/interaction-zoom.test.ts
Both controls accept and store the resolved hover mode. In passthrough mode, eligible idle hover reaches chart focus handling. Tests cover default and passthrough behavior, gestures, zoom focus, and invalid zoom values.
Public guidance and release notes
.changeset/control-hover-passthrough.md, API-FRICTION.md, docs/guides/interactions-and-selections.md, docs/reference/*, packages/charts-core/docs/guides/interactions-and-selections.md, packages/charts-core/docs/reference/*
Documents the shared option, control-owned inputs, and a zoom tooltip example. Adds the changeset and records the API friction finding.
Bundle and conformance measurement refresh
benchmarks/bundle-size/universal-baseline.json, benchmarks/comparison/bundle-baseline.json, benchmarks/conformance/previews/manifest.json, docs/comparison.md, packages/charts-core/docs/comparison.md, scripts/measure-bundles.mjs
Updates bundle measurements, source revision and date records, the preview source hash, and gzip budgets.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Pointer
  participant ChartRenderer
  participant BrushOrZoomControl
  participant PointFocus
  Pointer->>ChartRenderer: send pointer movement
  ChartRenderer->>BrushOrZoomControl: check passesHover
  BrushOrZoomControl-->>ChartRenderer: allow eligible idle hover
  ChartRenderer->>PointFocus: resolve chart focus
Loading

Suggested reviewers: tannerlinsley

Merge Risk: ⚪ Minimal · up to 31310

No actionable defect was established. The change is mergeable after normal checks; real-device hover behavior remains unverified.

Security architecture risk: 🔵 Low · up to b32fb

The change is opt-in and preserves the existing capture default. Reviewed paths retain control ownership for activation and suppress hover forwarding during active gestures. No introduced security issue was substantiated, but real-browser touch, pen and concurrent-pointer behavior remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The identified exposure expansion is the control-surface area from which users can trigger existing chart focus and tooltip behavior. Downstream effects can include configured cursor-session participants and application callbacks; their application-specific authority is not established by the supplied library evidence.

Trust Boundaries and Controls

  • observed — Passthrough changes event ownership policy rather than granting new credentials or identity. A contained target is exempted only when the renderer requests hover handling and its control accepts that target; another containing control can still capture it. Ordinary click and keyboard checks do not request this exemption.

Resilience and Maintainability Implications

  • observed — Host controls are retained by extension and control key, destroyed when replaced or removed, and collectively destroyed with the renderer. This existing lifecycle bounds the new predicate to currently retained control instances.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 9 files. (10 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: allowing brushX and zoomX to pass idle hover to point focus.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 9 files. (10 skipped: 10 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

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.

@changeset-bot

changeset-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 92b48a9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@tanstack/charts Minor
@tanstack/alpine-charts Minor
@tanstack/angular-charts Minor
@tanstack/lit-charts Minor
@tanstack/octane-charts Minor
@tanstack/preact-charts Minor
@tanstack/react-charts Minor
@tanstack/react-native-charts Minor
@tanstack/solid-charts Minor
@tanstack/svelte-charts Minor
@tanstack/vue-charts Minor
@tanstack/charts-scales Minor

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

@tannerlinsley

Copy link
Copy Markdown
Member

Thanks Greg, I checked the unchanged runtime at 92b48a9 against current main in isolated Chromium with real mouse input. Default and explicit capture kept focus inside the control. Passthrough focused points, drag movement cleared point focus, and idle hover restored it. None of those gestures selected a chart point.

Matched production fixtures measured these gzip increases, in decimal kB:

  • DOM host: 0.019 kB
  • Brush and DOM host: 0.132 kB
  • Zoom and DOM host: 0.113 kB
  • Compact React: 0.018 kB
  • React line with Canvas: 0.019 kB
  • Motion renderer: unchanged

The cost summary needs a refresh. The current diff raises seven source ceilings, not two, and the zoom ceiling rises by about 0.094 kB. Those changes still need approval. The PR description also says F-307 while the actual entry is F-315, which overlaps other pending contributions.

This is bounded browser and bundle verification, not a full repository validation pass. Touch, pen, wheel behavior and packed consumers still need verification before merge.

@tannerlinsley

Copy link
Copy Markdown
Member

Follow-up on 92b48a9: wheel zoom and accepted panning now pass the isolated Chromium checks too. Focus stays positioned correctly after wheel zoom, clears during a pan, and returns at the updated point position afterward. Accepted mouse pans and Chromium touch/pen input emulation all pass.

I also checked default, capture and passthrough modes for both controls with mouse, touch and pen input. Brush callbacks fire, gestures do not select chart points, and idle pen hover follows the selected mode. Touch and pen gestures were separated beyond D3’s existing 500 ms ghost-click suppression window, so this does not claim immediate mixed-device switching or physical-device coverage.

Full repository CI and the bundle-cost review are still outstanding.

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.

2 participants