From 151aca23ebc4eff11310cb24a619a535f3772aa2 Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 27 Jun 2023 22:00:22 +0100 Subject: [PATCH] Support View Transition API for navigations (#935) Adds the ability to use the View Transitions API. It's based around the MPA View Transitions support which is currently becoming available in Chrome. When navigating between pages, we check for the presence of `view-transition` meta tags in both documents, with a value of `same-origin`. If those tags are present, and the browser supports the View Transitions API, we can render the page update within a transition. When not opted in with the meta tags, or when support is not available, we fallback to the previous behaviour. This mimics the behaviour that a supporting browser would have when performing a full-page navigation between those pages. We also suppress snapshot caching on pages that specify these meta tags, since the snapshots would interfere with the animations. Note that while the API is based on MPA view transitions, the implementation only requires SPA view transitions, which is already available in the latest Chrome versions. Also note that this is based on an API that is very new, and not yet widely supported. We'll want to keep an eye on how that develops and update our implementation accordingly. --- src/core/drive/page_snapshot.ts | 8 +++-- src/core/drive/page_view.ts | 6 +++- src/core/drive/view_transitions.ts | 17 ++++++++++ src/tests/fixtures/transitions/left.html | 31 +++++++++++++++++++ src/tests/fixtures/transitions/other.html | 13 ++++++++ src/tests/fixtures/transitions/right.html | 30 ++++++++++++++++++ .../functional/drive_view_transition_tests.ts | 30 ++++++++++++++++++ 7 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/core/drive/view_transitions.ts create mode 100644 src/tests/fixtures/transitions/left.html create mode 100644 src/tests/fixtures/transitions/other.html create mode 100644 src/tests/fixtures/transitions/right.html create mode 100644 src/tests/functional/drive_view_transition_tests.ts diff --git a/src/core/drive/page_snapshot.ts b/src/core/drive/page_snapshot.ts index 8b0172e7b..4da80ca31 100644 --- a/src/core/drive/page_snapshot.ts +++ b/src/core/drive/page_snapshot.ts @@ -56,17 +56,21 @@ export class PageSnapshot extends Snapshot { } get isPreviewable() { - return this.cacheControlValue != "no-preview" + return this.cacheControlValue != "no-preview" && !this.prefersViewTransitions } get isCacheable() { - return this.cacheControlValue != "no-cache" + return this.cacheControlValue != "no-cache" && !this.prefersViewTransitions } get isVisitable() { return this.getSetting("visit-control") != "reload" } + get prefersViewTransitions() { + return this.headSnapshot.getMetaValue("view-transition") === "same-origin" + } + // Private getSetting(name: string) { diff --git a/src/core/drive/page_view.ts b/src/core/drive/page_view.ts index 80635ab38..632b4665e 100644 --- a/src/core/drive/page_view.ts +++ b/src/core/drive/page_view.ts @@ -4,6 +4,7 @@ import { ErrorRenderer } from "./error_renderer" import { PageRenderer } from "./page_renderer" import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" +import { withViewTransition } from "./view_transitions" import { Visit } from "./visit" export type PageViewRenderOptions = ViewRenderOptions @@ -20,13 +21,16 @@ export class PageView extends View this.render(renderer)) } renderError(snapshot: PageSnapshot, visit?: Visit) { diff --git a/src/core/drive/view_transitions.ts b/src/core/drive/view_transitions.ts new file mode 100644 index 000000000..1f6c0f89d --- /dev/null +++ b/src/core/drive/view_transitions.ts @@ -0,0 +1,17 @@ +declare global { + type ViewTransition = { + finished: Promise + } + + interface Document { + startViewTransition?(callback: () => void): ViewTransition + } +} + +export function withViewTransition(shouldTransition: boolean, callback: () => Promise): Promise { + if (shouldTransition && document.startViewTransition) { + return document.startViewTransition(callback).finished + } else { + return callback() + } +} diff --git a/src/tests/fixtures/transitions/left.html b/src/tests/fixtures/transitions/left.html new file mode 100644 index 000000000..b45a070b1 --- /dev/null +++ b/src/tests/fixtures/transitions/left.html @@ -0,0 +1,31 @@ + + + + + Left + + + + + + + +

Left

+

go right

+
+

go other

+ + diff --git a/src/tests/fixtures/transitions/other.html b/src/tests/fixtures/transitions/other.html new file mode 100644 index 000000000..2310d8fcc --- /dev/null +++ b/src/tests/fixtures/transitions/other.html @@ -0,0 +1,13 @@ + + + + + Other + + + + +

Other

+

go left

+ + diff --git a/src/tests/fixtures/transitions/right.html b/src/tests/fixtures/transitions/right.html new file mode 100644 index 000000000..a986bc777 --- /dev/null +++ b/src/tests/fixtures/transitions/right.html @@ -0,0 +1,30 @@ + + + + + Right + + + + + + + +

Right

+

go left

+
+ + diff --git a/src/tests/functional/drive_view_transition_tests.ts b/src/tests/functional/drive_view_transition_tests.ts new file mode 100644 index 000000000..e4c9f48fd --- /dev/null +++ b/src/tests/functional/drive_view_transition_tests.ts @@ -0,0 +1,30 @@ +import { test } from "@playwright/test" +import { assert } from "chai" +import { nextBody } from "../helpers/page" + +test.beforeEach(async ({ page }) => { + await page.goto("/src/tests/fixtures/transitions/left.html") + + await page.evaluate(` + document.startViewTransition = (callback) => { + window.startViewTransitionCalled = true + callback() + } + `) +}) + +test("navigating triggers the view transition", async ({ page }) => { + await page.locator("#go-right").click() + await nextBody(page) + + const called = await page.evaluate(`window.startViewTransitionCalled`) + assert.isTrue(called) +}) + +test("navigating does not trigger a view transition when meta tag not present", async ({ page }) => { + await page.locator("#go-other").click() + await nextBody(page) + + const called = await page.evaluate(`window.startViewTransitionCalled`) + assert.isUndefined(called) +})