Skip to content

FE-1545: Adopt the worksheet focus layer in the left sidebar lists and search - #9420

Merged
kube merged 15 commits into
mainfrom
claude/fe-1545-sidebar-focus-layer
Sep 3, 2026
Merged

kube merged 15 commits into
mainfrom
claude/fe-1545-sidebar-focus-layer

Conversation

@kube

@kube kube commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Before this PR, the left sidebar carried three hand-rolled focus models. Filterable lists tracked a virtual focus index in React state over unfocusable rows, with hover-only row menus. Nets list made every row and its invisible delete button a tab stop with no arrow keys. Search hopped focus between input and results through DOM queries and remounted the results on every keystroke.

Adopts the worksheet keyboard-flow layer across the sidebar. Each list is one Tab stop, arrows move and select, row menus and add buttons are reachable with ArrowRight, and the search input and its results form one vertical flow.

9420.mp4

Links

Changes

  • Filterable lists move to real DOM focus through useFocusStops

    Nodes, types, differential equations, parameters, and the entities tree.
    Each list is one Tab stop, arrows move and select as they move, Shift+Arrow extends the range, group headers collapse and expand with ArrowLeft and ArrowRight.
    Virtual focusedIndex state, row-ref bookkeeping, its scroll effect, and a dead per-row key handler are deleted.

  • Empty and non-empty list components merge into one that derives the visible rows in a single pass
  • RowActionCell makes row menus and group add buttons keyboard-reachable

    Registers the row's trailing button as its second column, so ArrowRight reaches it and ArrowLeft returns. Buttons show on focus as well as hover.

  • Nets list adopts the same model

    One Tab stop, arrow keys, Enter or Space activates a net, Delete removes a subnet, ArrowRight reaches the delete button, which was an invisible tab stop on every row.

  • Search joins the input and its results in one vertical FocusStack at the panel level

    ArrowDown in the input hands focus to the list and ArrowUp from the first result hands it back, replacing the closest and querySelector hops.
    key={query} remount of the results is gone, so the list keeps its position while typing.

  • Orphaned clampIndex helper deleted
  • Behaviour changes

    Focus highlight is real :focus styling.
    Nets list row highlight follows focus.
    A group's empty message renders as plain hint text.
    Collapse state and plain, Shift, and Cmd click semantics are unchanged.

Review fixes

  • Row action buttons leave the tab order

    RowActionCell sets tabIndex -1 on the registered button; ArrowRight still reaches it, so each list stays one Tab stop.

  • Deleting a subnet from the keyboard focuses a neighbouring row first

    Focus no longer falls to the document when the deleted row unmounts.

Known issues

  • Single-letter editor shortcuts v, h, n, t still fire while a list row holds focus

    Same as before this PR. Settling that globally is a follow-up to the focus-layer adoption.

  • Section headers between the lists are not part of the arrow flow yet

    That is the shared-chrome adoption of headers, tabs, and toolbars, proposed separately.

Next steps

  • Adopt the layer in the shared chrome, SubViewHeader, tab bars, and toolbars

Test coverage

  • filterable-list-sub-view.test.tsx:

    Roving tabindex, select-as-you-move with Shift ranges, ArrowRight to the row menu and back, group collapse and expand with hidden children skipped.

  • Existing focus-flow.test.tsx:

    Underlying movement contract.

How to test

  • Open Petrinaut preview on Vercel
  • Menu > Load example > SIR Model
  • Left sidebar > Nodes, Tab into the list
  • ArrowDown
  • Expect the canvas selection to follow
  • Shift+ArrowDown
  • Expect a range selection
  • ArrowRight
  • Expect focus on the row's ⋯ menu, ArrowLeft returns to the row
  • Nets > Add subnet > ArrowRight on the new row
  • Expect focus on the delete button
  • Cmd+F, type a name, ArrowDown
  • Expect focus in the results with the selection following
  • ArrowUp from the first result
  • Expect focus back in the input

@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
hash Ready Ready Preview Sep 3, 2026 5:02pm UTC
petrinaut Ready Ready Preview Sep 3, 2026 5:02pm UTC
petrinaut-docs Ready Ready Preview Sep 3, 2026 5:02pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Sep 3, 2026 5:02pm UTC

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 labels Aug 28, 2026
@kube
kube marked this pull request as ready for review August 28, 2026 20:47
@cursor

cursor Bot commented Aug 28, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Editor UI and keyboard accessibility only; selection and collapse semantics are preserved with added tests and no auth or data-path changes.

Overview
Replaces three ad-hoc left-sidebar focus implementations with the shared worksheet keyboard-flow primitives (useFocusStops, useFocusMember, FocusStack).

Filterable lists (Nodes, Types, etc.) are now a single Tab stop with roving tabindex on real row focus. Arrows move and update selection (Shift extends ranges); ArrowRight / ArrowLeft reach row menus and group + actions via new RowActionCell. Virtual focusedIndex handling, clamp-index, and container-level key handlers are removed; empty and populated lists share one component driven by visible rows (collapsed groups skip children in the stop list).

Nets list follows the same two-column stop model: arrow navigation, Delete on subnet rows, delete button reachable by arrow instead of extra tab stops, with focus moved to a neighbor before delete.

Search wraps the panel in a vertical FocusStack so ArrowDown from the input enters results and ArrowUp from the first row returns; DOM querySelector hops and key={query} remounting of results are gone.

User-facing docs in drawing-a-net.md and architecture worksheet.mdx describe the new behavior; filterable-list-sub-view.test.tsx covers roving tabindex, select-as-you-move, menus, and group collapse.

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

Copilot AI left a comment

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.

🟡 Changes recommended

Row actions remain native Tab stops, and the fallback introduces quadratic rendering work.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adopts the shared worksheet keyboard-focus model across Petrinaut’s left sidebar.

Changes:

  • Adds roving focus and keyboard navigation to sidebar lists and search.
  • Makes row actions keyboard-reachable and adds Nets keyboard controls.
  • Adds tests and updates user/architecture documentation.
File summaries
File Description
.changeset/sidebar-focus-layer.md Records the keyboard-navigation change.
libs/@local/petrinaut-arch-docs/content/ui/worksheet.mdx Documents new worksheet-layer consumers.
libs/@hashintel/petrinaut/docs/drawing-a-net.md Documents sidebar keyboard controls.
libs/@hashintel/petrinaut/src/ui/lib/clamp-index.ts Removes the obsolete helper.
libs/@hashintel/petrinaut/src/ui/worksheet/focus-flow.test.tsx Tests removed-stop fallback behavior.
libs/@hashintel/petrinaut/src/ui/worksheet/use-focus-stops.ts Adds fallback roving-tab-stop selection.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/panel.tsx Wraps search in a vertical focus flow.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.test.tsx Tests list keyboard navigation.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/filterable-list-sub-view.tsx Migrates filterable lists to real focus.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/nets-list.tsx Adds roving focus and keyboard actions.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/row-action-cell.tsx Adds shared action-column wiring.
libs/@hashintel/petrinaut/src/ui/views/Editor/panels/LeftSideBar/subviews/search-panel.tsx Integrates input/results focus navigation.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread libs/@hashintel/petrinaut/src/ui/worksheet/use-focus-stops.ts Outdated
kube added 8 commits September 2, 2026 03:00
Drop `find`, `unregister`, the shortcut-collision warning, and the silent
run-refresh snapshots: `register` now replaces by id in place and notifies,
and `useCommand` re-registers only when the id, label, category, keywords,
or shortcut change, reading the latest `run` through a ref.

Move the registry into `core.command-registry` and the React bindings into
`react.commands`, export the bindings from `@hashintel/petrinaut/react`
only, and add the arch-docs usage manual with a flow diagram. One changeset
covers both packages.

@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 20f6b10. Configure here.

kube added 7 commits September 3, 2026 18:48
…ings

Move the auto-layout command into the editor's command set, through the
mode-aware `usePetrinautCommands`, so nothing writes to the registry during
render and the command respects edit mode and the active subnet;
`createPetrinaut` loses its `commandRegistry` option. Read the properties
panel width from the editor store only, give `useCommands` a server
snapshot, return no keycaps for an empty shortcut, close the example
palettes on Escape from any element inside them, and keep the sidebar
command's label static so it holds its palette position.
Fold the three per-column text helpers into `fullText`/`displayText` over
one `isTextColumn` predicate, collapse `updateCell` and the editor
open/close paths, group the gutter's focus props, and list the layer's
consumers in the worksheet architecture page.
…ppears

`useFocusStops` computes the roving tab stop once per render and falls
back to the first stop when the remembered row or column is gone, so a
cleared marking or a dropped type element cannot take a grid out of the
tab order. Rows expose `aria-selected`, and the spreadsheet changeset is
dropped as an internal detail.
Merge the empty and non-empty list components, derive the visible rows in
one pass instead of a flattened list re-searched per row, drop the
`selectable` style variant, and rename `RowActionSlot` to `RowActionCell`.
The worksheet architecture page lists the sidebar as a consumer.
…elete

Row action buttons leave the tab order; ArrowRight still reaches them, so
each list stays one Tab stop. Deleting a subnet from the keyboard focuses a
neighbouring row before the deleted one unmounts. The sidebar changeset is
dropped as an internal detail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/apps 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.

3 participants