Skip to content

fix(virtualization): correct scroll mapping and viewport sizing at non-100% zoom#2171

Merged
harbournick merged 5 commits intomainfrom
fix/virtualization-zoom-scroll-mismatch
Mar 2, 2026
Merged

fix(virtualization): correct scroll mapping and viewport sizing at non-100% zoom#2171
harbournick merged 5 commits intomainfrom
fix/virtualization-zoom-scroll-mismatch

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Feb 25, 2026

Summary

Fixes blank pages and excess scroll space when zoomed out (e.g., 75%) in long documents with virtualization enabled.

Relates to: IT-579 (cursor accelerates to bottom in long documents), IT-581 (scroll jumps on long documents)

Root Cause

Three interrelated bugs caused the virtualization system to break at zoom != 100%:

1. Virtual window scroll calculation ignores CSS transform scale

DomPainter.updateVirtualWindow() uses getBoundingClientRect().top to determine scroll position. This returns screen-space coordinates (affected by transform: scale(0.75)), but virtualOffsets are in layout space (unscaled). The mismatch grows linearly with scroll position — at scrollTop=15000 with 75% zoom, the error is ~5000px, causing the virtualization to mount pages above the actual viewport (blank pages).

2. Gap mismatch between PresentationEditor and DomPainter

PresentationEditor calculated viewport height using 24px page gaps (DEFAULT_PAGE_GAP), but DomPainter's virtualization defaulted to 72px (DEFAULT_VIRTUALIZED_PAGE_GAP). This made the actual rendered content taller than the viewport expected, causing the spacer math to be inconsistent.

3. CSS box overflow inflating scroll range

CSS transform: scale() does not change an element's CSS box dimensions. At zoom < 1, painterHost's CSS box (full unscaled height, e.g., 74304px) overflowed viewportHost's minHeight (scaled, e.g., 53442px), making the scroll container see ~30% extra unusable scrollable space at the bottom.

Changes

File Change
painters/dom/src/renderer.ts Added zoomFactor property + setZoom() method; fixed updateVirtualWindow() to divide rect.top by zoom to convert screen-space → layout-space
painters/dom/src/index.ts Exposed setZoom() through the createDomPainter facade
PresentationEditor.ts Wire zoom to DomPainter via setZoom() in both setZoom() and #ensurePainter(); normalize virtualization gap to match effective page gap; use explicit height + overflow: hidden on viewportHost

Before / After (75% zoom, scrollTop=15000)

Before After
Visible content Blank page Paragraphs 172-175 visible
scrollHeight 74304px (unscaled CSS box) 53462px (correct scaled size)
Viewport sizing minHeight: 53442px (overridden by child CSS box) height: 53442px + overflow: hidden

Test plan

  • painter-dom tests pass (638/638)
  • layout-engine tests pass (480/480)
  • pm-adapter tests pass (1544/1544)
  • TypeScript builds clean
  • Verified in browser: 75% zoom — content visible at scroll positions 0, 5000, 10000, 15000, 20000, 30000, and end-of-document
  • Verified 100% zoom still works correctly
  • Verified 150% zoom works correctly
  • Manual QA: test with various long documents at 50%, 75%, 100%, 125%, 150% zoom
  • Manual QA: verify cursor click-to-position works at zoomed-out levels

…n-100% zoom

Fix blank pages and excess scroll space when zoomed out in long documents.

Three interrelated bugs caused the virtualization system to break at
zoom != 100%:

1. updateVirtualWindow() used getBoundingClientRect().top (screen-space,
   affected by CSS transform: scale) but compared against virtualOffsets
   (layout-space, unscaled). Divide by zoom factor to fix.

2. PresentationEditor used 24px page gap for viewport height calculation
   while DomPainter virtualization defaulted to 72px. Normalize the
   virtualization gap to match the effective page gap.

3. CSS transform: scale() does not change layout box dimensions. At
   zoom < 1, painterHost's unscaled CSS box overflowed viewportHost,
   inflating the scroll range. Use explicit height + overflow: hidden
   on viewportHost to clip the CSS box.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 556f6bdbd9

ℹ️ 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".

