From d895e14e48326fa6b3a5285b56c6b335fe78f796 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 7 Jun 2026 12:22:46 +0000 Subject: [PATCH] feat(pdf): restore smooth crossfade for Chrome 111+ using document.startViewTransition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original code used container.startViewTransition(setDocument) with a synchronous callback, giving a 250ms CSS crossfade that looked smooth when the PDF happened to re-render before the animation ended — but was a race. Now there are three tiers: - Chrome 126+: element-level startViewTransition, async, waits for pagerendered - Chrome 111+ (Brave 138, Edge 111+): document-level startViewTransition with view-transition-name scoped to the PDF container, same async pattern - Firefox / Safari / older Chromium: canvas snapshot overlay (no animation, but seamless — introduced in build #108) The document-level path restores the smooth fade the user saw on Edge build #93, now guaranteed to crossfade old→new rather than old→blank. Co-Authored-By: Claude Sonnet 4.6 --- .../pdf-preview/util/pdf-js-wrapper.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) 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 9d7b154d63..a50414f7b2 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 @@ -118,9 +118,25 @@ export default class PDFJSWrapper { }) const container = this.container as typeof this.container & { - // supported since Chrome 126 + // element-level View Transitions (Level 2), Chrome 126+ startViewTransition?: (cb: () => void | Promise) => { ready: Promise + finished: Promise + } + } + const doc_ = document as typeof document & { + // document-level View Transitions (Level 1), Chrome 111+ + startViewTransition?: (cb: () => void | Promise) => { + ready: Promise + finished: Promise + } + } + + // Shared error handler for transition.ready rejections. + const onTransitionError = (err: any) => { + // InvalidStateError just means the document was hidden during the transition. + if (err?.name !== 'InvalidStateError') { + captureException(err, { tags: { handler: 'pdf-preview' } }) } } @@ -128,27 +144,33 @@ export default class PDFJSWrapper { typeof container.startViewTransition === 'function' && document.visibilityState !== 'hidden' ) { - // Chrome 126+: element-level View Transition API. - // The async callback holds the "before" snapshot until pagerendered fires, - // so the crossfade goes old→new rather than old→blank. + // Chrome 126+: element-level View Transition, scoped to this container. + // Async callback holds the "before" snapshot until pagerendered fires. const transition = container.startViewTransition(async () => { setDocument() await firstPageRendered }) - - transition.ready.catch(err => { - // ignore InvalidStateError, it just means the document was hidden - if (err?.name !== 'InvalidStateError') { - captureException(err, { - tags: { handler: 'pdf-preview' }, - }) - } + transition.ready.catch(onTransitionError) + } else if ( + typeof doc_.startViewTransition === 'function' && + document.visibilityState !== 'hidden' + ) { + // Chrome 111+ / Edge 111+: document-level View Transition. + // Give the PDF container a unique view-transition-name so only it + // crossfades; the rest of the page stays static. + const name = 'ol-pdf-viewer' + this.container.style.viewTransitionName = name + const transition = doc_.startViewTransition(async () => { + setDocument() + await firstPageRendered }) + transition.finished.finally(() => { + this.container.style.viewTransitionName = '' + }) + transition.ready.catch(onTransitionError) } else { - // All other browsers: snapshot the currently-rendered canvases into a - // temporary overlay so setDocument()'s synchronous canvas-clear doesn't - // produce a white flash. The overlay is removed once the first page of - // the new document has been painted. + // Firefox / Safari / very old Chromium: canvas snapshot overlay keeps + // the old content visible while setDocument() clears and repaints. const snap = this._snapshotCanvases() setDocument() if (snap) {