From 34d272afa9efab75e80cd75d02b365af513c8121 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Tue, 19 May 2026 12:32:01 +0100 Subject: [PATCH] Reapply "Wrap PDF setDocument in startViewTransition (#33346)" (#33633) GitOrigin-RevId: 11dc65d8a8195c8cd6e6e2b58905a0f8b7b218f4 --- .../pdf-preview/components/pdf-js-viewer.tsx | 3 + .../pdf-preview/util/pdf-js-wrapper.ts | 61 ++++++++++++++++--- .../stylesheets/pages/editor/pdf.scss | 14 +++++ 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx index 63a3774d5c..4822cf794f 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx @@ -114,6 +114,9 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) { } const handlePagesinit = () => { + // Set scale immediately to avoid a one-frame flash at PDF.js's default + // 1.333 scale (96/72 DPI) before the React restore effect can correct it. + pdfJsWrapper.viewer.currentScaleValue = scaleRef.current setInitialised(true) timePDFFetched = performance.now() if (document.hidden) { diff --git a/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.ts b/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.ts index 45e7a322db..b2163b50dc 100644 --- a/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.ts +++ b/services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.ts @@ -86,8 +86,49 @@ export default class PDFJSWrapper { return } - this.viewer.setDocument(doc) - this.linkService.setDocument(doc) + // Hold the .pdfViewer element's height steady across the synchronous page-clear + // that setDocument() triggers, so the viewer doesn't visually collapse while the + // new pages are being initialised. The min-height is released on pagesinit. + const viewerEl = this.container.querySelector('.pdfViewer') as HTMLElement + const currentHeight = viewerEl.getBoundingClientRect().height + if (currentHeight > 0) { + viewerEl.style.minHeight = `${currentHeight}px` + const clearMinHeight = () => { + viewerEl.style.minHeight = '' + this.eventBus.off('pagesinit', clearMinHeight) + } + this.eventBus.on('pagesinit', clearMinHeight) + } + + const setDocument = () => { + this.viewer.setDocument(doc) + this.linkService.setDocument(doc) + } + + const container = this.container as typeof this.container & { + // supported since Chrome 147 + startViewTransition?: (cb: () => void | Promise) => { + ready: Promise + } + } + + if ( + typeof container.startViewTransition === 'function' && + document.visibilityState !== 'hidden' + ) { + const transition = container.startViewTransition(setDocument) + + transition.ready.catch(err => { + // ignore InvalidStateError, it just means the document was hidden + if (err?.name !== 'InvalidStateError') { + captureException(err, { + tags: { handler: 'pdf-preview' }, + }) + } + }) + } else { + setDocument() + } return doc } catch (error: any) { @@ -206,14 +247,18 @@ export default class PDFJSWrapper { destArray, }) - // scroll the page left and down by an extra few pixels to account for the pdf.js viewer page border + // scrollPageIntoView aligns PDF content to the container top, ignoring the page margin. + // For a top-of-document position this leaves scrollTop = marginTop (margin hidden). + // Snap back to 0 so the margin is visible, but only when we are in that margin band — + // for any real mid-document scrollTop this condition is false and we leave it untouched. const pageIndex = this.viewer.currentPageNumber - 1 const pageView = this.viewer.getPageView(pageIndex) - const offset = parseFloat(getComputedStyle(pageView.div).borderWidth) - this.viewer.container.scrollBy({ - top: -offset, - left: -offset, - }) + if (pageView) { + const marginTop = parseFloat(getComputedStyle(pageView.div).marginTop) + if (this.viewer.container.scrollTop <= marginTop) { + this.viewer.container.scrollTop = 0 + } + } } isVisible() { diff --git a/services/web/frontend/stylesheets/pages/editor/pdf.scss b/services/web/frontend/stylesheets/pages/editor/pdf.scss index f1a5e122a3..46504b49fb 100644 --- a/services/web/frontend/stylesheets/pages/editor/pdf.scss +++ b/services/web/frontend/stylesheets/pages/editor/pdf.scss @@ -255,6 +255,7 @@ } .page { + display: block; // prevent Overleaf's .loading class (display:inline-flex) from affecting PDF.js page state box-sizing: content-box; margin: var(--spacing-05) auto; box-shadow: @@ -262,6 +263,13 @@ 0 3px 14px 0 #23282f08, 0 8px 10px 0 #23282f14; border: none; + + // Overleaf has its own loading state UI; suppress the PDF.js page-level loading + // spinner so the loadingIcon/loading classes have no layout or visual effect. + /* stylelint-disable-next-line selector-class-pattern */ + &.loadingIcon::after { + display: none; + } } .pdfjs-viewer-inner { @@ -271,6 +279,7 @@ height: 100%; -webkit-font-smoothing: initial; -moz-osx-font-smoothing: initial; + view-transition-name: pdf-viewer; /* fix review-panel overflow issue, see: https://github.com/overleaf/internal/issues/6781#issuecomment-1112708638 */ /* stylelint-disable-next-line selector-class-pattern */ @@ -316,6 +325,11 @@ } } +::view-transition-old(pdf-viewer), +::view-transition-new(pdf-viewer) { + animation-duration: 0.25s; +} + .pdfjs-viewer-controls { display: flex; align-items: center;