Skip to content

fix(presentation-editor): bail on clicks in inter-page gap (SD-2749)#3181

Merged
tupizz merged 4 commits intomainfrom
tadeu/sd-2749-bug-clicking-near-page-border-jumps-documents-to-different
May 6, 2026
Merged

fix(presentation-editor): bail on clicks in inter-page gap (SD-2749)#3181
tupizz merged 4 commits intomainfrom
tadeu/sd-2749-bug-clicking-near-page-border-jumps-documents-to-different

Conversation

@tupizz
Copy link
Copy Markdown
Contributor

@tupizz tupizz commented May 6, 2026

Summary

  • Fixes SD-2749: clicking in the vertical gap between two pages caused the viewer to jump to a different page.
  • One guard line added to EditorInputManager.#handlePointerDown that bails when no .superdoc-page is under the cursor (matches the existing in-margin guard from SD-2356).
  • Defensive layout?.pages?.[pageIndex] access in isOutsidePageBodyContent to prevent a latent crash when layout.pages is missing.

Root cause

PointerNormalization uses elementsFromPoint to find the .superdoc-page under the cursor. In the inter-page gap there is none, so it returns pageIndex: undefined. The existing isOutsidePageBodyContent guard from SD-2356 short-circuits to false when pageIndex is not finite, so the pointer flow fell through to the geometry fallback (clickToPositionGeometrysnapToNearestFragment), which always returns some nearest fragment. TextSelection.create + ProseMirror scrollIntoView then jumped the page.

clickToPositionDom        → null (no DOM hit)
findPageElement           → null (no page under cursor)
clickToPositionGeometry   → hitTestPage returns nearest page by center distance
  → hitTestFragment       → null
  → snapToNearestFragment → returns fragment closest to the click
→ TextSelection.create    → editor.view.dispatch
→ ProseMirror scrollIntoView → page jumps

The fix is symmetric with SD-2356: clicking in the gap is conceptually the same as clicking in a page margin — preserve selection and scroll.

Files changed

File Change
EditorInputManager.ts Extended pointerdown guard to bail when pageIndex is undefined; added defensive layout?.pages?.[pageIndex]
EditorInputManager.pageMarginClick.test.ts New regression test for SD-2749 (RED → GREEN)
EditorInputManager.{activeCommentClick,dragAutoScroll,footnoteClick,structuredContent}.test.ts Mocks updated to return realistic { x, y, pageIndex, pageLocalY } matching the production contract
PresentationEditor.test.ts Two integration tests stub document.elementsFromPoint so happy-dom returns the simulated .superdoc-page under the click point

Test plan

  • pnpm --filter super-editor test — 18,421/18,421 pass (1,240 files)
  • Targeted: EditorInputManager.pageMarginClick.test.ts (3 tests pass; new SD-2749 test fails on main, passes on this branch)
  • Browser end-to-end with IT-923 Headers.docx (7 pages):
    • Gap click at (600, 366) between pages 0 & 1: scrollTop and selection.from UNCHANGED
    • In-page text click at (600, 500): selection moves to clicked text, no scroll jump
  • Manual confirmation in Word-style customer doc (NVCA-Model-IRA.docx)
  • Manual confirmation that the existing SD-2356 fix (margin clicks) still works

Clicking in the vertical gap between two pages caused the viewer to jump to
a different page. PointerNormalization returns pageIndex=undefined when
elementsFromPoint finds no .superdoc-page under the cursor (the gap area),
but the existing isOutsidePageBodyContent guard from SD-2356 only fires when
pageIndex is finite. The pointer flow then fell through to the geometry
fallback, which always snaps to the nearest fragment, moving the selection
and triggering a scrollIntoView.

Treat "no page under cursor" the same as "click in page margin": prevent
default, focus the editor, do not change selection or scroll. Also harden
isOutsidePageBodyContent against a missing layout.pages array.

Tests:
- New regression test in EditorInputManager.pageMarginClick.test.ts asserts
  that a click with undefined pageIndex does not call into the position
  resolver or set a selection.
- Existing pointer tests had incomplete normalizeClientPoint mocks that did
  not return pageIndex. Updated them to mirror the production contract so
  in-page click paths still exercise position resolution.
- Two PresentationEditor integration tests stub elementsFromPoint to
  simulate a .superdoc-page under the click point, since happy-dom does
  not compute layout.

Verified end-to-end: clicking in the 24px gap between pages no longer
moves selection or scroll, while in-page text clicks still place the
caret normally.
@linear
Copy link
Copy Markdown

linear Bot commented May 6, 2026

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tupizz tupizz self-assigned this May 6, 2026
@tupizz tupizz requested a review from luccas-harbour May 6, 2026 15:24
… H/F (SD-2749)

After double-clicking a header/footer to enter editing mode and then
clicking back into a body paragraph, ProseMirror was dispatching the new
selection on the now-stale active (header/footer) editor. With the H/F
surface scrolled out of view, ProseMirror's scrollIntoView pulled the
viewport back to the H/F surface, jumping the user away from where they
clicked.

