diff --git a/src/core/drive/navigator.js b/src/core/drive/navigator.js index 8210c7471..4924344c7 100644 --- a/src/core/drive/navigator.js +++ b/src/core/drive/navigator.js @@ -99,7 +99,7 @@ export class Navigator { } else { await this.view.renderPage(snapshot, false, true, this.currentVisit) } - if(!snapshot.shouldPreserveScrollPosition) { + if (snapshot.refreshScroll !== "preserve") { this.view.scrollToTop() } this.view.clearSnapshotCache() diff --git a/src/core/drive/page_snapshot.js b/src/core/drive/page_snapshot.js index 58a5a75a9..56e0c63f1 100644 --- a/src/core/drive/page_snapshot.js +++ b/src/core/drive/page_snapshot.js @@ -74,12 +74,12 @@ export class PageSnapshot extends Snapshot { return this.headSnapshot.getMetaValue("view-transition") === "same-origin" } - get shouldMorphPage() { - return this.getSetting("refresh-method") === "morph" + get refreshMethod() { + return this.getSetting("refresh-method") } - get shouldPreserveScrollPosition() { - return this.getSetting("refresh-scroll") === "preserve" + get refreshScroll() { + return this.getSetting("refresh-scroll") } // Private diff --git a/src/core/drive/page_view.js b/src/core/drive/page_view.js index 1bcb04134..e746e87b3 100644 --- a/src/core/drive/page_view.js +++ b/src/core/drive/page_view.js @@ -16,7 +16,8 @@ export class PageView extends View { } renderPage(snapshot, isPreview = false, willRender = true, visit) { - const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage + const refreshMethod = visit?.refresh?.method || this.snapshot.refreshMethod + const shouldMorphPage = this.isPageRefresh(visit) && refreshMethod === "morph" const rendererClass = shouldMorphPage ? MorphingPageRenderer : PageRenderer const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender) @@ -60,7 +61,8 @@ export class PageView extends View { } shouldPreserveScrollPosition(visit) { - return this.isPageRefresh(visit) && this.snapshot.shouldPreserveScrollPosition + const refreshScroll = visit?.refresh?.scroll || this.snapshot.refreshScroll + return this.isPageRefresh(visit) && refreshScroll === "preserve" } get snapshot() { diff --git a/src/core/drive/visit.js b/src/core/drive/visit.js index ec7565979..7c3e73b46 100644 --- a/src/core/drive/visit.js +++ b/src/core/drive/visit.js @@ -12,7 +12,8 @@ const defaultOptions = { willRender: true, updateHistory: true, shouldCacheSnapshot: true, - acceptsStreamResponse: false + acceptsStreamResponse: false, + refresh: {} } export const TimingMetric = { @@ -72,7 +73,8 @@ export class Visit { updateHistory, shouldCacheSnapshot, acceptsStreamResponse, - direction + direction, + refresh } = { ...defaultOptions, ...options @@ -92,6 +94,7 @@ export class Visit { this.shouldCacheSnapshot = shouldCacheSnapshot this.acceptsStreamResponse = acceptsStreamResponse this.direction = direction || Direction[action] + this.refresh = refresh } get adapter() { diff --git a/src/core/session.js b/src/core/session.js index 1047d4463..3d268c28b 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -107,10 +107,11 @@ export class Session { } } - refresh(url, requestId) { + refresh(url, options = {}) { + const { method, requestId, scroll } = options const isRecentRequest = requestId && this.recentRequests.has(requestId) if (!isRecentRequest && !this.navigator.currentVisit) { - this.visit(url, { action: "replace", shouldCacheSnapshot: false }) + this.visit(url, { action: "replace", shouldCacheSnapshot: false, refresh: { method, scroll } }) } } diff --git a/src/core/streams/stream_actions.js b/src/core/streams/stream_actions.js index a038add0d..8fdfc26be 100644 --- a/src/core/streams/stream_actions.js +++ b/src/core/streams/stream_actions.js @@ -50,6 +50,10 @@ export const StreamActions = { }, refresh() { - session.refresh(this.baseURI, this.requestId) + const method = this.getAttribute("method") + const requestId = this.requestId + const scroll = this.getAttribute("scroll") + + session.refresh(this.baseURI, { method, requestId, scroll }) } } diff --git a/src/tests/functional/page_refresh_tests.js b/src/tests/functional/page_refresh_tests.js index 0b6945063..0ce44b296 100644 --- a/src/tests/functional/page_refresh_tests.js +++ b/src/tests/functional/page_refresh_tests.js @@ -177,6 +177,13 @@ test("uses morphing to update remote frames marked with refresh='morph'", async await expect(page.locator("#refresh-reload")).toHaveText("Loaded reloadable frame") }) +test("overrides the meta value to render with replace when the Turbo Stream has [method=replace] attribute", async ({ page }) => { + await page.goto("/src/tests/fixtures/page_refresh.html") + + await page.evaluate(() => document.body.insertAdjacentHTML("beforeend", ``)) + await nextEventNamed(page, "turbo:render", { renderMethod: "replace" }) +}) + test("don't refresh frames contained in [data-turbo-permanent] elements", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html") @@ -242,6 +249,18 @@ test("it preserves the scroll position when the turbo-refresh-scroll meta tag is await assertPageScroll(page, 10, 10) }) +test("overrides the meta value to reset the scroll position when the Turbo Stream has [scroll=reset] attribute", async ({ page }) => { + await page.goto("/src/tests/fixtures/page_refresh.html") + + await page.evaluate(() => window.scrollTo(10, 10)) + await assertPageScroll(page, 10, 10) + + await page.evaluate(() => document.body.insertAdjacentHTML("beforeend", ``)) + await nextEventNamed(page, "turbo:render", { renderMethod: "morph" }) + + await assertPageScroll(page, 0, 0) +}) + test("it does not preserve the scroll position on regular 'advance' navigations, despite of using a 'preserve' option", async ({ page }) => { await page.goto("/src/tests/fixtures/page_refresh.html")