Skip to content

FE-1544: Rebase the token spreadsheet on the worksheet focus layer - #9418

Open
kube wants to merge 1 commit into
mainfrom
claude/fe-1544-spreadsheet-focus-layer
Open

FE-1544: Rebase the token spreadsheet on the worksheet focus layer#9418
kube wants to merge 1 commit into
mainfrom
claude/fe-1544-spreadsheet-focus-layer

Conversation

@kube

@kube kube commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Rebase the token spreadsheet (place initial state, scenario forms) on the worksheet keyboard-flow layer from #9411, replacing its hand-rolled focus and key routing. This fixes two shipping defects: Tab was intercepted everywhere and swallowed at the last cell, trapping keyboard users, and row focus went through an unscoped document.querySelector, so a scenario form with several per-place grids could focus the wrong grid.

🔗 Related links

🔍 What does this change?

  • Spreadsheet delegates movement, tab order, and the row-number lane to useFocusStops: each grid is one Tab stop with a roving tabindex, arrows walk cells and the gutter, and Tab leaves the grid instead of being trapped. The ~250-line hand-rolled key router, the focusedCell/selectedRow state, and every setTimeout/document.querySelector focus call are deleted.
  • Row selection follows the layer's useRowSelection grammar: the row whose gutter holds focus renders selected; Delete there removes the row, and the index-keyed rows keep focus in the lane afterwards.
  • Cell clicks follow the layer's select-first grammar (useSelectFirstActivation): the first click selects a cell, a click on the selected cell opens its editor. This also gives mouse users an editing path, which did not exist before (editing was keyboard-only).
  • Editor focus hand-off (open, commit-and-advance, cancel) runs through flushSync plus the grid's own target map instead of timers and document-wide queries.
  • The scenario form wraps its per-place grids in FocusRoot/FocusStack, so arrows flow from one place's spreadsheet into the next.
  • The grid declares role="grid"/row/gridcell/rowheader per the layer's contract, and the uuid overlay is revealed by CSS :focus-within instead of focus state.
  • Intentional behaviour changes: Enter/typing on a cell still edits, but Escape on a non-editing cell no longer blurs it; the row highlight clears when the gutter loses focus; boolean t/f/1/0 shortcuts now ignore modifier chords (Cmd+T reaches the browser again); printable keys typed into a cell no longer leak to the editor's global single-key shortcuts.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • With a row-number cell focused, single-letter editor shortcuts (v/h/n/t) still reach the canvas, as before this PR. Settling that globally is part of the wider focus-layer adoption.

🐾 Next steps

  • FE-1545 (internal) adopts the layer in the left sidebar lists and search.

🛡 What tests cover this?

  • New spreadsheet.test.tsx: roving tabindex, arrow movement and the gutter lane, Tab not intercepted, edit open/commit/advance/cancel, phantom-row materialization, gutter delete with focus retention, boolean key handling, select-first clicks, and cross-grid flow inside a FocusStack.
  • Existing focus-flow.test.tsx covers the underlying layer contract.

❓ How to test this?

  1. yarn workspace @hashintel/petrinaut storybook, open Components / Spreadsheet.
  2. Tab into the grid (one stop), walk cells and the row-number lane with arrows, press Enter or type to edit, Enter to commit and advance, Delete on a row number to remove the row, and fill the bottom phantom row to add one.
  3. Tab again: focus leaves the grid.
  4. In the app, open a scenario's Initial State section with two coloured places and arrow from one grid into the next.

The grid's visuals are unchanged; the existing docs screenshots stay valid.

🤖 Generated with Claude Code

@kube kube self-assigned this Aug 28, 2026
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 30, 2026 12:27am
petrinaut Ready Ready Preview Aug 30, 2026 12:27am
petrinaut-docs Ready Ready Preview Aug 30, 2026 12:27am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 30, 2026 12:27am

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team labels Aug 28, 2026
@kube
kube marked this pull request as ready for review August 28, 2026 20:28
@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
This is a broad refactor of keyboard focus and editing for initial marking data, with intentional behaviour tweaks (Escape, row highlight, modifier keys); regressions would affect accessibility and scenario authoring rather than simulation correctness.

Overview
Token spreadsheets (place initial state and scenario coloured-place grids) now use the shared worksheet focus layer instead of a large hand-rolled keyboard router.

Each grid is one Tab stop with roving tabindex; arrow keys move between cells and the row-number gutter, Tab is no longer trapped in the grid, and focus hand-off uses a local target map plus flushSync instead of setTimeout / document.querySelector. The scenario form’s per-place grids sit in a vertical FocusStack so arrows can move from one place’s spreadsheet into the next.

Interaction matches the worksheet grammar: select-first clicks open the editor on a second click on the selected cell, Delete on the gutter removes a row while keeping focus in the lane, and the grid exposes role="grid" / row / gridcell / rowheader. UUID full-string overlays show on :focus-within rather than React focus state.

Adds spreadsheet.test.tsx for the focus/edit/delete/boolean/cross-grid behaviours and updates user docs (simulation, scenarios) and arch worksheet notes. Patch changeset for @hashintel/petrinaut.

Reviewed by Cursor Bugbot for commit 0bc5b0b. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0bc5b0b. Configure here.

tabIndex={
isReadOnly
? undefined
: tabIndexFor({ stopId, column: "gutter" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale roving tab stop after shrink

Medium Severity

The roving tabIndex is never cleared when the remembered cell disappears. After a type edit drops a column, or parent data shrinks past the last focused row, tabIndexFor matches nothing and every cell stays at -1, so Tab can no longer enter the grid until a click resets memory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0bc5b0b. Configure here.

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

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant