LearnNewsExamplesServices
Frontmatter
id18883
titleTab while a grid edit is suspended leaves the grid with the edit open
stateClosed
labels
enhancementaigridaccessibility
assigneesneo-fable
createdAtSep 18, 2026, 3:00 PM
updatedAtSep 18, 2026, 4:26 PM
githubUrlhttps://github.com/neomjs/neo/issues/18883
authorneo-fable
commentsCount0
parentIssue18762
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 18, 2026, 4:26 PM

Tab while a grid edit is suspended leaves the grid with the edit open

Closed v13.2.0/archive-v13-2-0-chunk-21 enhancementaigridaccessibility
neo-fable
neo-fable commented on Sep 18, 2026, 3:00 PM

Context

Leaf of #18762, split out of #18861 with its author's agreement (@neo-opus-grace, 2026-09-18): #18861 / PR #18878 deliver Tab and Shift+Tab for a MOUNTED editor, in the App Worker only. This is D#17832's second row of the same terminal table, and it needs a main-thread contract the mounted path does not.

unowned-rationale: split for scope, not claimed — I am finishing #18861 and #18871. Claimable.

The Problem

An edit is suspended when its cell leaves the row pool or the mounted column window: the session and its draft live on in the App Worker, no editor is in the DOM, and focus is on grid.View (measured for #18843: after a sort moved the record out of the pool, document.activeElement is div.neo-grid-view).

main.DomEvents#onKeyDown cancels Tab's default only for an INPUT whose event path contains neo-grid-editor. While suspended there is no such input, so Tab is the browser's own: focus leaves the grid, and the session is neither committed nor ended.

D#17832's terminal table (OQ5) asks for: "before suspension the plugin pre-registers synchronous Main-thread suppression for the focused grid.View; the prevented event commits if valid, advances generation and target, and routes through grid.View; an invalid draft reprojects and blocks the move."

The Architectural Reality

  • src/main/DomEvents.mjs onKeyDown: the editor branch is isInput && event.key === 'Tab' && testPathInclusion(event, ['neo-grid-editor', 'neo-table-editor'], true). The declarative registry next to it, disabledInputKeys (registerDisabledInputChars / unregister…), is gated on isInput too — D#17832 constraint 11 names both options: generalise its registered-id branch to a non-input target, or add a sibling registry.
  • An App Worker key handler cannot call preventDefault() after dispatch: the suppression has to be registered BEFORE the key arrives, which is why the plugin registers it when the embodiment goes and removes it when the session ends or is embodied again.
  • grid.plugin.CellEditing#onTabKey (PR #18878) is the terminal; today it returns early unless isEditorEvent(data). The suspended path reaches it with the View as target.
  • isEmbodied(session) already answers "suspended" from the App Worker's own projection state.

The Fix

A main-thread registration that cancels Tab's default for a registered non-input node id. The plugin registers grid.View for as long as a SESSION is open, and unregisters it when the session ends — commit, cancel, disable, destroy. onTabKey accepts the View as the key's source and runs the same terminal as the mounted path: commit if valid, then edit the adjacent editable cell, born suspended when its Row is not rendered; an invalid draft scrolls its cell back into sight instead.

Revised while building it (2026-09-18): the first draft registered only while suspended. There is no event for "the embodiment went", and the narrower window buys nothing: a session that is embodied but not focused — the state every reprojection leaves, measured for #18843 — has the same problem, a Tab on the View would walk away from an open draft. One rule covers both: while a session is open, the View's Tab belongs to the edit.

Acceptance Criteria

  • With the edited row scrolled out of the pool, Tab commits a valid draft and edits the next editable cell; focus never leaves the grid. Red on dev first.
  • The same for a column scrolled out of the mounted window, and for Shift+Tab.
  • An invalid suspended draft blocks the move and its cell is scrolled back into sight.
  • With no session, Tab on grid.View stays the browser's own — before any edit, and after one ended: the registration never outlives its session (arm: focus moves off the View).
  • The cancelled default is observable: on a suspended Tab the only element that ever takes focus is the new editor's input — no bounce through a scrollbar or out of the grid.
  • Brain-free e2e arms, each killed by a named mutation recorded in the PR.

Out of Scope

The mounted path (#18861) · the IME probe and S3 opt-out (#18865) · reprojection focus (#18863).

Avoided Traps

  • Cancelling Tab for every grid.View: a keyboard trap — a grid nobody edits must let Tab through.
  • An App Worker handler "preventing" the default after the fact: the key has already acted by the time the worker sees it.

Related

#18762 · #18861 / PR #18878 · D#17832 (terminal table, OQ5, constraint 11) · #18843 (where the suspended focus was measured).

Live latest-open sweep: latest 12 open issues at 2026-09-18T12:57Z — no equivalent; #18861 is the ticket this splits from. A2A sweep at 12:54Z: the split was agreed by its author (message e04e5ec2).

Origin Session ID: 323cb989-06a2-4b3f-8d70-912d67796c11

Retrieval Hint: "grid cell editing suspended session Tab leaves grid main thread suppression disabledInputKeys non-input registry"