LearnNewsExamplesServices
Frontmatter
id17935
titleGrid cell editing still speaks the pre-View-anchor focus dialect — keyboard mount dead, editors abandoned as zombies
stateClosed
labels
bug
assigneesneo-opus-vega
createdAtAug 31, 2026, 12:07 PM
updatedAtAug 31, 2026, 1:31 PM
githubUrlhttps://github.com/neomjs/neo/issues/17935
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 1:24 PM

Grid cell editing still speaks the pre-View-anchor focus dialect — keyboard mount dead, editors abandoned as zombies

neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 12:07 PM

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):

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. 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.
  2. 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.
  3. 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

  • Enter/Space on a grid with a selected cell mounts the editor for that cell: keys live on the View's registry (the documented single key registry), and the target cell comes from the selection model. Headless spec: select → Enter → the editor input holds DOM focus.
  • focusCells either performs a real focus (View-anchored, per the design comment) or retires; no silent no-op call path remains.
  • Switching the selected cell while an editor is mounted settles the editor (submit-or-unmount — pick and document the polarity) instead of abandoning it; the onSelectionChange TODO resolves or is removed with its reason.
  • Re-invoking edit on a cell that carries a stale editor produces a mounted AND focused editor (spec pins document.activeElement = the editor input after the second invocation).
  • Container-level focusLeave keeps unmounting the editor when DOM focus truly leaves the grid (existing behavior pinned so the migration cannot regress it).
  • Grid cell resolution works for record inputs (the pooled-row index derivation lives in the grid plugin), and the unmount path redraws pooled rows via the row component — no createRow call against grid.Body remains.
  • The selection model resolves from its actual host (view where present, owner otherwise) for connect, key-path resolution, and settle.

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) 🌿