Skip to content

fix(suggestion): keep dismissed state after dismissal#7570

Merged
bdbch merged 5 commits intomainfrom
fix/suggestion-keep-dismissed-state
Mar 31, 2026
Merged

fix(suggestion): keep dismissed state after dismissal#7570
bdbch merged 5 commits intomainfrom
fix/suggestion-keep-dismissed-state

Conversation

@bdbch
Copy link
Copy Markdown
Member

@bdbch bdbch commented Mar 6, 2026

Changes Overview

After dismissing a suggestion via Escape, the suggestion menu would reappear as soon as the user typed another character in the same word. This makes dismissal feel broken — one Escape press should suppress the suggestion until the user clearly starts a
new input context.

Implementation Approach

Added a dismissedFrom: number | null field to the suggestion plugin's internal state. When the user dismisses via Escape (dispatching { exit: true } metadata), the trigger character's position (range.from) is stored in dismissedFrom.

On every subsequent transaction, before re-activating a match, the plugin checks:

  • Same word? — if match.range.from === dismissedFrom, the cursor is still in the dismissed word → stay suppressed
  • Whitespace/newline inserted? — if the transaction inserted any whitespace, the user has moved on → clear dismissedFrom
  • Different trigger position? — if the match starts at a different position, it's a different word → clear dismissedFrom
  • No match at all? — cursor has left any trigger context entirely → clear dismissedFrom

The key design decision: dismissedFrom intentionally survives the !next.active cleanup block at the bottom of apply(), so it persists across inactive transactions.

Testing Done

Added four new unit tests in suggestion.test.ts covering the full matrix:

  1. Typing more characters in the same word after dismissal → suggestion stays hidden
  2. Inserting a space after dismissal then typing a new @ → suggestion reopens
  3. Backspacing past the trigger char, then retyping it → suggestion reopens
  4. Typing a new @ at a different position after dismissal → suggestion reopens

Verification Steps

  1. Open an editor with the mention extension
  2. Type @foo — suggestion menu opens
  3. Press Escape — menu closes
  4. Type more characters (bar) — menu should not reappear (was broken before)
  5. Press Space, then type @ — menu should reappear ✓
  6. Repeat steps 2–3, then click somewhere else in the document and type @ — menu should reappear ✓

Additional Notes

No public API changes. The dismissedFrom field lives entirely in internal plugin state. The existing allow, shouldShow, and exitSuggestion API are unaffected.

Checklist

  • I have created a changeset for this PR if necessary.
  • My changes do not break the library.
  • I have added tests where applicable.
  • I have followed the project guidelines.
  • I have fixed any lint issues.

Related Issues

Follows up on #6833 (Escape key to dismiss suggestions).

Copilot AI review requested due to automatic review settings March 6, 2026 18:12
@bdbch bdbch self-assigned this Mar 6, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 6, 2026

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit 53c5ae1
🔍 Latest deploy log https://app.netlify.com/projects/tiptap-embed/deploys/69c7e0476869260008f35ff9
😎 Deploy Preview https://deploy-preview-7570--tiptap-embed.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 53c5ae1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 72 packages
Name Type
@tiptap/suggestion Patch
@tiptap/extension-emoji Patch
@tiptap/extension-mention Patch
@tiptap/core Patch
@tiptap/extension-audio Patch
@tiptap/extension-blockquote Patch
@tiptap/extension-bold Patch
@tiptap/extension-bubble-menu Patch
@tiptap/extension-bullet-list Patch
@tiptap/extension-code-block-lowlight Patch
@tiptap/extension-code-block Patch
@tiptap/extension-code Patch
@tiptap/extension-collaboration-caret Patch
@tiptap/extension-collaboration Patch
@tiptap/extension-color Patch
@tiptap/extension-details Patch
@tiptap/extension-document Patch
@tiptap/extension-drag-handle-react Patch
@tiptap/extension-drag-handle-vue-2 Patch
@tiptap/extension-drag-handle-vue-3 Patch
@tiptap/extension-drag-handle Patch
@tiptap/extension-file-handler Patch
@tiptap/extension-floating-menu Patch
@tiptap/extension-font-family Patch
@tiptap/extension-hard-break Patch
@tiptap/extension-heading Patch
@tiptap/extension-highlight Patch
@tiptap/extension-horizontal-rule Patch
@tiptap/extension-image Patch
@tiptap/extension-invisible-characters Patch
@tiptap/extension-italic Patch
@tiptap/extension-link Patch
@tiptap/extension-list Patch
@tiptap/extension-mathematics Patch
@tiptap/extension-node-range Patch
@tiptap/extension-ordered-list Patch
@tiptap/extension-paragraph Patch
@tiptap/extension-strike Patch
@tiptap/extension-subscript Patch
@tiptap/extension-superscript Patch
@tiptap/extension-table-of-contents Patch
@tiptap/extension-table Patch
@tiptap/extension-text-align Patch
@tiptap/extension-text-style Patch
@tiptap/extension-text Patch
@tiptap/extension-twitch Patch
@tiptap/extension-typography Patch
@tiptap/extension-underline Patch
@tiptap/extension-unique-id Patch
@tiptap/extension-youtube Patch
@tiptap/extensions Patch
@tiptap/html Patch
@tiptap/markdown Patch
@tiptap/pm Patch
@tiptap/react Patch
@tiptap/starter-kit Patch
@tiptap/static-renderer Patch
@tiptap/vue-2 Patch
@tiptap/vue-3 Patch
@tiptap/extension-character-count Patch
@tiptap/extension-dropcursor Patch
@tiptap/extension-focus Patch
@tiptap/extension-gapcursor Patch
@tiptap/extension-history Patch
@tiptap/extension-list-item Patch
@tiptap/extension-list-keymap Patch
@tiptap/extension-placeholder Patch
@tiptap/extension-table-cell Patch
@tiptap/extension-table-header Patch
@tiptap/extension-table-row Patch
@tiptap/extension-task-item Patch
@tiptap/extension-task-list Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes @tiptap/suggestion dismissal behavior so exiting a suggestion (Escape / exitSuggestion) suppresses re-activation while the user continues typing in the same trigger context.

Changes:

  • Track dismissed trigger position in plugin state (dismissedFrom) and suppress activation when the match is in the same word.
  • Add whitespace-detection helper to clear dismissal when the user inserts whitespace/newlines.
  • Add unit tests for dismissal scenarios and a patch changeset entry.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/suggestion/src/suggestion.ts Adds dismissedFrom state + activation suppression logic; introduces hasInsertedWhitespace helper.
packages/suggestion/src/tests/suggestion.test.ts Adds integration tests covering dismissal persistence and re-activation scenarios.
.changeset/fix-suggestion-dismissed-state-calm-river-flow.md Patch changeset documenting the user-visible fix.

Comment thread packages/suggestion/src/suggestion.ts
Comment thread packages/suggestion/src/suggestion.ts Outdated
Comment thread packages/suggestion/src/suggestion.ts
Comment thread packages/suggestion/src/__tests__/suggestion.test.ts
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 6, 2026

Open in StackBlitz

@tiptap/extension-character-count

npm i https://pkg.pr.new/@tiptap/extension-character-count@7570

@tiptap/extension-dropcursor

npm i https://pkg.pr.new/@tiptap/extension-dropcursor@7570

@tiptap/extension-focus

npm i https://pkg.pr.new/@tiptap/extension-focus@7570

@tiptap/extension-gapcursor

npm i https://pkg.pr.new/@tiptap/extension-gapcursor@7570

@tiptap/extension-list-item

npm i https://pkg.pr.new/@tiptap/extension-list-item@7570

@tiptap/extension-history

npm i https://pkg.pr.new/@tiptap/extension-history@7570

@tiptap/extension-list-keymap

npm i https://pkg.pr.new/@tiptap/extension-list-keymap@7570

@tiptap/extension-placeholder

npm i https://pkg.pr.new/@tiptap/extension-placeholder@7570

@tiptap/extension-table-header

npm i https://pkg.pr.new/@tiptap/extension-table-header@7570

@tiptap/extension-table-cell

npm i https://pkg.pr.new/@tiptap/extension-table-cell@7570

@tiptap/extension-table-row

npm i https://pkg.pr.new/@tiptap/extension-table-row@7570

@tiptap/extension-task-item

npm i https://pkg.pr.new/@tiptap/extension-task-item@7570

@tiptap/extension-task-list

npm i https://pkg.pr.new/@tiptap/extension-task-list@7570

@tiptap/extension-audio

npm i https://pkg.pr.new/@tiptap/extension-audio@7570

@tiptap/core

npm i https://pkg.pr.new/@tiptap/core@7570

@tiptap/extension-bubble-menu

npm i https://pkg.pr.new/@tiptap/extension-bubble-menu@7570

@tiptap/extension-bold

npm i https://pkg.pr.new/@tiptap/extension-bold@7570

@tiptap/extension-blockquote

npm i https://pkg.pr.new/@tiptap/extension-blockquote@7570

@tiptap/extension-code

npm i https://pkg.pr.new/@tiptap/extension-code@7570

@tiptap/extension-bullet-list

npm i https://pkg.pr.new/@tiptap/extension-bullet-list@7570

