Bumps foliate-js to drop the redundant manual `scrollBy` the scrolled-mode page iframes ran on every wheel event. Because those iframes are `scrolling="no"`, the browser already chains the wheel to the host scroller natively; the extra scrollBy stacked on top, so wheeling over a page moved it ~2x as far in an instant lurch while the margins scrolled smoothly by one notch. Native scroll-chaining now provides the single smooth scroll over both the page and the margins. Adds a browser-lane regression test that mounts the real <foliate-fxl> renderer in scrolled mode and asserts a wheel over a page does not programmatically move the host scroller. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// Registers the <foliate-fxl> custom element (the fixed-layout / PDF renderer).
|
||||
import 'foliate-js/fixed-layout.js';
|
||||
|
||||
// Regression test for readest#4727: in fixed-layout scroll mode, a wheel tick
|
||||
// that lands on a page iframe (during the brief idle window where the iframe is
|
||||
// interactive) must NOT be scrolled by JS. The browser already chains the wheel
|
||||
// to the host scroller natively; if the in-iframe wheel handler also scrolls the
|
||||
// host, the page travels twice as far in an instant lurch instead of one smooth
|
||||
// notch.
|
||||
//
|
||||
// This needs a real browser: the bug is the interaction between native wheel
|
||||
// scrolling and a programmatic scrollBy, and jsdom has no layout/scroll. We
|
||||
// mount the real renderer and assert the JS-side handler does not move the host
|
||||
// scroller on its own (the synthetic wheel does not trigger native scrolling,
|
||||
// so any movement we observe is purely the JS handler — which must be zero).
|
||||
|
||||
const PAGE_HTML = `<!doctype html><html><head><style>
|
||||
html, body { margin: 0; height: 1000px; background: linear-gradient(#fff, #ccc); }
|
||||
</style></head><body></body></html>`;
|
||||
|
||||
const makeBook = (sectionCount: number) => ({
|
||||
dir: 'ltr',
|
||||
rendition: { viewport: { width: 600, height: 1000 }, spread: 'none' },
|
||||
sections: Array.from({ length: sectionCount }, () => ({
|
||||
// `src` only has to be truthy — when `data` is present the renderer loads it
|
||||
// via srcdoc, which keeps the iframe same-origin so its document (and the
|
||||
// wheel listener attached to it) is reachable.
|
||||
load: async () => ({ src: 'srcdoc', data: PAGE_HTML }),
|
||||
linear: 'yes',
|
||||
})),
|
||||
});
|
||||
|
||||
const waitFor = async <T>(fn: () => T | null | undefined, timeout = 4000): Promise<T> => {
|
||||
const start = performance.now();
|
||||
for (;;) {
|
||||
const value = fn();
|
||||
if (value) return value;
|
||||
if (performance.now() - start > timeout) throw new Error('waitFor timed out');
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
}
|
||||
};
|
||||
|
||||
let renderer: HTMLElement | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
renderer?.remove();
|
||||
renderer = null;
|
||||
});
|
||||
|
||||
describe('fixed-layout scroll mode wheel handling (readest#4727)', () => {
|
||||
it('does not double-scroll the host when a wheel lands on a page', async () => {
|
||||
renderer = document.createElement('foliate-fxl');
|
||||
renderer.style.width = '600px';
|
||||
renderer.style.height = '400px';
|
||||
renderer.setAttribute('flow', 'scrolled');
|
||||
document.body.append(renderer);
|
||||
|
||||
(renderer as unknown as { open(book: unknown): void }).open(makeBook(3));
|
||||
|
||||
// The first page loads via the IntersectionObserver; wait until its iframe
|
||||
// has rendered (display flipped from 'none') — by then the wheel listener
|
||||
// that #loadScrollPage attaches to the iframe document is in place.
|
||||
const iframe = await waitFor(() => {
|
||||
const f = renderer!.shadowRoot?.querySelector<HTMLIFrameElement>('.scroll-page iframe');
|
||||
return f && f.style.display && f.style.display !== 'none' && f.contentDocument ? f : null;
|
||||
});
|
||||
|
||||
const scroller = renderer as unknown as HTMLElement;
|
||||
scroller.scrollTop = 0;
|
||||
|
||||
// A wheel over the page iframe. With the bug, the iframe handler runs
|
||||
// `host.scrollBy({ top: deltaY })`, moving the host scroller by ~120px on
|
||||
// top of the (here absent) native scroll. With the fix it must stay put.
|
||||
iframe.contentDocument!.dispatchEvent(
|
||||
new WheelEvent('wheel', { deltaY: 120, bubbles: true, cancelable: true }),
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
|
||||
expect(scroller.scrollTop).toBe(0);
|
||||
});
|
||||
});
|
||||
+1
-1
Submodule packages/foliate-js updated: 24d9a0c0ee...e366bdb79c
Reference in New Issue
Block a user