diff --git a/src/js/scrollLocks.js b/src/js/scrollLocks.js index 93602134..b3cf8b79 100644 --- a/src/js/scrollLocks.js +++ b/src/js/scrollLocks.js @@ -51,18 +51,17 @@ function lockScroll(container) { header, main, scrollY, - lockedMainTop: main ? main.getBoundingClientRect().top : null, }; } function unlockScroll( - { header, main, scrollY, lockedMainTop }, + { header, main, scrollY }, { restoreScroll = true } = {}, ) { - let scrollTo = 0; + // Not measured at lock time: iOS WebKit can report main's rect there + // against the pre-lock scroll offset + const releasedMainTop = main ? main.getBoundingClientRect().top : null; if (main) { - // Follows main if it moved while locked - scrollTo = scrollY + lockedMainTop - main.getBoundingClientRect().top; main.style.marginTop = ""; main.style.display = ""; } @@ -79,6 +78,12 @@ function unlockScroll( body.style.width = ""; body.style.height = ""; if (restoreScroll) { + let scrollTo = scrollY; + if (main) { + // Keeps main where it is on screen, following it if it moved while locked + const mainDocumentTop = main.getBoundingClientRect().top + window.scrollY; + scrollTo = mainDocumentTop - releasedMainTop; + } window.scrollTo(0, scrollTo); } resyncIOSFixedLayers(); diff --git a/tests/unit/specs/scrollLock.test.js b/tests/unit/specs/scrollLock.test.js index d12c6aae..43fcbb30 100644 --- a/tests/unit/specs/scrollLock.test.js +++ b/tests/unit/specs/scrollLock.test.js @@ -165,14 +165,47 @@ describe("ScrollLock", () => { assert.deepEqual(scrollToCalls[0], [0, 3600]); }); + // Locked, main sits where its margin puts it; unlocked, its rect follows + // its document position and the scroll position + const mockMainRect = (main, layout) => { + main.getBoundingClientRect = () => ({ + top: main.style.marginTop + ? layout.lockedTop + : layout.documentTop - window.scrollY, + }); + }; + it("follows main if it moved while locked", () => { const main = container.querySelector("main"); - main.getBoundingClientRect = () => ({ top: -3500 }); + const layout = { lockedTop: -3500, documentTop: 100 }; + mockMainRect(main, layout); const lock = createLock(); - main.getBoundingClientRect = () => ({ top: -3540 }); + layout.lockedTop = -3540; lock.release(); assert.deepEqual(scrollToCalls[0], [0, 3640]); }); + + it("ignores a stale lock-time rect (iOS WebKit)", () => { + const main = container.querySelector("main"); + // Seen on iOS: right after locking, the rect is still offset by scrollY + main.getBoundingClientRect = () => ({ top: 100 }); + const lock = createLock(); + mockMainRect(main, { lockedTop: -3500, documentTop: 100 }); + lock.release(); + assert.deepEqual(scrollToCalls[0], [0, 3600]); + }); + + it("keeps main in place when content above it changed while locked", () => { + const main = container.querySelector("main"); + const layout = { lockedTop: -3500, documentTop: 100 }; + mockMainRect(main, layout); + const lock = createLock(); + // 300px inserted above main + layout.lockedTop = -3200; + layout.documentTop = 400; + lock.release(); + assert.deepEqual(scrollToCalls[0], [0, 3600]); + }); }); it("restores a locked scrollable ancestor's overflow on unlock", () => {