diff --git a/assets/js/phoenix_live_view/live_socket.js b/assets/js/phoenix_live_view/live_socket.js index 1052d60b14..f09cb3b155 100644 --- a/assets/js/phoenix_live_view/live_socket.js +++ b/assets/js/phoenix_live_view/live_socket.js @@ -362,7 +362,11 @@ export default class LiveSocket { view.setHref(this.getHref()) view.joinDead() if(!this.main){ this.main = view } - window.requestAnimationFrame(() => view.execNewMounted()) + window.requestAnimationFrame(() => { + view.execNewMounted() + // restore scroll position when navigating from an external / non-live page + this.maybeScroll(history.state?.scroll) + }) } } diff --git a/test/e2e/support/navigation.ex b/test/e2e/support/navigation.ex index 70e33f45a4..fd8b90703a 100644 --- a/test/e2e/support/navigation.ex +++ b/test/e2e/support/navigation.ex @@ -43,6 +43,10 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.Layout do <.link navigate="/stream" style="background-color: #f1f5f9; padding: 0.5rem;"> LiveView (other session) + + <.link navigate="/navigation/dead" style="background-color: #f1f5f9; padding: 0.5rem;"> + Dead View +
@@ -165,3 +169,25 @@ defmodule Phoenix.LiveViewTest.E2E.Navigation.BLive do """ end end + +defmodule Phoenix.LiveViewTest.E2E.Navigation.Dead do + use Phoenix.Controller, formats: [:html], layouts: [html: {Phoenix.LiveViewTest.E2E.Navigation.Layout, :live}] + import Phoenix.Component, only: [sigil_H: 2] + + def index(conn, _params) do + assigns = %{} + + conn + |> render(:index) + end +end + +defmodule Phoenix.LiveViewTest.E2E.Navigation.DeadHTML do + use Phoenix.Component + + def index(assigns) do + ~H""" +

Dead view

+ """ + end +end diff --git a/test/e2e/test_helper.exs b/test/e2e/test_helper.exs index f599861699..1e4449f7b3 100644 --- a/test/e2e/test_helper.exs +++ b/test/e2e/test_helper.exs @@ -161,6 +161,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do live "/a", Phoenix.LiveViewTest.E2E.Navigation.ALive live "/b", Phoenix.LiveViewTest.E2E.Navigation.BLive, :index live "/b/:id", Phoenix.LiveViewTest.E2E.Navigation.BLive, :show + get "/dead", Phoenix.LiveViewTest.E2E.Navigation.Dead, :index end end diff --git a/test/e2e/tests/navigation.spec.js b/test/e2e/tests/navigation.spec.js index b3b735915d..13a95597ad 100644 --- a/test/e2e/tests/navigation.spec.js +++ b/test/e2e/tests/navigation.spec.js @@ -226,6 +226,33 @@ test("scrolls hash el into view after live navigation (issue #3452)", async ({pa expect(scrollTop).toBeLessThanOrEqual(offset + 500) }) +test("restores scroll position when navigating from dead view", async ({page}) => { + await page.goto("/navigation/b") + await syncLV(page) + + await expect(page.locator("#items")).toContainText("Item 42") + + expect(await page.evaluate(() => document.documentElement.scrollTop)).toEqual(0) + const offset = (await page.locator("#items-item-42").evaluate((el) => el.offsetTop)) - 200 + await page.evaluate((offset) => window.scrollTo(0, offset), offset) + // LiveView only updates the scroll position every 100ms + await page.waitForTimeout(150) + + await page.getByRole("link", {name: "Dead"}).click() + await page.waitForURL("/navigation/dead") + + await page.goBack() + await syncLV(page) + + // scroll position is restored + await expect.poll( + async () => { + return await page.evaluate(() => document.documentElement.scrollTop) + }, + {message: "scrollTop not restored", timeout: 5000} + ).toBe(offset) +}) + test("navigating all the way back works without remounting (only patching)", async ({page}) => { await page.goto("/navigation/a") await syncLV(page)