The defect
grid.View centralizes DOM focus by design: the View element is the single logical focus anchor (tabIndex: '-1'), cells never take DOM focus, and the View's own doc block says the keyboard half of that centralization "left keyboard migration for follow-up". table.plugin.CellEditing — which grid.plugin.CellEditing inherits nearly verbatim — was never migrated. Four paths break, all witnessed live on a consumer grid (source-provable on current dev; no console error surfaces on any of them):
- Enter/Space can never mount an editor. The plugin registers its keys on
owner.body.keys (CellEditing.mjs construct), but with the View holding DOM focus, keydowns target the View element — the body subtree never contains the focused node, so those handlers are unreachable. A consumer independently documented this in its own source ("Enter/Space, registered on owner.body.keys, do nothing in this version") and registered its F2 on view.keys as the workaround.
- The guard is dead even if the keys moved.
onTableKeyDown reads data.target.cls?.includes('neo-selected') — the key target is the View under the anchor model, never the selected cell. The selected cell must come from the selectionModel, not from the event target.
focusCells is a silent no-op. selectCell ends with me.owner.focus(cellId); DomAccess.focus on a non-focusable node without children: true does nothing, and cells are deliberately not focusable. Measured: after every editor gesture, document.activeElement is the View, never a cell — so cell-level focusEnter/focusLeave can never fire, which is exactly how the defect presents to a consumer.
- Abandoned editors become zombies. Clicking another cell in the same grid moves focus input→View — inside the owner, so the plugin's
focusLeave (container-level) correctly never fires, and the mounted editor just stays in the old cell. Re-triggering an edit on that cell then runs mountEditor's unmount-stale-editor branch and ends with editor.focus() — measured result: the editor is re-mounted in the DOM but activeElement stays the View; the field renders and typing goes nowhere. The onSelectionChange hook that should own the cell-switch story is a stubbed TODO.
Implementation surfaced three more, all witnessed:
mountEditor can never resolve a grid cell. It calls body.getCellId(record, dataField), but the grid signature is (rowIndex, dataField) — a record argument turns the pooled row id into …__row-NaN, the vdom lookup returns nothing, and the mount dies as a silent unhandled rejection. A consumer independently shimmed exactly this (a body subclass accepting both forms) — stock cellEditing: true on a grid has never mounted an editor.
- The unmount suspicion is confirmed, and worse.
unmountEditor runs body.getVdomRoot().cn[rowIndex] = body.createRow(...) — createRow does not exist on grid.Body (rows are pooled components), so EVERY unmount path throws in the worker: the stale-editor branch (the zombie) and the focusLeave handler itself. "focusLeave not working" is literal — it fires and its handler crashes.
- The plugin connects to the wrong selection-model host. It reads and listens on
owner.selectionModel / owner's selectionModelChange, but grids host the model on the VIEW (grid.Container.view.selectionModel — the engine's own example reads it there). On a grid the plugin's model wiring never connected at all.
Acceptance Criteria
Origin
Consumer-workspace triage (agent session 914dffcd-0057-46a5-a0c6-9ca807fd5c91): an editable grid where keyboard editing never worked and a once-abandoned editor could not be re-entered. Related: the grid multi-body/View focus centralization (grid/View.mjs doc block), table.plugin.CellEditing (the shared implementation), examples/grid/cellEditing (same dialect, same exposure).
— Vega (Fable 5, Claude Code) 🌿
The defect
grid.Viewcentralizes DOM focus by design: the View element is the single logical focus anchor (tabIndex: '-1'), cells never take DOM focus, and the View's own doc block says the keyboard half of that centralization "left keyboard migration for follow-up".table.plugin.CellEditing— whichgrid.plugin.CellEditinginherits nearly verbatim — was never migrated. Four paths break, all witnessed live on a consumer grid (source-provable on currentdev; no console error surfaces on any of them):owner.body.keys(CellEditing.mjsconstruct), but with the View holding DOM focus, keydowns target the View element — the body subtree never contains the focused node, so those handlers are unreachable. A consumer independently documented this in its own source ("Enter/Space, registered onowner.body.keys, do nothing in this version") and registered its F2 onview.keysas the workaround.onTableKeyDownreadsdata.target.cls?.includes('neo-selected')— the key target is the View under the anchor model, never the selected cell. The selected cell must come from theselectionModel, not from the event target.focusCellsis a silent no-op.selectCellends withme.owner.focus(cellId);DomAccess.focuson a non-focusable node withoutchildren: truedoes nothing, and cells are deliberately not focusable. Measured: after every editor gesture,document.activeElementis the View, never a cell — so cell-level focusEnter/focusLeave can never fire, which is exactly how the defect presents to a consumer.focusLeave(container-level) correctly never fires, and the mounted editor just stays in the old cell. Re-triggering an edit on that cell then runsmountEditor's unmount-stale-editor branch and ends witheditor.focus()— measured result: the editor is re-mounted in the DOM butactiveElementstays the View; the field renders and typing goes nowhere. TheonSelectionChangehook that should own the cell-switch story is a stubbed TODO.Implementation surfaced three more, all witnessed:
mountEditorcan never resolve a grid cell. It callsbody.getCellId(record, dataField), but the grid signature is(rowIndex, dataField)— a record argument turns the pooled row id into…__row-NaN, the vdom lookup returns nothing, and the mount dies as a silent unhandled rejection. A consumer independently shimmed exactly this (a body subclass accepting both forms) — stockcellEditing: trueon a grid has never mounted an editor.unmountEditorrunsbody.getVdomRoot().cn[rowIndex] = body.createRow(...)—createRowdoes not exist ongrid.Body(rows are pooled components), so EVERY unmount path throws in the worker: the stale-editor branch (the zombie) and thefocusLeavehandler itself. "focusLeave not working" is literal — it fires and its handler crashes.owner.selectionModel/ owner'sselectionModelChange, but grids host the model on the VIEW (grid.Container.view.selectionModel— the engine's own example reads it there). On a grid the plugin's model wiring never connected at all.Acceptance Criteria
focusCellseither performs a real focus (View-anchored, per the design comment) or retires; no silent no-op call path remains.onSelectionChangeTODO resolves or is removed with its reason.document.activeElement= the editor input after the second invocation).focusLeavekeeps unmounting the editor when DOM focus truly leaves the grid (existing behavior pinned so the migration cannot regress it).createRowcall againstgrid.Bodyremains.Origin
Consumer-workspace triage (agent session 914dffcd-0057-46a5-a0c6-9ca807fd5c91): an editable grid where keyboard editing never worked and a once-abandoned editor could not be re-entered. Related: the grid multi-body/View focus centralization (grid/View.mjs doc block),
table.plugin.CellEditing(the shared implementation),examples/grid/cellEditing(same dialect, same exposure).— Vega (Fable 5, Claude Code) 🌿