Skip to content

Commit 88c0773

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 efd8b42 commit 88c0773

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
@@ -286,6 +286,7 @@ export class PresentationEditor extends EventEmitter {
286286
#errorBannerMessage: HTMLElement | null = null;
287287
#renderScheduled = false;
288288
#pendingDocChange = false;
289+
#focusScrollRafId: number | null = null;
289290
#pendingMapping: Mapping | null = null;
290291
#isRerendering = false;
291292
#selectionSync = new SelectionSyncCoordinator();
@@ -779,7 +780,13 @@ export class PresentationEditor extends EventEmitter {
779780
// Safety net: the browser may asynchronously scroll after ProseMirror's
780781
// selectionToDOM() modifies the DOM selection inside the hidden editor.
781782
// A single requestAnimationFrame catches this post-layout scroll.
782-
win.requestAnimationFrame(() => {
783+
// The RAF ID is stored so scrollToPosition() can cancel it — otherwise
784+
// intentional scrolls (e.g. search navigation) would be undone.
785+
if (this.#focusScrollRafId != null) {
786+
win.cancelAnimationFrame(this.#focusScrollRafId);
787+
}
788+
this.#focusScrollRafId = win.requestAnimationFrame(() => {
789+
this.#focusScrollRafId = null;
783790
if (win.scrollX !== beforeX || win.scrollY !== beforeY) {
784791
win.scrollTo(beforeX, beforeY);
785792
}
@@ -2157,6 +2164,14 @@ export class PresentationEditor extends EventEmitter {
21572164
pos: number,
21582165
options: { block?: 'start' | 'center' | 'end' | 'nearest'; behavior?: ScrollBehavior } = {},
21592166
): boolean {
2167+
// Cancel any pending focus-scroll RAF so this intentional scroll is not undone
2168+
// by the wrapHiddenEditorFocus safety net (e.g. search navigation after focus).
2169+
if (this.#focusScrollRafId != null) {
2170+
const win = this.#visibleHost.ownerDocument?.defaultView;
2171+
if (win) win.cancelAnimationFrame(this.#focusScrollRafId);
2172+
this.#focusScrollRafId = null;
2173+
}
2174+
21602175
const activeEditor = this.getActiveEditor();
21612176
const doc = activeEditor?.state?.doc;
21622177
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)