LearnNewsExamplesServices
Frontmatter
id18907
titleEnter in a grid picker editor opens the picker of the edit it ends
stateClosed
labels
bugaigrid
assigneesneo-opus-ada
createdAtSep 18, 2026, 5:24 PM
updatedAtSep 18, 2026, 6:30 PM
githubUrlhttps://github.com/neomjs/neo/issues/18907
authorneo-fable
commentsCount0
parentIssue18762
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 18, 2026, 6:30 PM

Enter in a grid picker editor opens the picker of the edit it ends

neo-fable
neo-fable commented on Sep 18, 2026, 5:24 PM

Context

Leaf of #18762 (grid cell editing, responsibility 2: the interaction contract). Found while evaluating the epic on the public example examples/grid/cellEditing, left unowned in the leaf map, measured today.

In a grid, Enter in an editor commits: keys route through grid.View, "the grid's single key registry" (src/grid/plugin/CellEditing.mjs, class docblock). A picker field has its own Enter: form.field.Picker#onKeyDownEnter opens the picker. Both run on the same key — the field's first, because the key event reaches the deepest component first.

The Problem

Measured under Playwright (Chromium) on the example's Date column, with a worker-side probe on Picker#createPicker / #showPicker and a main-thread MutationObserver on .neo-picker-container, 3 of 3 runs each:

Case App Worker DOM
fresh Date editor, Enter showPicker → createPicker: a whole picker, date selector included, is built for the editor the same key destroys no picker node
Date editor after a picker visit (picker instance exists), Enter showPicker the picker node is inserted and removed in the same millisecond, ~20 ms after the key

So every Enter-commit on a Date or Time editor builds or mounts a picker for an editor that is already ending. The mount for a dying owner is also the source of a flake: on a loaded CI runner the picker node has been seen to stay in document.body after the editor was gone (#18880 removed the assertion that tripped on it, and recorded the orphan as a defect-note).

form.field.ComboBox and its subclasses (Color, Country, Chip) are not affected: keys is a plain config, so their map (Down, Escape) replaces the base map and has no Enter.

The Architectural Reality

  • src/form/field/Picker.mjs: keys: {Enter: 'onKeyDownEnter', Escape: 'onKeyDownEscape'}; onKeyDownEnter() → !pickerIsMounted && showPicker(). Date.mjs and Time.mjs override it and call super for the closed case.
  • onKeyDownEscape() already shows the shape of a key the field owns: it acts only while the picker is mounted, then sets data.cancelBubble and returns false, so the grid's Escape never sees it — the two-step Escape CellEditingExample.spec.mjs pins.
  • Enter has no such split: the field acts, claims nothing, and grid.View's registry runs grid.plugin.CellEditing#onEnterKey → completeEdit() → editor.destroy() → Picker#destroy unmounts and destroys the picker it was just asked to show.
  • Picker already has the sibling switch for the other implicit open: showPickerOnFocus (default false).
  • The plugin creates editors in startEdit() as {module: TextField, ...column.editor, …} and already treats editor capabilities as optional (editor.selectText?.()).

The Fix

  • form.field.Picker: a config showPickerOnEnter (default true, today's behaviour everywhere outside a grid), read by onKeyDownEnter.
  • grid.plugin.CellEditing#startEdit: a picker editor is created with showPickerOnEnter: false, unless its column's editor config says otherwise. In a grid, Enter is the grid's.
  • Witnesses: a unit arm on Picker (the config off → Enter creates and shows no picker), and the e2e date: arm in CellEditingExample.spec.mjs gets back the assertion #18880 had to drop, on the right premise this time: no picker node is ever sighted after the committing Enter (MutationObserver, ordering proven by the editor's removal).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
form.field.Picker#showPickerOnEnter (new config) src/form/field/Picker.mjs (showPickerOnFocus is the precedent) true: Enter opens a closed picker, as today. false: Enter is left to the field's host default true — no behaviour change outside grids config docblock unit arm
grid.plugin.CellEditing#startEdit (editor defaults) src/grid/plugin/CellEditing.mjs picker editors get showPickerOnEnter: false; column.editor can override — method docblock e2e arm on the public example

Decision Record impact

none

Acceptance Criteria

  • AC-1: with showPickerOnEnter: false, Enter on a closed picker field neither creates nor shows a picker; the default keeps today's behaviour. Unit tier.
  • AC-2: Enter in a grid Date editor commits with no picker node sighted at any point after the key, fresh and after a picker visit; the arm is red on dev for the after-a-visit case. e2e tier, public example.
  • AC-3: the existing date: arms (trigger opens the picker inside the session, two-step Escape) stay green.

Out of Scope

  • A keyboard path to open a picker inside a grid editor. Measured today: in a Date editor ArrowDown and Alt+ArrowDown open nothing, and Enter — the field's only open key — is the grid's commit, so a keyboard user cannot reach the date picker in a grid at all (the value itself stays typeable: the input is type="date"). This ticket does not change that; it only stops building pickers nobody sees. Facts for whoever takes it: util.KeyNavigation key maps carry no modifiers; on type="date" the plain arrow keys step the focused segment natively; the WAI-ARIA date-picker combobox pattern opens on Alt+ArrowDown; ComboBox already opens on Down.
  • What Enter means while the picker is open and focus is in the input (today: the grid commits, measured).
  • Picker#onKeyDownEnter(data, callback, callbackScope) forwards two arguments showPicker() does not take — dead parameters through Date.mjs / Time.mjs.
  • The engine-level race behind the orphan node (a floating component destroyed while its mount is in flight). This ticket removes one producer of that race, not the race.

Avoided Traps

  • Claiming Enter in the field (cancelBubble), as Escape does. Then a closed Date editor could never be committed by Enter: every Enter would open the picker again.
  • Teaching the field about grids (reading the neo-grid-editor class in Picker). The host knows its key contract; the field should not know its hosts.

Related

#18762 (epic) · #18838 / PR #18852 (the picker joins the field's component tree) · #18850 / PR #18856 (a destroyed floating component takes no focus) · #18880 / PR #18882 (the flaky picker-node assertion)

Live latest-open sweep: checked the latest 20 open issues at 2026-09-18T15:40Z plus an all-state search (picker / Enter / grid editor); no equivalent found — #18863 excludes the picker island and does not name Enter in picker editors. A2A sweep (latest 12, all read-states): no claim on this scope. Memory Core sweep: only my own 09-18 measurements (the "9 ms picker flash" input sent to the epic author, never filed). Own open assignments: #18865, #15252 — neither overlaps.

unowned-rationale: small and fully specified, so an Opus seat resolves it at a fraction of my seat's cost (operator's 2026-09-18 budget steer); I am on two flaking arms of mine first, and take this one if it is still open afterwards.

Origin Session ID: 323cb989-06a2-4b3f-8d70-912d67796c11

Retrieval Hint: "picker editor Enter showPickerOnEnter grid commit orphan picker node"