fix(reader): reopen a PDF at its saved page instead of the first

The PDF reader restored the saved page with a smooth scroll, which Chrome
starts about a second after it is asked. With Fit page, Fit width or
Automatic zoom, the first viewport resize refits the page from the
current offset with an instant scroll, and a classic scrollbar appearing
as the document loads is such a resize. The refit cancelled the restore
before it moved, so the reader stayed on page 1 while the toolbar showed
the saved page, and the next scroll saved page 1 or 2 over the position.

Restore the opening page with an instant scroll, so the refit starts from
the saved page and keeps it. Page turns still scroll smoothly.

Fixes #1857
This commit is contained in:
neon
2026-10-20 04:19:56 +00:00
parent a5c4dfac16
commit 58f258a5fc
2 changed files with 15 additions and 3 deletions
@@ -10,7 +10,7 @@ const mocks = vi.hoisted(() => ({
layoutReadyListener: null as ((event: LayoutReadyEvent) => void) | null,
pageChangeListener: null as ((event: PageChangeEvent) => void) | null,
totalPages: 10,
scrollToPage: vi.fn<(options: { pageNumber: number }) => void>(),
scrollToPage: vi.fn<(options: { pageNumber: number; behavior?: ScrollBehavior }) => void>(),
}))
vi.mock('@embedpdf/plugin-scroll/vue', async () => {
@@ -21,7 +21,7 @@ vi.mock('@embedpdf/plugin-scroll/vue', async () => {
state: ref({ currentPage: 1, totalPages: mocks.totalPages }),
provides: ref({
// EmbedPDF reports the target page synchronously when a programmatic page change starts.
scrollToPage: (options: { pageNumber: number }) => {
scrollToPage: (options: { pageNumber: number; behavior?: ScrollBehavior }) => {
mocks.scrollToPage(options)
mocks.pageChangeListener?.({ documentId: 'document-1', pageNumber: options.pageNumber, totalPages: mocks.totalPages })
},
@@ -125,6 +125,15 @@ describe('PdfReaderContent', () => {
expect(mocks.scrollToPage.mock.calls.length).toBeLessThanOrEqual(1)
})
it('restores the opening page with an instant scroll', () => {
mountContent(7)
layoutReady()
// A smooth scroll that has not started is cancelled by the fit zoom's refit on the first
// viewport resize, which leaves the reader on page 1.
expect(mocks.scrollToPage).toHaveBeenCalledExactlyOnceWith({ pageNumber: 7, behavior: 'instant' })
})
it('marks the opening page as restored and later turns as moves', () => {
const wrapper = mountContent(null, 37.5)
layoutReady()
@@ -343,7 +343,10 @@ watch(
restoredInitialPage.value = true
if (page > 1) {
restoringPage = page
scroll.value?.scrollToPage({ pageNumber: page })
// Instant, because a fit zoom refits on the first viewport resize (a classic scrollbar
// appearing) from the current offset, which cancels a smooth scroll that has not started
// and strands the reader on page 1, so the next scroll saves page 1 over the position.
scroll.value?.scrollToPage({ pageNumber: page, behavior: 'instant' })
}
})
const unsubscribePage = capability.onPageChange((event) => {