Skip to content

fix(context-menu): fix slash menu dismissal state (SD-2747)#3234

Open
tupizz wants to merge 2 commits into
mainfrom
tadeu/sd-2747-slash-menu-dismiss-state
Open

fix(context-menu): fix slash menu dismissal state (SD-2747)#3234
tupizz wants to merge 2 commits into
mainfrom
tadeu/sd-2747-slash-menu-dismiss-state

Conversation

@tupizz
Copy link
Copy Markdown
Contributor

@tupizz tupizz commented May 11, 2026

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:

  1. Backspace and Delete did nothing — 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. Type /, dismiss the menu, type / again — and a literal / got inserted instead of the menu reopening.
  3. Escape closed the menu but didn't re-emit the slash. The original / was preventDefault'd on open, so the user's keystroke disappeared.

Per the ticket spec (matching Google Docs):

  • Backspace / Delete: dismiss the menu, no slash inserted.
  • Escape: dismiss the menu, leave the slash visible at the original anchor.
  • Subsequent / reopens the menu immediately, no cooldown.

Changes

  • extensions/context-menu/context-menu.js (PM plugin): removed slashCooldown + its 5 s timeout. handleKeyDown now branches on Backspace/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 to handleGlobalKeyDown so 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

  • New unit tests: 6 cases covering Backspace closes, Delete closes, Escape inserts the slash, Backspace doesn't insert, immediate reopen works, Backspace ignored when menu closed.
  • Full pnpm test for super-editor (12 711 / 12 724 pass — same as main aside from the new tests).
  • Live browser repro on the dev app:
Scenario Step Menu Doc Expected
Backspace press / → press Backspace → press / again open → closedopen → ``
Escape press / → press Escape open → closed `` → /
Delete press / → press Delete open → closed
Visual press / menu visible with Insert text / Insert table / Paste and first item highlighted

Why 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 by pluginState.open itself: when the menu is already open, the second / is ignored at the dispatch site without needing a separate timer.

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.
@tupizz tupizz requested a review from a team as a code owner May 11, 2026 17:45
@linear
Copy link
Copy Markdown

linear Bot commented May 11, 2026

SD-2747

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +237 to +241
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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-commenter
Copy link
Copy Markdown

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.
@tupizz tupizz self-assigned this May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants