Skip to content

fix: prevent cursor jump during IME composition in colored text#7626

Open
semimikoh wants to merge 2 commits intoueberdosis:mainfrom
semimikoh:fix/ime-cursor-jump-text-style
Open

fix: prevent cursor jump during IME composition in colored text#7626
semimikoh wants to merge 2 commits intoueberdosis:mainfrom
semimikoh:fix/ime-cursor-jump-text-style

Conversation

@semimikoh
Copy link

@semimikoh semimikoh commented Mar 19, 2026

When typing with IME (Korean/Chinese/Japanese) before the last character
of colored text, the cursor would jump to the end of the paragraph.

Root cause: ProseMirror's DOMSerializer sets styles via style.cssText,
which causes Chrome to normalize hex colors (#FF0000) to rgb format
(rgb(255, 0, 0)). During IME composition, this mismatch between the
stored mark attributes and re-parsed DOM values causes findDiff to
detect the entire mark region as changed, triggering a full replacement
that misplaces the cursor.

Fix: Add a custom markView to TextStyle that uses setAttribute instead
of cssText, preserving the original color format. Also filter empty
strings in parseHTML and injectExtensionAttributesToParseRule to prevent
additional attribute mismatches.

Closes #7621

Changes Overview

Fix cursor jumping to end of paragraph when typing with IME (Korean/Chinese/Japanese) before the last character of text styled with
color/textStyle marks.

  • Added addMarkView to TextStyle extension that uses setAttribute instead of style.cssText
  • Added empty string filtering in parseHTML for color, backgroundColor, fontFamily, fontSize, lineHeight
  • Added empty string check in injectExtensionAttributesToParseRule

Implementation Approach

Root cause: ProseMirror's DOMSerializer sets styles via dom.style.cssText, which causes Chrome to normalize hex colors (#FF0000) to
rgb (rgb(255, 0, 0)). During IME composition, MarkViewDesc becomes CONTENT_DIRTY and falls back to re-parsing the DOM — reading the
normalized rgb value instead of the original hex. This mismatch makes findDiff detect the entire mark region as changed, triggering a
full-range replacement where Chrome's composition guard blocks correct cursor positioning.

Fix: A custom addMarkView creates the span using span.setAttribute('style', ...) which preserves the original color format, preventing
the hex→rgb normalization.

Alternatives considered:

  • Normalize colors to rgb at storage time — loses user's original format, wider change scope
  • Patch ProseMirror's DOMSerializer upstream — maintainer is conservative about changes
  • Set inclusive: false on textStyle mark — causes side effects (new text won't inherit style at mark boundary)

Testing Done

  • Manually tested IME input (Korean) before last character of colored text — cursor stays in place
  • Manually tested IME input in middle of colored text — works normally
  • Manually tested non-IME input with color/font-family/font-size — works as before
  • All 684 unit tests pass

Verification Steps

  1. Open demo with TextStyle/Color extensions
  2. Select text and apply a color
  3. Place cursor before the last character of the colored text
  4. Type with IME (Korean/Chinese/Japanese input method)
  5. Before fix: cursor jumps to end of paragraph after composition
  6. After fix: cursor stays at the correct position

Additional Notes

Debug logging confirmed the issue — during IME composition, findDiff returns {start: 410, endA: 429, endB: 429} (endA==endB means
mark-only change), replacing the entire colored range instead of just the inserted text. This only happens in Chrome due to cssText
normalization.

Open to feedback if there's a better approach!

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

Closes #7621

  When typing with IME (Korean/Chinese/Japanese) before the last character
  of colored text, the cursor would jump to the end of the paragraph.

  Root cause: ProseMirror's DOMSerializer sets styles via style.cssText,
  which causes Chrome to normalize hex colors (#FF0000) to rgb format
  (rgb(255, 0, 0)). During IME composition, this mismatch between the
  stored mark attributes and re-parsed DOM values causes findDiff to
  detect the entire mark region as changed, triggering a full replacement
  that misplaces the cursor.

  Fix: Add a custom markView to TextStyle that uses setAttribute instead
  of cssText, preserving the original color format. Also filter empty
  strings in parseHTML and injectExtensionAttributesToParseRule to prevent
  additional attribute mismatches.

  Closes ueberdosis#7621
@netlify
Copy link

netlify bot commented Mar 19, 2026

Deploy Preview for tiptap-embed ready!

Name Link
🔨 Latest commit de4cbae
🔍 Latest deploy log https://app.netlify.com/projects/tiptap-embed/deploys/69bb58634c96330008f121b2
😎 Deploy Preview https://deploy-preview-7626--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

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: de4cbae

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

This PR includes changesets to release 72 packages
Name Type
@tiptap/extension-text-style Patch
@tiptap/core Patch
@tiptap/extension-color Patch
@tiptap/extension-details Patch
@tiptap/extension-file-handler Patch
@tiptap/extension-font-family Patch
@tiptap/extension-invisible-characters Patch
@tiptap/extension-audio Patch
@tiptap/extension-blockquote Patch
@tiptap/extension-bold Patch
@tiptap/extension-bubble-menu 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-document Patch
@tiptap/extension-drag-handle Patch
@tiptap/extension-emoji Patch
@tiptap/extension-floating-menu 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-italic Patch
@tiptap/extension-link Patch
@tiptap/extension-list Patch
@tiptap/extension-mathematics Patch
@tiptap/extension-mention Patch
@tiptap/extension-node-range 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 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/react Patch
@tiptap/starter-kit Patch
@tiptap/static-renderer Patch
@tiptap/suggestion Patch
@tiptap/vue-2 Patch
@tiptap/vue-3 Patch
@tiptap/extension-drag-handle-react Patch
@tiptap/extension-drag-handle-vue-2 Patch
@tiptap/extension-drag-handle-vue-3 Patch
@tiptap/extension-bullet-list Patch
@tiptap/extension-ordered-list Patch
@tiptap/extension-list-item Patch
@tiptap/extension-list-keymap Patch
@tiptap/extension-task-item Patch
@tiptap/extension-task-list Patch
@tiptap/extension-table-cell Patch
@tiptap/extension-table-header Patch
@tiptap/extension-table-row Patch
@tiptap/extension-character-count Patch
@tiptap/extension-dropcursor Patch
@tiptap/extension-focus Patch
@tiptap/extension-gapcursor Patch
@tiptap/extension-history Patch
@tiptap/extension-placeholder Patch
@tiptap/pm 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

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.

The cursor will jump when typing text before the end of colored text using an input method

1 participant