Skip to content

Commit 5684726

Browse files
committed
fix(editor): cancel focus-scroll RAF on intentional scroll
Store the RAF handle from wrapHiddenEditorFocus on the instance and cancel it when scrollToPosition is called. Prevents the safety net from undoing intentional scrolls like search navigation.
1 parent dbce0f6 commit 5684726

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ export class PresentationEditor extends EventEmitter {
284284
#errorBannerMessage: HTMLElement | null = null;
285285
#renderScheduled = false;
286286
#pendingDocChange = false;
287+
#focusScrollRafId: number | null = null;
287288
#pendingMapping: Mapping | null = null;
288289
#isRerendering = false;
289290
#selectionSync = new SelectionSyncCoordinator();
@@ -747,7 +748,13 @@ export class PresentationEditor extends EventEmitter {
747748
// Safety net: the browser may asynchronously scroll after ProseMirror's
748749
// selectionToDOM() modifies the DOM selection inside the hidden editor.
749750
// A single requestAnimationFrame catches this post-layout scroll.
750-
win.requestAnimationFrame(() => {
751+
// The RAF ID is stored so scrollToPosition() can cancel it — otherwise
752+
// intentional scrolls (e.g. search navigation) would be undone.
753+
if (this.#focusScrollRafId != null) {
754+
win.cancelAnimationFrame(this.#focusScrollRafId);
755+
}
756+
this.#focusScrollRafId = win.requestAnimationFrame(() => {
757+
this.#focusScrollRafId = null;
751758
if (win.scrollX !== beforeX || win.scrollY !== beforeY) {
752759
win.scrollTo(beforeX, beforeY);
753760
}
@@ -2021,6 +2028,14 @@ export class PresentationEditor extends EventEmitter {
20212028
pos: number,
20222029
options: { block?: 'start' | 'center' | 'end' | 'nearest'; behavior?: ScrollBehavior } = {},
20232030
): boolean {
2031+
// Cancel any pending focus-scroll RAF so this intentional scroll is not undone
2032+
// by the wrapHiddenEditorFocus safety net (e.g. search navigation after focus).
2033+
if (this.#focusScrollRafId != null) {
2034+
const win = this.#visibleHost.ownerDocument?.defaultView;
2035+
if (win) win.cancelAnimationFrame(this.#focusScrollRafId);
2036+
this.#focusScrollRafId = null;
2037+
}
2038+
20242039
const activeEditor = this.getActiveEditor();
20252040
const doc = activeEditor?.state?.doc;
20262041
if (!doc) return false;

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,23 @@ describe('PresentationEditor - Focus Wrapping (#wrapHiddenEditorFocus)', () => {
371371
expect(rafSpy).toHaveBeenCalledTimes(1);
372372
expect(rafSpy).toHaveBeenCalledWith(expect.any(Function));
373373
});
374+
375+
it('cancels focus-scroll RAF when scrollToPosition is called', () => {
376+
editor = new PresentationEditor({
377+
element: container,
378+
documentId: 'test-doc',
379+
pageSize: { w: 612, h: 792 },
380+
});
381+
382+
const cancelSpy = vi.spyOn(window, 'cancelAnimationFrame');
383+
384+
editor.editor.view.focus();
385+
386+
// scrollToPosition will fail (no layout) but should still cancel the RAF
387+
editor.scrollToPosition(0);
388+
389+
expect(cancelSpy).toHaveBeenCalled();
390+
});
374391
});
375392

376393
describe('mock detection', () => {

0 commit comments

Comments
 (0)