@tiptap/extension-code-block

npm i https://pkg.pr.new/@tiptap/extension-code-block@7570

@tiptap/extension-collaboration

npm i https://pkg.pr.new/@tiptap/extension-collaboration@7570

@tiptap/extension-code-block-lowlight

npm i https://pkg.pr.new/@tiptap/extension-code-block-lowlight@7570

@tiptap/extension-collaboration-caret

npm i https://pkg.pr.new/@tiptap/extension-collaboration-caret@7570

@tiptap/extension-color

npm i https://pkg.pr.new/@tiptap/extension-color@7570

@tiptap/extension-details

npm i https://pkg.pr.new/@tiptap/extension-details@7570

@tiptap/extension-drag-handle

npm i https://pkg.pr.new/@tiptap/extension-drag-handle@7570

@tiptap/extension-document

npm i https://pkg.pr.new/@tiptap/extension-document@7570

@tiptap/extension-drag-handle-react

npm i https://pkg.pr.new/@tiptap/extension-drag-handle-react@7570

@tiptap/extension-drag-handle-vue-2

npm i https://pkg.pr.new/@tiptap/extension-drag-handle-vue-2@7570

@tiptap/extension-emoji

npm i https://pkg.pr.new/@tiptap/extension-emoji@7570

@tiptap/extension-drag-handle-vue-3

npm i https://pkg.pr.new/@tiptap/extension-drag-handle-vue-3@7570

@tiptap/extension-file-handler

npm i https://pkg.pr.new/@tiptap/extension-file-handler@7570

@tiptap/extension-floating-menu

npm i https://pkg.pr.new/@tiptap/extension-floating-menu@7570

@tiptap/extension-font-family

npm i https://pkg.pr.new/@tiptap/extension-font-family@7570

@tiptap/extension-heading

npm i https://pkg.pr.new/@tiptap/extension-heading@7570

@tiptap/extension-hard-break

npm i https://pkg.pr.new/@tiptap/extension-hard-break@7570

@tiptap/extension-highlight

npm i https://pkg.pr.new/@tiptap/extension-highlight@7570

@tiptap/extension-horizontal-rule

npm i https://pkg.pr.new/@tiptap/extension-horizontal-rule@7570

@tiptap/extension-image

npm i https://pkg.pr.new/@tiptap/extension-image@7570

@tiptap/extension-invisible-characters

npm i https://pkg.pr.new/@tiptap/extension-invisible-characters@7570

@tiptap/extension-italic

npm i https://pkg.pr.new/@tiptap/extension-italic@7570

@tiptap/extension-link

npm i https://pkg.pr.new/@tiptap/extension-link@7570

@tiptap/extension-list

npm i https://pkg.pr.new/@tiptap/extension-list@7570

@tiptap/extension-mathematics

npm i https://pkg.pr.new/@tiptap/extension-mathematics@7570

@tiptap/extension-mention

npm i https://pkg.pr.new/@tiptap/extension-mention@7570

@tiptap/extension-node-range

npm i https://pkg.pr.new/@tiptap/extension-node-range@7570

@tiptap/extension-ordered-list

npm i https://pkg.pr.new/@tiptap/extension-ordered-list@7570

@tiptap/extension-paragraph

npm i https://pkg.pr.new/@tiptap/extension-paragraph@7570

@tiptap/extension-subscript

npm i https://pkg.pr.new/@tiptap/extension-subscript@7570

@tiptap/extension-strike

npm i https://pkg.pr.new/@tiptap/extension-strike@7570

@tiptap/extension-superscript

npm i https://pkg.pr.new/@tiptap/extension-superscript@7570

@tiptap/extension-table

npm i https://pkg.pr.new/@tiptap/extension-table@7570

@tiptap/extension-text

npm i https://pkg.pr.new/@tiptap/extension-text@7570

@tiptap/extension-table-of-contents

npm i https://pkg.pr.new/@tiptap/extension-table-of-contents@7570

@tiptap/extension-text-align

npm i https://pkg.pr.new/@tiptap/extension-text-align@7570

@tiptap/extension-text-style

npm i https://pkg.pr.new/@tiptap/extension-text-style@7570

@tiptap/extension-twitch

npm i https://pkg.pr.new/@tiptap/extension-twitch@7570

@tiptap/extension-underline

npm i https://pkg.pr.new/@tiptap/extension-underline@7570

@tiptap/extension-unique-id

npm i https://pkg.pr.new/@tiptap/extension-unique-id@7570

@tiptap/extension-typography

npm i https://pkg.pr.new/@tiptap/extension-typography@7570

@tiptap/extension-youtube

