LearnNewsExamplesServices
Frontmatter
id18764
titleGrid-native CellEditing core: a logical session projected into pooled cells
stateClosed
labels
enhancementaigrid
assigneesneo-opus-grace
createdAtSep 16, 2026, 10:31 AM
updatedAtSep 16, 2026, 1:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/18764
authorneo-opus-grace
commentsCount0
parentIssue18762
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 16, 2026, 1:40 PM

Grid-native CellEditing core: a logical session projected into pooled cells

Closed v13.2.0/archive-v13-2-0-chunk-21 enhancementaigrid
neo-opus-grace
neo-opus-grace commented on Sep 16, 2026, 10:31 AM

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:

  1. 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.
  2. 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:374 afterSetCellEditing lazily imports ./plugin/CellEditing.mjs and pushes it onto plugins — the public entry point, kept
src/grid/Row.mjs:144 applyRendererOutput / :332 createVdom builds every cell; the recycle pass reuses a cell node while data.recordId and data.field still match — the projection authority
src/mixin/VdomLifecycle.mjs:928 syncVnodeTree → unmountRemovedChildren marks a child whose node left its parent's tree unmounted; a child re-seated by reference remounts
src/grid/View.mjs:57 keys the grid's single key registry; the selection models push onto view.keys._keys
src/selection/grid/BaseModel.mjs:374 hasEditorFocus arrow navigation stands down while the event path holds neo-grid-editor
src/main/DomEvents.mjs:592 onKeyDown suppresses Tab's default in neo-grid-editor paths — the precedent for defect 1
src/grid/column/Base.mjs declares no editable or editor; the example sets them as undeclared properties
resources/scss/src/grid/plugin/CellEditing.scss already styles .neo-grid-cell:has(.neo-grid-editor)

The Fix

  • 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 unchanged outside grid editors; neo-table-editor untouched inline comment defect-1 arm (PageDown) red on dev
neo-grid-editor class src/main/DomEvents.mjs, src/selection/grid/BaseModel.mjs:374 unchanged cross-thread contract — — existing consumers keep reading it
Neo.selection.Model#deselect / #deselectAll src/selection/Model.mjs:147, :175 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.

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/, needs viewConfig) · #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"

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
tobiu closed this issue on Sep 16, 2026, 1:40 PM