* fix(chat): defer composer value writeback during IME composition The controlled-writeback effect compared the value prop against the CodeMirror document and, on mismatch, dispatched a wholesale replacement with the caret forced to the end. While the browser composes (pinyin, kana, hangul) the uncommitted text lives in the DOM, not in the document, so the mismatch is expected and the dispatch interrupted the IME session and jumped the cursor. Skip the writeback while the view is composing, using CodeMirror's public compositionStarted getter; the composition commits through its own pipeline and reports via onChange. Fixes #2527 * fix(chat): preserve external composer writes during IME * fix(chat): restore composition-wide writeback guard --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
7ef6441bf3
commit
1c76dbefe4
@@ -344,6 +344,10 @@ export const ComposerEditor = React.forwardRef<ComposerEditorHandle, ComposerEdi
|
||||
if (!view) return;
|
||||
const current = view.state.doc.toString();
|
||||
if (current === value) return;
|
||||
// Skip every controlled writeback while the browser is composing.
|
||||
// A stale value echo can differ from CodeMirror's newer document,
|
||||
// and replacing it would interrupt the IME session and move the caret.
|
||||
if (view.compositionStarted) return;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: current.length, insert: value },
|
||||
// An external rewrite (draft restore, history navigation,
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const composerEditorSource = readFileSync(
|
||||
new URL('../ComposerEditor.tsx', import.meta.url),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const writebackEffect = (): string => {
|
||||
const start = composerEditorSource.indexOf('// Controlled value:');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const end = composerEditorSource.indexOf('}, [value]);', start);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return composerEditorSource.slice(start, end);
|
||||
};
|
||||
|
||||
describe('composer value writeback composition guard (issue #2527)', () => {
|
||||
test('checks equality, then composition, before dispatching', () => {
|
||||
const effect = writebackEffect();
|
||||
const equalityCheck = effect.indexOf('if (current === value) return;');
|
||||
const compositionGuard = effect.indexOf('if (view.compositionStarted) return;');
|
||||
const dispatch = effect.indexOf('view.dispatch({');
|
||||
|
||||
expect(equalityCheck).toBeGreaterThan(-1);
|
||||
expect(compositionGuard).toBeGreaterThan(equalityCheck);
|
||||
expect(dispatch).toBeGreaterThan(compositionGuard);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user