npm i https://pkg.pr.new/@tiptap/extension-youtube@7570

@tiptap/html

npm i https://pkg.pr.new/@tiptap/html@7570

@tiptap/extensions

npm i https://pkg.pr.new/@tiptap/extensions@7570

@tiptap/markdown

npm i https://pkg.pr.new/@tiptap/markdown@7570

@tiptap/react

npm i https://pkg.pr.new/@tiptap/react@7570

@tiptap/pm

npm i https://pkg.pr.new/@tiptap/pm@7570

@tiptap/starter-kit

npm i https://pkg.pr.new/@tiptap/starter-kit@7570

@tiptap/static-renderer

npm i https://pkg.pr.new/@tiptap/static-renderer@7570

@tiptap/suggestion

npm i https://pkg.pr.new/@tiptap/suggestion@7570

@tiptap/vue-2

npm i https://pkg.pr.new/@tiptap/vue-2@7570

@tiptap/vue-3

npm i https://pkg.pr.new/@tiptap/vue-3@7570

commit: a6d2c40

@oBusk
Copy link
Copy Markdown

oBusk commented Mar 7, 2026

Will this implementation mean that adding/removing any character before the trigger. And then editing after the trigger again will activate it again? Maybe an acceptable compromise.

Are you also testing with allowSpaces: true?

@bdbch
Copy link
Copy Markdown
Member Author

bdbch commented Mar 7, 2026

This implementation is dismissing the mention state until you leave the mention via a whitespace or newline character or you move the selection out of the suggestion range. That means, if you insert characters right before your mention and go back in, you'll have the mention state again which I think is fair.

I think having support for allowSpaces makes it tricky to restore the suggestion state again except maybe using a newline.

If you want I could add an escape hatch to give you more control over when you restore the mention state again, but then you'd need to handle the logic yourself if the default isn't fitting your needs.

@oBusk
Copy link
Copy Markdown

oBusk commented Mar 9, 2026

I was considering how to solve it with allowSpaces, and there was some mentions of tracking the triggering position to identify "already closed", but the fact that something that could cause that trigger to move would reset and begins showing suggestions again made it slightly flimsy. I was considering if there could be some decoration on the triggering char to mark it as "consumed", but there might be other flimsyness with that.

Since we want allowSpaces for our usecase, I think the most reliable we can do is to try to trigger suggestions based on keydown rather than text content, but I understand that would mean we can't really use tiptap/suggestion to accomplish that.

This PR will definely make escape work more like you expect, especially without allowSpaces, so that's great! Could you clarify what control the escapehatch would give me? Are you saying I call some function to "reactivate" mention state manually?

@bdbch bdbch changed the base branch from develop to main March 14, 2026 15:04
@gethari
Copy link
Copy Markdown
Contributor

gethari commented Mar 25, 2026

Hi @bdbch any updates on this please ? Are we moving forward with this fix, considering allowSpaces will work smoothly after this ?

Copy link
Copy Markdown
Contributor

@arnaugomez arnaugomez left a comment

Choose a reason for hiding this comment

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

Looks good to me. However, I wonder if storing the position means that, if there is a collaborative transaction or another transaction that modifying the document, the stored positions would become invalid. Perhaps you can leverage the position utility to avoid this edge case.

@bdbch
Copy link
Copy Markdown
Member Author

bdbch commented Mar 28, 2026

Pushed a small follow-up: now maps with the transaction, and I added a newline regression test too. Should cover the review concern a bit better now.

@bdbch
Copy link
Copy Markdown
Member Author

bdbch commented Mar 28, 2026

@oBusk pushed an update here: Escape now clears the decoration properly, allowSpaces dismissal is handled, and there is a small shouldResetDismissed escape hatch for custom reset logic too.

@bdbch
Copy link
Copy Markdown
Member Author

bdbch commented Mar 28, 2026

Added documentation for the escape hatch function here:
ueberdosis/tiptap-docs#710

@bdbch bdbch requested a review from arnaugomez March 28, 2026 13:59
@bdbch bdbch force-pushed the fix/suggestion-keep-dismissed-state branch from 028d953 to 53c5ae1 Compare March 28, 2026 14:05
@oBusk
Copy link
Copy Markdown

oBusk commented Mar 31, 2026

@oBusk pushed an update here: Escape now clears the decoration properly, allowSpaces dismissal is handled, and there is a small shouldResetDismissed escape hatch for custom reset logic too.

@bdbch sounds good! I will see if i time to try it out in it's current state tmrw

@bdbch bdbch merged commit 7f6d63c into main Mar 31, 2026
17 checks passed
@bdbch bdbch deleted the fix/suggestion-keep-dismissed-state branch March 31, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants