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..730ac476da 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,36 @@ 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 container = this.container as typeof this.container & { + // supported since Chrome 147 + startViewTransition?: (cb: () => void | Promise) => unknown + } + if ( + container.startViewTransition && + typeof container.startViewTransition === 'function' + ) { + container.startViewTransition(() => { + this.viewer.setDocument(doc) + this.linkService.setDocument(doc) + }) + } else { + this.viewer.setDocument(doc) + this.linkService.setDocument(doc) + } return doc } catch (error: any) { @@ -206,14 +234,16 @@ 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, - }) + 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..4a7a286705 100644 --- a/services/web/frontend/stylesheets/pages/editor/pdf.scss +++ b/services/web/frontend/stylesheets/pages/editor/pdf.scss @@ -217,6 +217,7 @@ .pdf-viewer { isolation: isolate; + view-transition-name: pdf-viewer; iframe { width: 100%; @@ -255,6 +256,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 +264,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 { @@ -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;