From 66afe1ff9e6340caaba1d4f3af6ccc54b16efa9d Mon Sep 17 00:00:00 2001 From: Grace Kind Date: Sat, 30 May 2026 16:58:08 -0500 Subject: [PATCH] Update page hidden styles --- package.json | 2 +- src/css/style.css | 4 +- src/js/router.js | 2 - tests/e2e/specs/concerns/scroll.test.js | 64 +++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 99fead62..223655d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.14.120", + "version": "0.14.121", "type": "module", "scripts": { "start": "rm -rf build && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index b570ebe5..0ca7bd69 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -304,7 +304,9 @@ video { } .page.page-hidden { - display: none; + visibility: hidden; + position: fixed; + transform: translateX(-200vw); } .page.page-visible { diff --git a/src/js/router.js b/src/js/router.js index 6c721d66..a17dad41 100644 --- a/src/js/router.js +++ b/src/js/router.js @@ -127,8 +127,6 @@ export class Router extends EventEmitter { this.pages.delete(path); this.pages.set(path, page); const scrollY = this.scrollStates.get(path) ?? 0; - // Keep page hidden during scroll restoration to prevent flash - // this.currentPage.style.opacity = 0; this.currentPage.classList.remove("page-hidden"); this.currentPage.classList.add("page-visible"); this.currentPage.dispatchEvent( diff --git a/tests/e2e/specs/concerns/scroll.test.js b/tests/e2e/specs/concerns/scroll.test.js index 0cc3ba11..80f0921a 100644 --- a/tests/e2e/specs/concerns/scroll.test.js +++ b/tests/e2e/specs/concerns/scroll.test.js @@ -55,4 +55,68 @@ test.describe("Scroll position restoration", () => { // Verify the post we scrolled to is still visible (scroll position restored) await expect(targetPost).toBeVisible({ timeout: 10000 }); }); + + test("keeps cached pages laid out and off-screen so the sticky header doesn't flash on return", async ({ + page, + }) => { + const mockServer = new MockServer(); + const posts = []; + for (let i = 1; i <= 60; i++) { + posts.push( + createPost({ + uri: `at://did:plc:author${i}/app.bsky.feed.post/post${i}`, + text: `Timeline post ${i}`, + authorHandle: `author${i}.bsky.social`, + authorDisplayName: `Author ${i}`, + }), + ); + } + mockServer.addTimelinePosts(posts); + await mockServer.setup(page); + + await login(page); + await page.goto("/"); + + const view = page.locator("#home-view"); + await expect(view.locator('[data-testid="feed-item"]')).toHaveCount(41, { + timeout: 10000, + }); + + // Scroll down so the home page is taller than the page we navigate to, + // then navigate away so home becomes a cached/hidden page. + const targetPost = view + .locator('[data-testid="feed-item"]') + .filter({ hasText: "Timeline post 30" }); + await targetPost.scrollIntoViewIfNeeded(); + await targetPost.locator('[data-testid="small-post"]').click(); + await expect(page.locator("#post-detail-view")).toBeVisible({ + timeout: 10000, + }); + + const probe = await page.evaluate(() => { + const hiddenPage = document.querySelector(".page-hidden"); + const header = hiddenPage?.querySelector("header"); + const pageRect = hiddenPage?.getBoundingClientRect(); + const headerRect = header?.getBoundingClientRect(); + return { + hasHiddenPage: !!hiddenPage, + headerHeight: headerRect ? Math.round(headerRect.height) : null, + pageRight: pageRect ? Math.round(pageRect.right) : null, + scrollHeight: Math.round(document.scrollingElement.scrollHeight), + }; + }); + + // The home page is cached rather than torn down. + expect(probe.hasHiddenPage).toBe(true); + // Its layout is preserved while hidden (a non-zero header height). This is + // what prevents the reveal-time relayout that flashed the sticky header. + expect(probe.headerHeight).toBeGreaterThan(0); + // It is positioned outside the viewport so it neither overlaps the visible + // page nor triggers IntersectionObserver-driven behaviour (video/gif + // playback, infinite scroll, post-seen tracking). + expect(probe.pageRight).toBeLessThanOrEqual(0); + // And it does not inflate the scrollable height of the (shorter) visible + // page, despite the cached home feed being much taller. + expect(probe.scrollHeight).toBeLessThan(2000); + }); }); -- 2.51.2