Skip to content

Commit c315599

Browse files
authored
fix(super-editor): prevent cursor jump when changing font from toolbar (#2468)
When the user selects a font from the toolbar dropdown, the hidden ProseMirror editor loses focus. On re-focus, the browser places the DOM selection at an arbitrary position inside the off-screen contenteditable (left: -9999px). ProseMirror's DOMObserver reads this stale position via a selectionchange event and overwrites PM state, causing the cursor to jump to position 2 (near the top of the document). The wrapped focus in PresentationEditor only called view.dom.focus() without calling selectionToDOM(), unlike ProseMirror's original focus() which stops the observer, focuses, syncs the selection, then restarts. Fix: call view.domObserver.suppressSelectionUpdates() after focusing when the editor was not previously focused. This tells PM to re-apply its own selection to the DOM instead of reading the stale browser position.
1 parent 5eca65b commit c315599

3 files changed

Lines changed: 23 additions & 0 deletions

File tree

packages/super-editor/src/core/presentation-editor/PresentationEditor.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,7 @@ export class PresentationEditor extends EventEmitter {
753753

754754
const beforeX = win.scrollX;
755755
const beforeY = win.scrollY;
756+
const alreadyFocused = view.hasFocus();
756757
let focused = false;
757758

758759
// Strategy 1: Try focus with preventScroll option (modern browsers)
@@ -791,6 +792,16 @@ export class PresentationEditor extends EventEmitter {
791792
}
792793
}
793794

795+
// When the editor was not focused before, the browser places the DOM selection
796+
// at an arbitrary position inside the off-screen contenteditable. ProseMirror's
797+
// DOMObserver would read this stale position via a selectionchange event and
798+
// overwrite PM state, causing the cursor to jump. Suppress selection updates
799+
// for the next 50ms so PM re-applies its own selection to the DOM instead.
800+
if (!alreadyFocused) {
801+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
802+
(view as any).domObserver.suppressSelectionUpdates();
803+
}
804+
794805
// Restore scroll position if any focus attempt changed it
795806
if (win.scrollX !== beforeX || win.scrollY !== beforeY) {
796807
win.scrollTo(beforeX, beforeY);

packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.draggableFocus.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,12 @@ vi.mock('../../Editor.js', () => {
118118
focus: function () {
119119
// Plain function that can be wrapped
120120
},
121+
hasFocus: function () {
122+
return domElement === domElement.ownerDocument.activeElement;
123+
},
124+
domObserver: {
125+
suppressSelectionUpdates: vi.fn(),
126+
},
121127
dispatch: vi.fn(),
122128
},
123129
options: {

packages/super-editor/src/core/presentation-editor/tests/PresentationEditor.focusWrapping.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ vi.mock('../../Editor.js', () => {
133133
focus: function () {
134134
// Plain function that can be wrapped
135135
},
136+
hasFocus: function () {
137+
return domElement === domElement.ownerDocument.activeElement;
138+
},
139+
domObserver: {
140+
suppressSelectionUpdates: vi.fn(),
141+
},
136142
dispatch: vi.fn(),
137143
},
138144
options: {

0 commit comments

Comments
 (0)