fix(context-menu): fix slash menu dismissal state (SD-2747)#3234
fix(context-menu): fix slash menu dismissal state (SD-2747)#3234tupizz wants to merge 2 commits into
Conversation
The slash command menu had three independent state bugs that combined to break the dismiss-and-retype flow: 1. Backspace and Delete were not handled anywhere — neither the PM plugin's handleKeyDown nor the Vue component's document keydown listener caught them, so pressing Backspace after opening the menu left it open. 2. A 5-second slashCooldown locked out subsequent `/` presses immediately after dismissal. The user typed `/`, dismissed the menu, typed `/` again to retry — and got a literal `/` inserted instead of the menu reopening. 3. Escape closed the menu but did not insert the slash the user originally typed (it had been preventDefault'd on open). Per the requirements that match Google Docs, dismissing with Escape should leave the slash visible while dismissing with Backspace should remove it. Plugin handleKeyDown now handles Backspace / Delete (close, no insert) and Escape / ArrowLeft (close, insert `/` at the original anchor). The 5-second cooldown is gone — subsequent `/` reopens the menu immediately. Focus shifts to the Vue search input when the menu opens, so the PM plugin can't see keys typed there. The Vue handleGlobalKeyDown handler gets the same three branches (Backspace/Delete close without insert, Escape closes and inserts the slash) so the dismissal works whichever element holds focus. Removed the three unit tests that codified the cooldown behavior; added six new tests covering the corrected dismissal contract.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 879d35941b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (props.editor && anchorPos !== null && anchorPos !== undefined) { | ||
| const tr = props.editor.state.tr.insertText('/', anchorPos); | ||
| const insertedAt = anchorPos + 1; | ||
| tr.setSelection(props.editor.state.selection.constructor.near(tr.doc.resolve(insertedAt))); | ||
| props.editor.dispatch(tr); |
There was a problem hiding this comment.
Gate slash reinsertion to slash-triggered menu dismissals
handleGlobalKeyDown always reinserts '/' on Escape whenever the menu is open, but the menu is also opened by right-click (handleRightClick dispatches type: 'open' with a normal cursor position). In that common path there was no suppressed slash keystroke to restore, so pressing Escape now mutates the document by inserting an unexpected / at anchorPos. Please condition this reinsertion on a slash-triggered open only (e.g., based on trigger/context), and keep Escape as close-only for context-menu opens.
Useful? React with 👍 / 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… (SD-2747) While the menu is open, focus is on a hidden search input that captures keystrokes for filtering. The user saw no feedback — they typed `intex`, the filter eliminated all items, and the menu collapsed to a zero-height invisible box. Visually it looked like the menu had silently vanished. Two additions, scoped to the same menu: - A "Searching: /<typed text>" header appears at the top of the menu whenever the user has typed any filter characters. The header uses a monospaced font for the slash + query so it reads as "this is what you're literally typing," matching command-palette conventions. - A "No matching commands" empty state renders inside the items list when the filter has eliminated every item, so the menu always has visible content as long as it's open. Existing items, divider rendering, and selection state are unchanged.
Demo
CleanShot.2026-05-11.at.15.00.36.mp4
CleanShot.2026-05-11.at.14.59.55.mp4
Summary
Three independent bugs combined to break the dismiss-and-retype flow on the
/command menu:handleKeyDownnor the Vue component's documentkeydownlistener caught them, so pressing Backspace after opening the menu left it open.slashCooldownlocked out subsequent/presses immediately after dismissal. Type/, dismiss the menu, type/again — and a literal/got inserted instead of the menu reopening./waspreventDefault'd on open, so the user's keystroke disappeared.Per the ticket spec (matching Google Docs):
/reopens the menu immediately, no cooldown.Changes
extensions/context-menu/context-menu.js(PM plugin): removedslashCooldown+ its 5 s timeout.handleKeyDownnow branches onBackspace/Delete(close, no insert),Escape/ArrowLeft(close, insert/at anchor), and the existing/-to-open path.components/context-menu/ContextMenu.vue(Vue): focus shifts to the hidden search input when the menu opens, so the PM plugin can't see keys typed afterward. Added the same Backspace/Delete and Escape branches tohandleGlobalKeyDownso the dismissal works whichever element holds focus.extensions/context-menu/context-menu.test.js: removed the three cooldown unit tests (they locked in the buggy behavior); added six new unit tests for Backspace / Delete / Escape / immediate reopen.Test plan
pnpm testforsuper-editor(12 711 / 12 724 pass — same asmainaside from the new tests)./→ press Backspace → press/again→→ ``/→ press Escape//→ press Delete→/Insert text/Insert table/Pasteand first item highlightedWhy remove the cooldown entirely
The original 5 s cooldown was intended to prevent rapid menu reopens, but its only behavioral effect was the bug in the ticket — typing
/again after dismissal inserted a literal/for 5 seconds. The dismissal handlers above are now explicit about state, so the cooldown's purpose is fully served bypluginState.openitself: when the menu is already open, the second/is ignored at the dispatch site without needing a separate timer.