Context
Found while scoping hover submenus (operator request, 2026-09-16): #18759 and #18760 are blocked by this ticket. On dev@d688710175, examples/button/base: with the parent Item 3 already showing its submenu, a MutationObserver armed before a further click on Item 3 recorded zero menu-list additions or removals. #4606 specified that Enter on a parent toggles its submenu, and #4669 routes clicks through the same onKeyDownEnter().
The Problem
src/menu/List.mjs writes its submenu cache under one key and reads it under another:
- write:
showSubMenu() stores under store.getKey(record); toggleSubMenu() reads the same way.
- read:
onKeyDownEnter() and onSelect() look up getItemRecordId(nodeId).
Since #9071 (2026-02-09), list.Base#useInternalId defaults to true, so getItemRecordId() returns the internalId (neo-record-9). A menu record's key is its id (file-open), or -1, -2, … assigned by collection.Base when it has none. The two never match:
- The toggle branch in
onKeyDownEnter() is unreachable, so Enter and click on an open parent never close it.
- The
activeSubMenu !== subMenuMap[…] guard in onSelect() is always true. Harmless only because selection.Model#select() calls onSelect() on a change alone.
A second defect sits beside it. showSubMenu() replaces activeSubMenu without unmounting a different submenu that is showing. Today only onSelect() reaches it, and it hides first. Once the toggle path runs again, toggleSubMenu() on a sibling would strand the previous submenu: mounted, with nothing tracking it.
The Architectural Reality
list.Base#getItemRecordId() and getRecordId() are the internalId-aware pair. A record's cached submenu is an identity by key, so it resolves through store.getKey(record), like getSubMenuData() already does.
- Listener order decides what a click does.
list.Base registers click → onClick in construct(); the selection model registers click → onListClick in register(), called from onConstructed(); manager.DomEvent sorts equal priorities stably. Read from source — not yet run — item handling runs before selection, which is the order #4606's toggle needs. AC-3 witnesses it.
The Fix — src/menu/List.mjs
- One accessor returns a record's cached submenu through
store.getKey(record). onKeyDownEnter(), onSelect(), showSubMenu() and toggleSubMenu() all use it.
showSubMenu() hides a different active submenu before it shows its own, so at most one submenu per level is mounted. onSelect() reduces to show-or-hide.
Acceptance Criteria
Out of Scope
- Hover-shown submenus (#18759) and keyboard cascade walking (#18760).
Related
#4606 · #4669 · #9071
Live latest-open sweep: latest 20 open issues at 2026-09-16T08:07:16Z; no equivalent found
A2A claim sweep: latest 30 inbox messages, all read-states, at 2026-09-16T08:07Z; no [lane-claim] / [lane-intent] on menus
MC sweep: query_raw_memories on the symptom ("menu.List toggleSubMenu onKeyDownEnter subMenuMap; need to click a menu list item to expand the child list"), 6 results, no prior decision; KB ticket query surfaced #4606
Own-assignment sweep: 5 open, none on src/menu
Origin Session ID: fda7faab-19c5-43a0-b192-003966068172
Retrieval Hint: "menu.List submenu cache key space: store.getKey vs getItemRecordId internalId since useInternalId"
Context
Found while scoping hover submenus (operator request, 2026-09-16): #18759 and #18760 are blocked by this ticket. On
dev@d688710175,examples/button/base: with the parentItem 3already showing its submenu, a MutationObserver armed before a further click onItem 3recorded zero menu-list additions or removals. #4606 specified that Enter on a parent toggles its submenu, and #4669 routes clicks through the sameonKeyDownEnter().The Problem
src/menu/List.mjswrites its submenu cache under one key and reads it under another:showSubMenu()stores understore.getKey(record);toggleSubMenu()reads the same way.onKeyDownEnter()andonSelect()look upgetItemRecordId(nodeId).Since #9071 (2026-02-09),
list.Base#useInternalIddefaults totrue, sogetItemRecordId()returns the internalId (neo-record-9). A menu record's key is itsid(file-open), or-1, -2, …assigned bycollection.Basewhen it has none. The two never match:onKeyDownEnter()is unreachable, so Enter and click on an open parent never close it.activeSubMenu !== subMenuMap[…]guard inonSelect()is always true. Harmless only becauseselection.Model#select()callsonSelect()on a change alone.A second defect sits beside it.
showSubMenu()replacesactiveSubMenuwithout unmounting a different submenu that is showing. Today onlyonSelect()reaches it, and it hides first. Once the toggle path runs again,toggleSubMenu()on a sibling would strand the previous submenu: mounted, with nothing tracking it.The Architectural Reality
list.Base#getItemRecordId()andgetRecordId()are the internalId-aware pair. A record's cached submenu is an identity by key, so it resolves throughstore.getKey(record), likegetSubMenuData()already does.list.Baseregistersclick → onClickinconstruct(); the selection model registersclick → onListClickinregister(), called fromonConstructed();manager.DomEventsorts equal priorities stably. Read from source — not yet run — item handling runs before selection, which is the order #4606's toggle needs. AC-3 witnesses it.The Fix —
src/menu/List.mjsstore.getKey(record).onKeyDownEnter(),onSelect(),showSubMenu()andtoggleSubMenu()all use it.showSubMenu()hides a different active submenu before it shows its own, so at most one submenu per level is mounted.onSelect()reduces to show-or-hide.Acceptance Criteria
id.Out of Scope
Related
#4606 · #4669 · #9071
Live latest-open sweep: latest 20 open issues at 2026-09-16T08:07:16Z; no equivalent found A2A claim sweep: latest 30 inbox messages, all read-states, at 2026-09-16T08:07Z; no
[lane-claim]/[lane-intent]on menus MC sweep:query_raw_memorieson the symptom ("menu.List toggleSubMenu onKeyDownEnter subMenuMap; need to click a menu list item to expand the child list"), 6 results, no prior decision; KB ticket query surfaced #4606 Own-assignment sweep: 5 open, none onsrc/menuOrigin Session ID: fda7faab-19c5-43a0-b192-003966068172 Retrieval Hint: "menu.List submenu cache key space: store.getKey vs getItemRecordId internalId since useInternalId"