Skip to content

feat(focus): two-dimensional grid focus for heatmaps - #164

Draft
greglum wants to merge 8 commits into
TanStack:mainfrom
greglum:greg/grid-focus-strategy
Draft

greglum wants to merge 8 commits into
TanStack:mainfrom
greglum:greg/grid-focus-strategy

Conversation

@greglum

@greglum greglum commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Draft, to settle a contract question before polishing.

Heatmap keyboard focus walks one flat list, and ArrowDown behaves like ArrowRight (interaction.ts). We build a person-by-week capacity heatmap in a production app as an HTML table with one button per cell, partly for this reason. The friction is logged as API-FRICTION F-307.

This draft adds an optional step(points, { point, key, modifier }) to ChartFocusStrategy. It returns the next point, or undefined to fall back to navigation() order. It also adds an opt-in focusGrid from @tanstack/charts/focus/grid:

  • Left/Right move within a row and Up/Down within a column.
  • Home/End go to the row ends, and Ctrl/Cmd+Home/End go to the first and last cell, following the ARIA grid pattern.
  • Missing cells are skipped in the direction of travel. Focus stays put at an edge, and recovers if the focused cell disappears.
  • Rows and columns come from shared pixel coordinates, the same keying axisFocus uses. A one-row or one-column strip still accepts pointers anywhere along it.
  • The existing tooltip status region already announces the cell's row and column.

Existing strategies behave exactly as before. Ordinary DOM charts grow about 41 B gzip for the dispatch. Static scene and SVG bundles do not change, and focusGrid alone is about 1.2 kB gzip.

I did not let navigation() return rows. That would change a return type that the renderer, the D3 host and React Native all consume, and it still could not express grid extremes or skipping missing cells.

Open questions:

  • (a) key is the raw KeyboardEvent.key, so only the DOM host dispatches step. The D3 host and React Native call navigation() only, so grid charts fall back to row-major linear order there; the docs say so. A host-neutral direction vocabulary ('right' | 'left' | 'up' | 'down' | 'rowStart' | 'rowEnd' | 'first' | 'last') would let every host dispatch it.
  • (b) modifier is one boolean for Control or Command. Would you prefer explicit ctrlKey and metaKey?
  • Should focusGrid also handle PageUp/PageDown, or logical Home/End for right-to-left layouts?

Verification:

  • focusGrid unit tests cover row and column moves, sparse skips, Home/End, edge clamping, single-row pointers and removed cells. The mounted ArrowDown test fails under linear order.
  • typecheck, docs check (the runnable heatmap example is type-checked), bundle and benchmark checks pass.
  • Not run: browser conformance and a real screen reader. package:check fails locally at the React 18 packed-consumer step with ERR_PNPM_PEER_DEP_ISSUES, and main fails the same way here.

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

Add an optional step(points, context) method to ChartFocusStrategy so a
strategy can choose the next point for a navigation key. Strategies
without step keep their navigation order. Ship focusGrid from
@tanstack/charts/focus/grid for row-and-column movement in heatmaps,
calendars, and matrices.
Single-row or single-column grids no longer reject pointers off the
center line. End with no focus moves to the last cell, matching linear
navigation. A focused cell that no longer exists restarts from the grid
edge instead of jumping inside the row. Document the offset-label caveat
and name the open key and modifier contract choices.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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: 030c131

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 @greglum, I checked the grid behavior at 030c1319053c9c845d7610ab0178717b4d44e50e using the exact core source in isolated Chromium, Firefox and WebKit contexts.

The sparse-grid checks passed against an independent row/column oracle, with more than 106,000 directional moves per browser. The mounted checks also passed for reversed x/y domains, LTR and RTL containers, resize preserving the focused datum, removing the focused cell, and cleanup, eight cases per browser. Removing step made the mounted ArrowDown check fail by moving to another column, so that check distinguishes the proposed behavior from linear navigation.

This is source-bundle verification with installed dependencies, not a full-checkout CI run, packed-package check or screen-reader test. I have not accepted the proposed budget changes or resolved the draft's input-contract questions.

One documentation detail to clarify: I could confirm the React Native fallback in createNativeChartFocusModel, but could not find the separate D3 companion host in this revision. Is that an external consumer? If so, the docs should identify it rather than imply it is another shipped host. The framework-neutral direction vocabulary remains worth settling before this becomes public API.

@tannerlinsley

Copy link
Copy Markdown
Member

Thanks @greglum, I was able to extend verification at the same 030c1319053c9c845d7610ab0178717b4d44e50e head.

The isolated checkout passes 2,231 DOM tests, all six additional framework test targets, TypeScript, the documentation contract, the catalog checks, and the seven-adapter package gate. The actual packed core export also passes the sparse-grid oracle and all eight mounted cases in each of Chromium, Firefox and WebKit, with no workspace source inputs. Disabling step in that packed harness fails the same-column check as expected.

This still is not a full CI or release-package approval. The React Native packed consumer cannot access the shared dependency store from the isolated environment. The bundle check passes against this PR's own proposed budgets, which does not approve those budget increases. The draft and input-contract questions above remain open.

@tannerlinsley

Copy link
Copy Markdown
Member

Thanks @greglum, the local package blocker is resolved at this same head. The full package gate now passes in the credential-free sandbox: packed exports, declarations and runtime, bare React Native and Expo Metro consumers, all seven adapters, and the unified artifact. I prepared a separate dependency store rather than granting access to the shared store.

The previously tested grid runtime is byte-identical in the final unified artifact across all 112 packed modules used by that fixture. The draft, input-contract discussion, proposed budget increases and required CI remain separate holds.

Verified head: 030c1319053c9c845d7610ab0178717b4d44e50e.

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