The captured `sessionMode`, `useActiveSurfaceHitTest`, and `editor`
locals in #handlePointerDown were captured before #handleClickInHeaderFooterMode
exited the session for a body-content click. Re-derive them from the
fresh getHeaderFooterSession() reading once the H/F handler returns false
(meaning it didn't claim the click), so subsequent hit testing and
selection dispatch target the body editor.

Adds EditorInputManager.headerFooterBodyClick.test.ts covering the
regression. Verified end-to-end in the browser: scrollTop and selection
are preserved when clicking body after editing a header.
@tupizz
Copy link
Copy Markdown
Contributor Author

tupizz commented May 6, 2026

Follow-up: extended fix for the header→body click variant of SD-2749.

Repro confirmed on origin/main (and on previous commit on this branch):

  • dblclick header → enters editing session
  • type some text in header
  • scroll down a few pages
  • click in middle of any body paragraph
  • scrollTop jumps from 3500 → 97 (page snaps back to where the header is)

Root cause: #handlePointerDown captured sessionMode, useActiveSurfaceHitTest, and editor locals at function start. When #handleClickInHeaderFooterMode exits the session for a bodyContent click and returns false, those locals stayed stale. Selection then got dispatched on the header editor → ProseMirror's scrollIntoView pulled the viewport back to the (now off-screen) header.

Fix (commit 57c46e5): convert the three locals to let and re-derive them from a fresh getHeaderFooterSession() read after the H/F handler returns. If the session is now 'body' and we're not in a note session, point editor at bodyEditor and recompute useActiveSurfaceHitTest.

Tests added EditorInputManager.headerFooterBodyClick.test.ts with one regression test asserting that body view.dispatch is called and header view.dispatch is NOT called for the post-H/F body click.

Verified end-to-end in the browser:

Scenario Before fix After fix
Dblclick header → edit → scroll → body click scrollTop 3500 → 97 (-3403) scrollTop unchanged (delta 0)
Gap click between pages (original SD-2749) (already fixed) scrollTop unchanged ✅
Normal in-page text click works selection moves correctly ✅

Full pnpm --filter super-editor test: 18,422 / 18,422 passing.

@tupizz tupizz marked this pull request as ready for review May 6, 2026 16:26
@tupizz tupizz requested a review from a team as a code owner May 6, 2026 16:26
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

(document as unknown as { elementsFromPoint: (x: number, y: number) => Element[] }).elementsFromPoint = () => [
fakePage,
];

P2 Badge Restore document.elementsFromPoint after stubbing

This test mutates the global document.elementsFromPoint and never restores it, so all later tests in this file inherit a fake hit chain even after fakePage is removed from the DOM. Any subsequent test that relies on pointer normalization or page hit detection can get a fabricated pageIndex/page-local coordinate and pass or fail for the wrong reason. Please save the original function and restore it in cleanup (or use a scoped spy/mock) to avoid cross-test contamination.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Address two P2 review findings on PR #3181:

1. Refresh activeStorySession after exiting H/F (codex-connector inline
   review). exitHeaderFooterMode synchronously clears the backing
   StoryPresentationSessionManager, but the previous patch recomputed
   useActiveSurfaceHitTest from the pre-exit activeStorySession variable.
   In production H/F sessions are story-backed, so that stale value kept
   the first body click on the active-surface path, skipping the new
   page-gap guard. Re-read getActiveStorySession() after the exit before
   recomputing the locals.

   The headerFooterBodyClick test now mocks a story-backed session so it
   would fail without the re-read, and a second test asserts that an
   inter-page gap click (pageIndex undefined) after exiting an H/F
   session still preserves the selection.

2. Restore document.elementsFromPoint after stubbing (Codex review).
   The two PresentationEditor.test.ts tests that fake elementsFromPoint
   now save and restore the original (or delete the stub if there was
   none) via try/finally, so subsequent tests do not inherit the fake
   hit chain.
@tupizz
Copy link
Copy Markdown
Contributor Author

tupizz commented May 6, 2026

Addressed both P2 review comments in 8ddc44e.

1. [P2] Refresh story session after exiting H/F — correct catch. exitHeaderFooterMode synchronously clears the backing StoryPresentationSessionManager, so activeStorySession becomes stale right when I recomputed useActiveSurfaceHitTest from it. In production every H/F session is story-backed, so the old variable was always truthy → useActiveSurfaceHitTest = true → my SD-2749 page-gap guard was bypassed for the first click. Now I also re-read getActiveStorySession() after the H/F handler returns.

Strengthened the existing unit test to mock a story-backed session, and added a second test that exercises the inter-page gap path (pageIndex undefined) while an H/F session was just exited. The new test fails on the prior commit and passes after the fix, so the regression is now actually pinned.

2. [P2] Restore document.elementsFromPoint after stubbing — correct, my bad. The two PresentationEditor.test.ts tests that simulated the page-under-cursor were leaking the stub onto subsequent tests in the same file. Wrapped both in try/finally that restores the original (or deletes the stub if there wasn't one — happy-dom doesn't ship the API).

Verified end-to-end:

  • pnpm --filter super-editor test — 18,423 / 18,423 passing (one more test than before)
  • Browser repro (header dblclick → edit → scroll → body click) → scrollTop delta = 0

Pins the implicit invariant that an active note session is exited before
useActiveSurfaceHitTest is computed, so the new pointerOffAnyPage bail
also catches inter-page gap clicks during footnote/endnote editing.

Fails on main, passes on this branch.
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tupizz good call re-reading the story session after H/F exit, that path was easy to miss :)

i pushed a sibling regression test for the note-session × gap path, and ran render-parity against the merge-base (no rendering delta).

lgtm.

@tupizz tupizz merged commit 3a694d6 into main May 6, 2026
67 of 68 checks passed
@tupizz tupizz deleted the tadeu/sd-2749-bug-clicking-near-page-border-jumps-documents-to-different branch May 6, 2026 18:41
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.63

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.105

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in superdoc-cli v0.8.0-next.79

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in vscode-ext v2.3.0-next.107

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in superdoc v1.30.0-next.61

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 6, 2026

🎉 This PR is included in superdoc-sdk v1.8.0-next.61

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 7, 2026

🎉 This PR is included in superdoc-cli v0.9.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 7, 2026

🎉 This PR is included in superdoc v1.32.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 7, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.4.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 7, 2026

🎉 This PR is included in @superdoc-dev/react v1.3.0

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 7, 2026

🎉 This PR is included in vscode-ext v2.4.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants