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
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"
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#onKeyDownEnteropens 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/#showPickerand a main-threadMutationObserveron.neo-picker-container, 3 of 3 runs each:showPicker→createPicker: a whole picker, date selector included, is built for the editor the same key destroysshowPickerSo 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.bodyafter the editor was gone (#18880 removed the assertion that tripped on it, and recorded the orphan as a defect-note).form.field.ComboBoxand its subclasses (Color,Country,Chip) are not affected:keysis 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.mjsandTime.mjsoverride it and callsuperfor the closed case.onKeyDownEscape()already shows the shape of a key the field owns: it acts only while the picker is mounted, then setsdata.cancelBubbleand returnsfalse, so the grid's Escape never sees it — the two-step EscapeCellEditingExample.spec.mjspins.grid.View's registry runsgrid.plugin.CellEditing#onEnterKey→completeEdit()→editor.destroy()→Picker#destroyunmounts and destroys the picker it was just asked to show.Pickeralready has the sibling switch for the other implicit open:showPickerOnFocus(defaultfalse).startEdit()as{module: TextField, ...column.editor, …}and already treats editor capabilities as optional (editor.selectText?.()).The Fix
form.field.Picker: a configshowPickerOnEnter(defaulttrue, today's behaviour everywhere outside a grid), read byonKeyDownEnter.grid.plugin.CellEditing#startEdit: a picker editor is created withshowPickerOnEnter: false, unless its column'seditorconfig says otherwise. In a grid, Enter is the grid's.Picker(the config off → Enter creates and shows no picker), and the e2edate:arm inCellEditingExample.spec.mjsgets 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
form.field.Picker#showPickerOnEnter(new config)src/form/field/Picker.mjs(showPickerOnFocusis the precedent)true: Enter opens a closed picker, as today.false: Enter is left to the field's hosttrue— no behaviour change outside gridsgrid.plugin.CellEditing#startEdit(editor defaults)src/grid/plugin/CellEditing.mjsshowPickerOnEnter: false;column.editorcan overrideDecision Record impact
none
Acceptance Criteria
showPickerOnEnter: false, Enter on a closed picker field neither creates nor shows a picker; the default keeps today's behaviour. Unit tier.devfor the after-a-visit case. e2e tier, public example.date:arms (trigger opens the picker inside the session, two-step Escape) stay green.Out of Scope
type="date"). This ticket does not change that; it only stops building pickers nobody sees. Facts for whoever takes it:util.KeyNavigationkey maps carry no modifiers; ontype="date"the plain arrow keys step the focused segment natively; the WAI-ARIA date-picker combobox pattern opens on Alt+ArrowDown;ComboBoxalready opens onDown.Picker#onKeyDownEnter(data, callback, callbackScope)forwards two argumentsshowPicker()does not take — dead parameters throughDate.mjs/Time.mjs.Avoided Traps
cancelBubble), as Escape does. Then a closed Date editor could never be committed by Enter: every Enter would open the picker again.neo-grid-editorclass inPicker). 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"