Sub of #18762 — the session/projection core, with the minimal complete edit loop. Graduated from D#17832 (OQ1–OQ7 dispositions, OQ1 spike).
Context
Neo.grid.plugin.CellEditing extends Neo.table.plugin.CellEditing and mounts no editor by any gesture. Measured at dev@d688710175 on a grid with cellEditing: true and a CellModel: double-click, click+Enter and F2 mount nothing, and double-click throws Cannot read properties of null (reading 'vdom').
D#17832's OQ1 spike, on the same head, prototyped the target shape against a 40-column / 400-row grid with locked-start and locked-end columns: a View-owned session whose editor the rendering Row embeds into the proving cell. Double-click and F2 embodied a focused editor in pooled and locked cells; an in-range horizontal remap kept embodiment and focus; leaving the row pool or the column window removed it; returning reprojected it with the App-Worker draft intact; Enter committed and Escape cancelled. Zero page errors.
It also measured two defects that break the loop on common input:
Home, End, PageUp and PageDown pressed inside the editor scroll grid.View, at either caret position (Chromium, macOS key bindings). End scrolled 98 → 12081 and PageDown one page, recycling the proving row mid-typing; Home and PageUp scrolled 160 → 0. ArrowUp/ArrowDown are consumed by the input. On Linux key bindings Home/End move the caret instead, so PageUp/PageDown are the cross-platform red.
A pointer click inside the embodied editor hands DOM focus to grid.View.grid.Body#onRowClick (src/grid/Body.mjs:1219) focuses the View for every click inside a row.
The Problem
The inherited lifecycle cannot be patched into shape (D#17832 Reflective Pause): it resolves a cell with body.getCellId(record, dataField) against Grid's (rowIndex, dataField) signature, assumes the center body, registers keys on owner.body.keys — never on a key event's path while focus lives on grid.View — reads neo-selected from the focused element, and unmounts through body.createRow, which grid.Body lacks. Two fixes that kept the inheritance were dropped (#17935/PR #17937, #18217/PR #18222).
The Architectural Reality
surface
today
src/grid/Container.mjs:374afterSetCellEditing
lazily imports ./plugin/CellEditing.mjs and pushes it onto plugins — the public entry point, kept
Neo.grid.plugin.CellEditing extends Neo.plugin.Base, keeps its class name and ntype, and owns one session {recordId, dataField, editor, generation}. It registers Enter/F2/Escape on grid.View's keys and removes them on destroy.
The Row embeds the session's editor in applyRendererOutput for the cell proving {recordId, dataField}, in the pooled and permanent passes, and sets the editor's parent to itself — so unmountRemovedChildren tracks projection loss and the next render reprojects. Starting and ending a session repaints only the rows that render the record.
Terminals: Enter commits a valid dirty value to the record; Escape cancels; focus leaving the grid commits-if-valid; activating another cell commits-if-valid first; each ends with DOM focus on grid.View. Projection loss is classified from the logical resolver — is the record in a row pool and the column in the mounted window — never from message timing.
grid.column.Base declares editable_ (reactive) and editor. Turning editable off ends an active session on that column.
Defect 1:DomEvents#onKeyDown stops a navigation key pressed inside a neo-grid-editor from scrolling, keeping the platform's caret semantics for Home/End.
Defect 2: a pointer gesture inside .neo-grid-editor neither focuses the View nor changes the cell selection.
Anchor & Echo JSDoc on the plugin, the Row seam and the new column configs; regenerate docs/output/class-hierarchy.json.
Contract Ledger
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
Neo.grid.plugin.CellEditing (cellEditing: true)
src/grid/Container.mjs:374
Grid-native plugin; one logical session; View-routed keys
none — the inherited plugin mounts nothing today
class @summary
component + e2e arms red on dev, green at head
Neo.grid.Row#applyRendererOutput
src/grid/Row.mjs:144
embeds the active editor into the proving cell; the editor's parent follows the Row
cell renders as today when no session proves it
method JSDoc
e2e pooling arms
Neo.grid.column.Base#editable_ / #editor
src/grid/column/Base.mjs (undeclared today)
declared; editable defaults to false, today's !column.editable semantics; runtime false ends that column's session
an undeclared property keeps working as a config
config JSDoc
component arm
grid.Body#onRowClick focus
src/grid/Body.mjs:1219
clicks inside .neo-grid-editor do not focus the View or select
unchanged outside editors
method JSDoc
defect-2 arm red on dev
Neo.main.DomEvents#onKeyDown scroll keys in neo-grid-editor
src/main/DomEvents.mjs:592
Home/End/PageUp/PageDown never scroll the grid View from an editor
a view that renders its own selection (view.silentSelect) is not asked for a whole-view update; restoreSelection and select already read the flag
unchanged for a view that leaves selection rendering to the model
inline comment
the editable-off arm: two editors inserted without it
Neo.selection.grid.BaseModel#silently
src/selection/grid/BaseModel.mjs (reset today)
restores the previous view.silentSelect instead of resetting it to false, so nested changes stay silent
a non-nested call behaves as before
method JSDoc
same arm
Acceptance Criteria
AC-1 — src/grid/plugin/CellEditing.mjs extends Neo.plugin.Base; nothing under src/grid/ imports src/table/plugin/CellEditing.mjs, which is unchanged.
AC-2 — double-click, Enter and F2 on an editable cell in the center, locked-start and locked-end bodies embody one editor in that cell with DOM focus in its input; Space starts no edit; an editable: false column embodies nothing.
AC-3 — Enter writes a valid dirty value to the record; Escape leaves the record unchanged; an invalid value blocks Enter and keeps the editor focused; commit and cancel remove the embodiment, destroy the editor and return DOM focus to grid.View.
AC-4 — with a typed draft: an in-range horizontal remap keeps embodiment and focus; the column leaving the mounted window, or the record leaving the row pool, removes the embodiment without committing or cancelling; returning reprojects it with the draft. At no point does any cell show the editor for another record or dataField.
AC-5 — activating another cell commits-if-valid, then embodies the new target; focus leaving the grid commits-if-valid; exactly one .neo-grid-editor exists in the grid at any time.
AC-6 — setting a column's editable to false while its session is active cancels that session.
AC-7 — PageUp and PageDown pressed inside the editor do not move grid.View's scroll position (red on dev); where the platform binds Home/End to scrolling, neither do they; where it binds them to the caret, the caret still moves.
AC-8 — a click inside the embodied editor keeps DOM focus in its input and leaves the cell selection unchanged — red against the new plugin without the onRowClick exemption.
AC-9 — destroying the grid, or the plugin, destroys an active editor and removes the plugin's View keys; no page error in any arm.
AC-10 — coverage CI executes: a components-tier spec for AC-2, 3, 5, 6, 8, 9 and a Brain-free e2e spec (no neuralLink token) on a high-cardinality fixture for AC-2's bodies, AC-4 and AC-7 — each arm red-first where the defect exists on dev, and node buildScripts/util/e2eCiSelection.mjs --coverage-line counting the new e2e file.
Out of Scope
Tab/Shift+Tab traversal and the suspended-Tab pre-registration · picker editors, the interaction island and two-stage Escape · GestureClaimArbiter ordering · restoring DOM focus on reprojection · aria-readonly and live announcement of invalid state · the S3 opt-out · the IME probe · examples/grid/cellEditing (its renderer-scope crash), the Grids guide, the Table control · Table editing.
Avoided Traps
Hooks on the Table plugin or a getCellId shim — PR #18222's shape; keeps the forbidden inheritance load-bearing.
Mounting the editor by mutating a found cell's vdom — the inherited approach; the next Row render drops it, and a pool slot then carries it for another record.
Classifying projection loss by waiting for the focus manager's focusLeave timing — the spike saw both orders; the logical resolver is deterministic.
A Neural Link whitebox spec as the proof — deselected in CI by buildScripts/util/e2eCiSelection.mjs.
Suppressing Home/End unconditionally — breaks caret movement on platforms that bind them to the caret.
Live latest-open sweep: latest 20 open issues at 2026-09-16T08:30Z; no equivalent. A2A claim sweep: no claim on grid cell editing. Memory Core rationale sweep: D#17832's authoring and fold sessions; nothing contradicts this leaf. Own-assignment sweep: nothing on grid editing.
tobiu referenced in commit d32a0c3 - "feat(grid): cell editing is a Grid-native session projected into whichever pooled cell renders it (#18764) (#18769) on Sep 16, 2026, 1:40 PM
Sub of #18762 — the session/projection core, with the minimal complete edit loop. Graduated from D#17832 (OQ1–OQ7 dispositions, OQ1 spike).
Context
Neo.grid.plugin.CellEditingextendsNeo.table.plugin.CellEditingand mounts no editor by any gesture. Measured atdev@d688710175on a grid withcellEditing: trueand aCellModel: double-click, click+Enter and F2 mount nothing, and double-click throwsCannot read properties of null (reading 'vdom').D#17832's OQ1 spike, on the same head, prototyped the target shape against a 40-column / 400-row grid with locked-start and locked-end columns: a View-owned session whose editor the rendering Row embeds into the proving cell. Double-click and F2 embodied a focused editor in pooled and locked cells; an in-range horizontal remap kept embodiment and focus; leaving the row pool or the column window removed it; returning reprojected it with the App-Worker draft intact; Enter committed and Escape cancelled. Zero page errors.
It also measured two defects that break the loop on common input:
grid.View, at either caret position (Chromium, macOS key bindings). End scrolled 98 → 12081 and PageDown one page, recycling the proving row mid-typing; Home and PageUp scrolled 160 → 0. ArrowUp/ArrowDown are consumed by the input. On Linux key bindings Home/End move the caret instead, so PageUp/PageDown are the cross-platform red.grid.View.grid.Body#onRowClick(src/grid/Body.mjs:1219) focuses the View for every click inside a row.The Problem
The inherited lifecycle cannot be patched into shape (D#17832 Reflective Pause): it resolves a cell with
body.getCellId(record, dataField)against Grid's(rowIndex, dataField)signature, assumes the center body, registers keys onowner.body.keys— never on a key event's path while focus lives ongrid.View— readsneo-selectedfrom the focused element, and unmounts throughbody.createRow, whichgrid.Bodylacks. Two fixes that kept the inheritance were dropped (#17935/PR #17937, #18217/PR #18222).The Architectural Reality
src/grid/Container.mjs:374afterSetCellEditing./plugin/CellEditing.mjsand pushes it ontoplugins— the public entry point, keptsrc/grid/Row.mjs:144applyRendererOutput/:332createVdomdata.recordIdanddata.fieldstill match — the projection authoritysrc/mixin/VdomLifecycle.mjs:928syncVnodeTree→unmountRemovedChildrensrc/grid/View.mjs:57keysview.keys._keyssrc/selection/grid/BaseModel.mjs:374hasEditorFocusneo-grid-editorsrc/main/DomEvents.mjs:592onKeyDownneo-grid-editorpaths — the precedent for defect 1src/grid/column/Base.mjseditableoreditor; the example sets them as undeclared propertiesresources/scss/src/grid/plugin/CellEditing.scss.neo-grid-cell:has(.neo-grid-editor)The Fix
Neo.grid.plugin.CellEditingextendsNeo.plugin.Base, keeps its class name and ntype, and owns one session{recordId, dataField, editor, generation}. It registers Enter/F2/Escape ongrid.View's keys and removes them on destroy.applyRendererOutputfor the cell proving{recordId, dataField}, in the pooled and permanent passes, and sets the editor's parent to itself — sounmountRemovedChildrentracks projection loss and the next render reprojects. Starting and ending a session repaints only the rows that render the record.grid.View. Projection loss is classified from the logical resolver — is the record in a row pool and the column in the mounted window — never from message timing.grid.column.Basedeclareseditable_(reactive) andeditor. Turningeditableoff ends an active session on that column.DomEvents#onKeyDownstops a navigation key pressed inside aneo-grid-editorfrom scrolling, keeping the platform's caret semantics for Home/End..neo-grid-editorneither focuses the View nor changes the cell selection.docs/output/class-hierarchy.json.Contract Ledger
Neo.grid.plugin.CellEditing(cellEditing: true)src/grid/Container.mjs:374@summarydev, green at headNeo.grid.Row#applyRendererOutputsrc/grid/Row.mjs:144Neo.grid.column.Base#editable_/#editorsrc/grid/column/Base.mjs(undeclared today)editabledefaults tofalse, today's!column.editablesemantics; runtimefalseends that column's sessiongrid.Body#onRowClickfocussrc/grid/Body.mjs:1219.neo-grid-editordo not focus the View or selectdevNeo.main.DomEvents#onKeyDownscroll keys inneo-grid-editorsrc/main/DomEvents.mjs:592neo-table-editoruntoucheddevneo-grid-editorclasssrc/main/DomEvents.mjs,src/selection/grid/BaseModel.mjs:374Neo.selection.Model#deselect/#deselectAllsrc/selection/Model.mjs:147,:175view.silentSelect) is not asked for a whole-view update;restoreSelectionandselectalready read the flagNeo.selection.grid.BaseModel#silentlysrc/selection/grid/BaseModel.mjs(reset today)view.silentSelectinstead of resetting it to false, so nested changes stay silentAcceptance Criteria
src/grid/plugin/CellEditing.mjsextendsNeo.plugin.Base; nothing undersrc/grid/importssrc/table/plugin/CellEditing.mjs, which is unchanged.editable: falsecolumn embodies nothing.grid.View.dataField..neo-grid-editorexists in the grid at any time.editabletofalsewhile its session is active cancels that session.grid.View's scroll position (red ondev); where the platform binds Home/End to scrolling, neither do they; where it binds them to the caret, the caret still moves.onRowClickexemption.neuralLinktoken) on a high-cardinality fixture for AC-2's bodies, AC-4 and AC-7 — each arm red-first where the defect exists ondev, andnode buildScripts/util/e2eCiSelection.mjs --coverage-linecounting the new e2e file.Out of Scope
Tab/Shift+Tab traversal and the suspended-Tab pre-registration · picker editors, the interaction island and two-stage Escape ·
GestureClaimArbiterordering · restoring DOM focus on reprojection ·aria-readonlyand live announcement of invalid state · the S3 opt-out · the IME probe ·examples/grid/cellEditing(its renderer-scope crash), the Grids guide, the Table control · Table editing.Avoided Traps
getCellIdshim — PR #18222's shape; keeps the forbidden inheritance load-bearing.focusLeavetiming — the spike saw both orders; the logical resolver is deterministic.buildScripts/util/e2eCiSelection.mjs.Decision Record
Discussion-origin: NOT_NEEDED. Impact: none.
Discussion Criteria Mapping
OQ1 → AC-2, AC-4 · OQ2 (S2 default, the scroll-away defect) → AC-4, AC-7 · OQ3 → AC-2 · OQ4 → AC-2, AC-5 · OQ5 (Enter, Escape, focus leaving the grid, activating another cell, the pointer defect) → AC-3, AC-5, AC-8 · OQ6 → AC-3, AC-5 · OQ7 → AC-1
Related
#18762 (parent) · D#17832 · #6168 (completion claim superseded) · #17935 · #18217 · PR #17937 · PR #18222 (salvage fixtures:
test/playwright/component/apps/grid-cell-editing/, needsviewConfig) · #18626 (View-owned selection model)Live latest-open sweep: latest 20 open issues at 2026-09-16T08:30Z; no equivalent. A2A claim sweep: no claim on grid cell editing. Memory Core rationale sweep: D#17832's authoring and fold sessions; nothing contradicts this leaf. Own-assignment sweep: nothing on grid editing.
Origin Session ID: a758f0e1-0dc1-4e4e-a975-4824a9319487
Retrieval Hint: "grid CellEditing render-time projection Row applyRendererOutput session pooled cells editable_ DomEvents scroll keys"