Blank documents created by the store lacked an `id`, causing the
PresentationEditor instance to not register in the static `#instances`
map. When `setGlobalZoom()` iterated the map it found nothing, so
zoom CSS transforms were never applied.

Two fixes:
- Add `id: uuidv4()` to the blank document config in superdoc-store
- Always register PresentationEditor instances with a fallback key
  when `documentId` is not provided, ensuring `setGlobalZoom` works
  regardless of document configuration
Copy link
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.

good start!

the core fix (zoom × virtualization) isn't covered by tests - each side is tested separately but the interaction isn't.

a unit test in virtualization.test.ts that calls setZoom(0.75) + triggers scroll and checks the virtual window would catch regressions here.

…ng, zoom test

- Replace options.documentId mutation with #registryKey private field
- Replace overflow:hidden with negative margin-bottom on painterHost
  to avoid clipping collaboration cursor labels
- Add zoom × virtualization interaction test for non-scrollable container
@tupizz tupizz requested a review from caio-pizzol February 27, 2026 17:21
Copy link
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 registryKey, overflow clipping, and zoom test all addressed. left one inline comment — the test assertions are loose enough to pass on the old buggy behavior too, so they don't guard the regression yet. not blocking.

@caio-pizzol
Copy link
Contributor

@tupizz a behavior test that loads a long doc at 75% zoom, scrolls to mid-document, and checks content is visible (not blank) would go a long way — this bug is easy to regress when virtualization or zoom code changes.

Tighten the zoom × virtualization unit test to assert exact page indices
[7, 8, 9] instead of loose range checks that pass on buggy behavior.

Add a Playwright behavior test that generates a long document, sets 75%
zoom, scrolls to mid-document, and verifies content is visible (not
blank). This guards against regressions when virtualization or zoom code
changes.
Copy link
Collaborator

@harbournick harbournick left a comment

Choose a reason for hiding this comment

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

LGTM

@harbournick harbournick merged commit 84af4c0 into main Mar 2, 2026
8 checks passed
@harbournick harbournick deleted the fix/virtualization-zoom-scroll-mismatch branch March 2, 2026 22:11
@superdoc-bot
Copy link
Contributor

superdoc-bot bot commented Mar 2, 2026

🎉 This PR is included in superdoc-cli v0.2.0-next.53

The release is available on GitHub release

@superdoc-bot
Copy link
Contributor

superdoc-bot bot commented Mar 2, 2026

🎉 This PR is included in superdoc v1.17.0-next.59

The release is available on GitHub release

harbournick pushed a commit that referenced this pull request Mar 3, 2026
# [1.17.0](v1.16.0...v1.17.0) (2026-03-03)

### Bug Fixes

* active track change ([#2163](#2163)) ([108c14d](108c14d))
* add currentTotalPages getter and pagination-update event ([#2202](#2202)) ([95b4579](95b4579)), closes [#958](#958)
* always call resolveComment after custom TC bubble handlers (SD-2049) ([#2204](#2204)) ([34fb4e0](34fb4e0))
* backward replace insert text ([#2172](#2172)) ([66f0849](66f0849))
* before paragraph spacing inside table cells ([#1842](#1842)) ([c7efa85](c7efa85))
* **collaboration:** deduplicate updateYdocDocxData during replaceFile (SD-1920) ([#2162](#2162)) ([52962fc](52962fc))
* **comments:** cross-page collision avoidance for floating comment bubbles (SD-1998) ([#2180](#2180)) ([6cfbeca](6cfbeca))
* **comments:** emit empty comment positions so undo clears orphan bubbles ([#2235](#2235)) ([12ba727](12ba727))
* **comments:** improve multiline comment input styling ([#2242](#2242)) ([e6a0dab](e6a0dab))
* **comments:** prevent comment mark from extending to adjacent typed text ([#2241](#2241)) ([07fecd8](07fecd8))
* **comments:** reduce sidebar jitter when clicking comments (SD-2034) ([#2250](#2250)) ([c3568d2](c3568d2))
* **comments:** remove synchronous dispatch from plugin apply() (SD-1940) ([#2157](#2157)) ([887175b](887175b))
* **css:** scope ProseMirror CSS to prevent bleeding into host apps (SD-1850) ([#2134](#2134)) ([b9d98fa](b9d98fa))
* document-api improvements, plan mode, query.match, mutations ([6221580](6221580))
* **document-api:** delete table cell fix ([#2209](#2209)) ([5e5c43f](5e5c43f))
* **document-api:** distribute columns command fixes ([#2207](#2207)) ([8f4eaf7](8f4eaf7))
* **document-api:** fix cell shading in document api ([#2215](#2215)) ([456f60e](456f60e))
* **document-api:** insert table cell ([#2210](#2210)) ([357ee90](357ee90))
* **document-api:** plan-engine reliability fixes and error diagnostics ([#2185](#2185)) ([abfd81b](abfd81b))
* **document-api:** split table cell command ([#2217](#2217)) ([0b3e2b4](0b3e2b4))
* **document-api:** split table command ([#2214](#2214)) ([ec31699](ec31699))
* **editor:** render styles applied inside SDT fields (SD-2011) ([#2188](#2188)) ([9c34be3](9c34be3))
* **editor:** selection highlight flickers when dragging across mark boundaries (SD-2024) ([#2205](#2205)) ([ba03e76](ba03e76))
* extract duplicate block identity normalization from docxImporter ([7f7ff93](7f7ff93))
* improve backspace behavior near run boundaries for tracked changes ([#2175](#2175)) ([6c9c7a3](6c9c7a3))
* **layout:** per-section footer constraints for multi-section docs (SD-1837) ([#2022](#2022)) ([e11acc5](e11acc5))
* markdown block-separator blank lines and heading split style-mark normalization ([e988adc](e988adc))
* normalize review namespace into trackChanges, harden input validation ([33e907b](33e907b))
* outside click for toolbar dropdown ([#2174](#2174)) ([5f859c7](5f859c7))
* prefer full decoration range ([#2239](#2239)) ([ac15e31](ac15e31)), closes [#collectDesiredState](https://github.com/superdoc-dev/superdoc/issues/collectDesiredState) [#resolveEffectiveRanges](https://github.com/superdoc-dev/superdoc/issues/resolveEffectiveRanges) [#setPreviousRanges](https://github.com/superdoc-dev/superdoc/issues/setPreviousRanges)
* preserve line spacing and indentation on Google Docs paste ([#2183](#2183)) ([b9a7357](b9a7357)), closes [#2151](#2151)
* preserve text-align on paste from Google Docs ([#2208](#2208)) ([762231b](762231b))
* rollback comments colors / ui ([#2216](#2216)) ([a99b5ab](a99b5ab))
* **scroll:** wait for virtualized page mount and center text element ([#2221](#2221)) ([95f634e](95f634e))
* **shapes:** render grouped DrawingML shapes with custom geometry (SD-1877) ❇️ ([#2105](#2105)) ([14985a5](14985a5))
* splitting run with header adds empty row ([#2229](#2229)) ([e1965fc](e1965fc))
* **super-converter:** handle empty pic:spPr in image import ([#2254](#2254)) ([2b8dbce](2b8dbce))
* **super-editor:** backspace across run boundaries without splitting list items ([#2258](#2258)) ([27ccb64](27ccb64))
* support cell spacing ([#1879](#1879)) ([1639967](1639967))
* **tables:** defaultTableStyle support, cell fixes ([#2246](#2246)) ([74fca9c](74fca9c))
* **tables:** expand auto-width tables to fill available page width ([#2109](#2109)) ([15f36bc](15f36bc))
* **tables:** preserve TableGrid defaults and style-driven spacing/bor… ([#2230](#2230)) ([b0a482f](b0a482f))
* text highlight on export ([#2189](#2189)) ([9cbd022](9cbd022))
* track highlight changes ([#2192](#2192)) ([e164625](e164625))
* **track-changes:** correct format change description for already-formatted text (SD-2077) ([#2253](#2253)) ([b2ffc0d](b2ffc0d))
* **track-changes:** handle ReplaceAroundStep in tracked changes mode (SD-2061) ([#2225](#2225)) ([8f3cbe4](8f3cbe4))
* **track-changes:** remove ghost TrackFormat on multi-node format cancel ([#2233](#2233)) ([e925ef9](e925ef9))
* undo/redo actions ([#2161](#2161)) ([495e92f](495e92f))
* **virtualization:** correct scroll mapping and viewport sizing at non-100% zoom ([#2171](#2171)) ([84af4c0](84af4c0)), closes [#registryKey](https://github.com/superdoc-dev/superdoc/issues/registryKey)

### Features

* allow custom accept/reject handlers for TC bubbles ([#1921](#1921)) ([e30abf6](e30abf6))
* **comments:** improve floating comments ui ([#2195](#2195)) ([e870cfb](e870cfb))
* **document-api:** add format operations font size alignment color font family ([#2179](#2179)) ([f19c688](f19c688))
* **document-api:** add get markdown to sdks ([e42b56d](e42b56d))
* **document-api:** add plan-based mutation engine with query.match and style capture ([#2160](#2160)) ([365293a](365293a))
* **document-api:** default table style setting ([#2248](#2248)) ([3ad4e9f](3ad4e9f))
* **document-api:** default target-less insert to document end ([#2244](#2244)) ([c717e2b](c717e2b))
* **document-api:** doc default initial styles ([#2184](#2184)) ([f25e41f](f25e41f))
* **document-api:** format.paragraph for w:pPr formatting ([#2218](#2218)) ([32c9991](32c9991))
* **document-api:** history name space ([#2219](#2219)) ([41dea37](41dea37))
* **document-api:** include anchored text in comments list response ([#2177](#2177)) ([b3a2912](b3a2912))
* **document-api:** inline formatting parity core end-to-end ([#2197](#2197)) ([b405b03](b405b03))
* **document-api:** inline formatting rpr parity ([#2198](#2198)) ([41ab771](41ab771))
* **document-api:** lists namespace  ([#2223](#2223)) ([09ebfcb](09ebfcb))
* **document-api:** section commands ([#2199](#2199)) ([ec4abe3](ec4abe3))
* **document-api:** support deleting entire block nodes not only text ([#2181](#2181)) ([2897246](2897246))
* **document-api:** table of contents commands ([#2200](#2200)) ([baa72c4](baa72c4))
* **document-api:** tables namespace and commands ([#2182](#2182)) ([b80ee31](b80ee31))
* **document-api:** toc commands ([#2220](#2220)) ([767e010](767e010))
* **images:** allow drag-and-drop for images in editor ([#2227](#2227)) ([4b36780](4b36780))
* **layout-engine:** render table headers, tblLook support ([#2256](#2256)) ([db6a2ff](db6a2ff))
* **link-popover:** custom link popovers ([#2222](#2222)) ([070190f](070190f))
* **markdown:** add markdown override to sdk, improve conversion ([#2196](#2196)) ([04a1c71](04a1c71))
* preserve w:view setting through DOCX round-trip ([#2190](#2190)) ([48b4210](48b4210)), closes [#2070](#2070)
* real time collab in python sdk ([#2243](#2243)) ([dc3b4fd](dc3b4fd))
* **tables:** allow resizing table rows ([#2226](#2226)) ([2c6da10](2c6da10))
* **tables:** improve cell color application (context), column dragging, table pasting ([#2228](#2228)) ([066b9eb](066b9eb))
* **table:** toggle header row sets both cell types and repeatHeader atomically ([#2245](#2245)) ([2f5899d](2f5899d))
* **track-changes:** clear comment bubbles when bulk accept or reject TCs ([#2159](#2159)) ([27fbe8e](27fbe8e))

### Performance Improvements

* **comments:** batch tracked change comment creation on load ([#2166](#2166)) ([0c2eca5](0c2eca5))
* **comments:** batch tracked change creation and virtualize floating bubbles (SD-1997) ([#2168](#2168)) ([70fd7d9](70fd7d9))
@harbournick
Copy link
Collaborator

🎉 This PR is included in superdoc v1.17.0

The release is available on GitHub release

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.

4 participants