Context
Leaf of #18762, found by #18867's witness arms. The session is keyed by record id and dataField, and grid.Row#applyRendererOutput embeds the editor into whichever body renders the column. A runtime lock or unlock of the edited column moves it between bodies while the editor is open. Measured on #grid-cell-editing-pooled (test/playwright/component/apps/grid-cell-editing), locking c3 from inside the App Worker (no pointer, so no focus change ends the edit first):
Open edit on record 3's c3, then |
Observed |
lock c3 to the start |
the editor follows into the start body, with its draft, and survives a vertical pool round trip |
lock c3 to the end |
no cell embodies the editor. A vertical round trip does not bring it back |
| lock to the start, then unlock |
no cell embodies the editor, and App Worker: vdom update wedged: "neo-grid-body-4" has been in-flight for over 5000ms. Its reply was likely lost — the component will not update again |
Controls with no open edit: locking and unlocking c3 renders every c3 cell in the new body in both directions. Unlocking from the end places c3 after c38, so it renders once scrolled right (by design, not this defect).
The Mechanism
Measured at 0887e08dd8 (#18887's head), with the App Worker's plugin methods, VDomUpdate registrations and Helper.updateBatch wrapped, and the main thread's DeltaUpdates logged. neo-grid-body-4 is the center body, -5 the start and -6 the end. Three defects compound:
- The editor is lost in the diff. One batch re-renders all three bodies. Its deltas recycle the center cell that held the editor,
{id: 'neo-grid-body-4__row-2__cell-2', innerHTML: 'r3c4'}, and only then move the editor, {action: 'moveNode', id: 'neo-textfield-1', parentId: 'neo-grid-body-6__row-2__c3'}. The innerHTML detaches the editor and focus drops to BODY, so the move finds no node. The start direction survives because the start body diffs before the center one, so its move lands before the wipe. vdom.Helper already defers removals behind moves (deltas.default.concat(deltas.remove)). An innerHTML switch that replaces old children, including one the same diff moves, stays in the default phase.
- The blur commits. The detached editor's
focusLeave reaches onEditorFocusLeave. isEmbodied() answers from the column's new body, which renders the record, so it returns true: {embodied: true, locked: 'end'}. completeEdit() writes the draft and destroys the editor. The unlock does the same through focusMove, with the editor already unmounted (mounted: false).
- The wedge. The batch in (1) is the center body's own flight, escalated to the viewport. The editor had merged into it (its
neo-is-dirty update was in flight), then Row#applyRendererOutput re-parented it to the end body's row. VDomUpdate#getMergedChildIds walks a bridge path from each merged child up to the owner. From the re-parented editor that walk never meets neo-grid-body-4, so it climbs to the root and adds every ancestor. collectPayloads() collects the viewport's full tree, the collision filter drops the body's own payload as covered, and the reply carries a vnode for the viewport only. The body's resolveVdomUpdate() never runs, so its registration never clears.
The Fix
An open edit is held for the duration of any lock change, not only one of its own column: a column changing body also shifts the pool slots of the columns beside it. While it is held, no Row embeds the editor, the blur its removal causes is neutral, and once the bodies have swapped columns, the body rendering the column embeds it again. The draft lives in the editor instance, which survives, as it does when a sort or scroll takes the record out of the row pool. This removes all three triggers here: no diff has to move the editor, and nothing re-parents it while it is merged into a flight. ("Held" rather than "suspended": #18892 uses "suspended" for an edit whose cell left the row pool.)
(1) and (3) are engine defects that do not need a grid to fire: #18900 and #18899, each with its own witness.
Acceptance Criteria
Out of Scope
- Where an unlocked column lands in the column order.
- Locks started by pointer (a header drag moves focus and commits the edit first).
- Focus returning to the re-embedded editor: the same interaction-contract scope as a reprojection after sort or filter.
Related
#18762 · #18867 · #18764 · #12946
Decision Record impact: none.
Live latest-open sweep: latest 20 open issues at 2026-09-18T13:17Z; all-state searches locked column editor and wedged vdom update: no equivalent. A2A lane-claims (latest 30): none on this scope. Memory Core: the runtime-lock body lifecycle (June, #9872) and #18764's retired-editor render (the closest class), no decision on moving an open edit. Own open assignments: #18839 (PR #18857), #18867.
Origin Session ID: 6ed6621f-e2e4-4b9d-9a95-744679cb3c59
Retrieval Hint: "grid cell editing runtime column lock unlock open editor lost vdom update wedged body merged child bridge path innerHTML moveNode"
Context
Leaf of #18762, found by #18867's witness arms. The session is keyed by record id and
dataField, andgrid.Row#applyRendererOutputembeds the editor into whichever body renders the column. A runtime lock or unlock of the edited column moves it between bodies while the editor is open. Measured on#grid-cell-editing-pooled(test/playwright/component/apps/grid-cell-editing), lockingc3from inside the App Worker (no pointer, so no focus change ends the edit first):c3, thenc3to the startc3to the endApp Worker: vdom update wedged: "neo-grid-body-4" has been in-flight for over 5000ms. Its reply was likely lost — the component will not update againControls with no open edit: locking and unlocking
c3renders everyc3cell in the new body in both directions. Unlocking from the end placesc3afterc38, so it renders once scrolled right (by design, not this defect).The Mechanism
Measured at
0887e08dd8(#18887's head), with the App Worker's plugin methods,VDomUpdateregistrations andHelper.updateBatchwrapped, and the main thread'sDeltaUpdateslogged.neo-grid-body-4is the center body,-5the start and-6the end. Three defects compound:{id: 'neo-grid-body-4__row-2__cell-2', innerHTML: 'r3c4'}, and only then move the editor,{action: 'moveNode', id: 'neo-textfield-1', parentId: 'neo-grid-body-6__row-2__c3'}. TheinnerHTMLdetaches the editor and focus drops toBODY, so the move finds no node. The start direction survives because the start body diffs before the center one, so its move lands before the wipe.vdom.Helperalready defers removals behind moves (deltas.default.concat(deltas.remove)). AninnerHTMLswitch that replaces old children, including one the same diff moves, stays in the default phase.focusLeavereachesonEditorFocusLeave.isEmbodied()answers from the column's new body, which renders the record, so it returns true:{embodied: true, locked: 'end'}.completeEdit()writes the draft and destroys the editor. The unlock does the same throughfocusMove, with the editor already unmounted (mounted: false).neo-is-dirtyupdate was in flight), thenRow#applyRendererOutputre-parented it to the end body's row.VDomUpdate#getMergedChildIdswalks a bridge path from each merged child up to the owner. From the re-parented editor that walk never meetsneo-grid-body-4, so it climbs to the root and adds every ancestor.collectPayloads()collects the viewport's full tree, the collision filter drops the body's own payload as covered, and the reply carries a vnode for the viewport only. The body'sresolveVdomUpdate()never runs, so its registration never clears.The Fix
An open edit is held for the duration of any lock change, not only one of its own column: a column changing body also shifts the pool slots of the columns beside it. While it is held, no Row embeds the editor, the blur its removal causes is neutral, and once the bodies have swapped columns, the body rendering the column embeds it again. The draft lives in the editor instance, which survives, as it does when a sort or scroll takes the record out of the row pool. This removes all three triggers here: no diff has to move the editor, and nothing re-parents it while it is merged into a flight. ("Held" rather than "suspended": #18892 uses "suspended" for an edit whose cell left the row pool.)
(1) and (3) are engine defects that do not need a grid to fire: #18900 and #18899, each with its own witness.
Acceptance Criteria
vdom update wedgedor other worker error during either direction.test.fail()arms intest/playwright/e2e/grid/CellEditingLockedBodies.spec.mjsflip to passing arms.Out of Scope
Related
#18762 · #18867 · #18764 · #12946
Decision Record impact: none.
Live latest-open sweep: latest 20 open issues at 2026-09-18T13:17Z; all-state searches
locked column editorandwedged vdom update: no equivalent. A2A lane-claims (latest 30): none on this scope. Memory Core: the runtime-lock body lifecycle (June, #9872) and #18764's retired-editor render (the closest class), no decision on moving an open edit. Own open assignments: #18839 (PR #18857), #18867.Origin Session ID: 6ed6621f-e2e4-4b9d-9a95-744679cb3c59 Retrieval Hint: "grid cell editing runtime column lock unlock open editor lost vdom update wedged body merged child bridge path innerHTML moveNode"