fix(pdf): replace document.startViewTransition with non-blocking canvas fade
Build and Deploy Verso / deploy (push) Successful in 14m22s
Build and Deploy Verso / deploy (push) Successful in 14m22s
document.startViewTransition with an async callback places a ::view-transition overlay on top of the entire page, intercepting pointer events for the duration of the callback (up to the 1s safety timeout + 250ms animation). With rapid auto-compiles this created interface freezes and overlapping transitions that could leave the visual lock in a broken state, causing 'stuck on compiling'. Replace with a canvas snapshot overlay + CSS opacity fade-out: - pointer-events:none so the overlay never blocks input - snapshot covers the canvas-clear from setDocument() (no white flash) - on pagerendered: opacity transitions to 0 over 250ms, then overlay removed - gives the same smooth visual crossfade, reliably, in all browsers Chrome 126+ retains the element-level startViewTransition path which is scoped to the PDF container and does not affect the rest of the page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -121,22 +121,6 @@ export default class PDFJSWrapper {
|
||||
// element-level View Transitions (Level 2), Chrome 126+
|
||||
startViewTransition?: (cb: () => void | Promise<void>) => {
|
||||
ready: Promise<void>
|
||||
finished: Promise<void>
|
||||
}
|
||||
}
|
||||
const doc_ = document as typeof document & {
|
||||
// document-level View Transitions (Level 1), Chrome 111+
|
||||
startViewTransition?: (cb: () => void | Promise<void>) => {
|
||||
ready: Promise<void>
|
||||
finished: Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// 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' } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,44 +129,31 @@ export default class PDFJSWrapper {
|
||||
document.visibilityState !== 'hidden'
|
||||
) {
|
||||
// Chrome 126+: element-level View Transition, scoped to this container.
|
||||
// Async callback holds the "before" snapshot until pagerendered fires.
|
||||
// The async callback holds the captured "before" state until the first
|
||||
// new page is rendered, so the crossfade goes old→new, not old→blank.
|
||||
const transition = container.startViewTransition(async () => {
|
||||
setDocument()
|
||||
await firstPageRendered
|
||||
})
|
||||
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. We also suppress the auto-generated root-level animation
|
||||
// (which would otherwise fade the entire page including the editor) by
|
||||
// injecting a temporary <style> that sets animation:none on the root
|
||||
// pseudo-elements. The style is removed after the transition finishes.
|
||||
const name = 'ol-pdf-viewer'
|
||||
this.container.style.viewTransitionName = name
|
||||
const noRootAnim = document.createElement('style')
|
||||
noRootAnim.textContent =
|
||||
'::view-transition-old(root),::view-transition-new(root){animation:none}'
|
||||
document.head.appendChild(noRootAnim)
|
||||
const transition = doc_.startViewTransition(async () => {
|
||||
setDocument()
|
||||
await firstPageRendered
|
||||
transition.ready.catch(err => {
|
||||
if (err?.name !== 'InvalidStateError') {
|
||||
captureException(err, { tags: { handler: 'pdf-preview' } })
|
||||
}
|
||||
})
|
||||
transition.finished.finally(() => {
|
||||
this.container.style.viewTransitionName = ''
|
||||
noRootAnim.remove()
|
||||
})
|
||||
transition.ready.catch(onTransitionError)
|
||||
} else {
|
||||
// Firefox / Safari / very old Chromium: canvas snapshot overlay keeps
|
||||
// the old content visible while setDocument() clears and repaints.
|
||||
// All other browsers: canvas snapshot overlay.
|
||||
// The snapshot covers the canvas-clear that setDocument() triggers so
|
||||
// the viewer never flashes white. Once the first new page is rendered
|
||||
// the overlay fades out (CSS opacity transition), giving the same smooth
|
||||
// visual crossfade without ever blocking user input (pointer-events:none).
|
||||
const snap = this._snapshotCanvases()
|
||||
setDocument()
|
||||
if (snap) {
|
||||
firstPageRendered.then(() => snap.remove())
|
||||
firstPageRendered.then(() => {
|
||||
snap.style.transition = 'opacity 0.25s ease'
|
||||
snap.style.opacity = '0'
|
||||
setTimeout(() => snap.remove(), 280)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user