From e99f923aa50d49cff71bea945e893c8373e7dec1 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 10 Apr 2023 12:33:14 +0100 Subject: [PATCH] feat(nuxt): add experimental View Transitions API support (#20092) --- docs/1.getting-started/5.transitions.md | 32 +++++++++++ packages/nuxt/src/app/nuxt.ts | 1 + .../app/plugins/view-transitions.client.ts | 55 +++++++++++++++++++ packages/nuxt/src/core/nuxt.ts | 6 ++ packages/schema/src/config/experimental.ts | 7 +++ 5 files changed, 101 insertions(+) create mode 100644 packages/nuxt/src/app/plugins/view-transitions.client.ts diff --git a/docs/1.getting-started/5.transitions.md b/docs/1.getting-started/5.transitions.md index 2696b7dbc..082ad63cd 100644 --- a/docs/1.getting-started/5.transitions.md +++ b/docs/1.getting-started/5.transitions.md @@ -408,3 +408,35 @@ When `` is used in `app.vue`, transition-props can be passed directl ::alert{type="warning"} Remember, this page transition cannot be overridden with `definePageMeta` on individual pages. :: + +## View Transitions API (experimental) + +Nuxt ships with an experimental implementation of the [**View Transitions API**](https://developer.chrome.com/docs/web-platform/view-transitions/) (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API)). This is an exciting new way to implement native browser transitions which (among other things) have the ability to transition between unrelated elements on different pages. + +The Nuxt integration is under active development, but can be enabled with the `experimental.viewTransition` option in your configuration file: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + experimental: { + viewTransition: true + } +}) +``` + +If you are also using Vue transitions like `pageTransition` and `layoutTransition` (see above) to achieve the same result as the new View Transitions API, then you may wish to _disable_ Vue transitions if the user's browser supports the newer, native web API. You can do this by creating `~/middleware/disable-vue-transitions.global.ts` with the following contents: + +```js +export default defineNuxtRouteMiddleware(to => { + if (!document.startViewTransition) { return } + + // Disable built-in Vue transitions + to.meta.pageTransition = false + to.meta.layoutTransition = false +}) +``` + +### Known issues + +- View transitions may not work as expected with nested pages/layouts/async components owing to this upstream Vue bug: . If you make use of this pattern, you may need to delay adopting this experimental feature or implement it yourself. Feedback is very welcome. + +- If you perform data fetching within your page setup functions, that you may wish to reconsider using this feature for the moment. (By design, View Transitions completely freeze DOM updates whilst they are taking place.) We're looking at restrict the View Transition to the final moments before `` resolves, but in the interim you may want to consider carefully whether to adopt this feature if this describes you. diff --git a/packages/nuxt/src/app/nuxt.ts b/packages/nuxt/src/app/nuxt.ts index 35751ffe2..41812dd23 100644 --- a/packages/nuxt/src/app/nuxt.ts +++ b/packages/nuxt/src/app/nuxt.ts @@ -42,6 +42,7 @@ export interface RuntimeNuxtHooks { 'link:prefetch': (link: string) => HookResult 'page:start': (Component?: VNode) => HookResult 'page:finish': (Component?: VNode) => HookResult + 'page:transition:start': () => HookResult 'page:transition:finish': (Component?: VNode) => HookResult 'vue:setup': () => void 'vue:error': (...args: Parameters[0]>) => HookResult diff --git a/packages/nuxt/src/app/plugins/view-transitions.client.ts b/packages/nuxt/src/app/plugins/view-transitions.client.ts new file mode 100644 index 000000000..7489f585d --- /dev/null +++ b/packages/nuxt/src/app/plugins/view-transitions.client.ts @@ -0,0 +1,55 @@ +import { useRouter } from '#app/composables/router' +import { defineNuxtPlugin } from '#app/nuxt' + +export default defineNuxtPlugin((nuxtApp) => { + if (!document.startViewTransition) { return } + + let finishTransition: undefined | (() => void) + let abortTransition: undefined | (() => void) + + const router = useRouter() + + router.beforeResolve((to) => { + if (to.meta.pageTransition === false) { return } + + const promise = new Promise((resolve, reject) => { + finishTransition = resolve + abortTransition = reject + }) + + let changeRoute: () => void + const ready = new Promise(resolve => (changeRoute = resolve)) + + const transition = document.startViewTransition!(() => { + changeRoute() + return promise + }) + + transition.finished.then(() => { + abortTransition = undefined + finishTransition = undefined + }) + + return ready + }) + + nuxtApp.hook('vue:error', () => { + abortTransition?.() + abortTransition = undefined + }) + + nuxtApp.hook('page:finish', () => { + finishTransition?.() + finishTransition = undefined + }) +}) + +declare global { + interface Document { + startViewTransition?: (callback: () => Promise | void) => { + finished: Promise + updateCallbackDone: Promise + ready: Promise + } + } +} diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index e281c241e..45405aa0f 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -281,6 +281,12 @@ async function initNuxt (nuxt: Nuxt) { addPlugin(resolve(nuxt.options.appDir, 'plugins/restore-state.client')) } + // Add experimental automatic view transition api support + if (nuxt.options.experimental.viewTransition) { + addPlugin(resolve(nuxt.options.appDir, 'plugins/view-transitions.client')) + } + + // Add experimental support for custom types in JSON payload if (nuxt.options.experimental.renderJsonPayloads) { nuxt.hook('modules:done', () => { nuxt.options.plugins.unshift(resolve(nuxt.options.appDir, 'plugins/revive-payload.client')) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 36162e53c..70326c2e4 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -121,6 +121,13 @@ export default defineUntypedSchema({ /** Enable cross-origin prefetch using the Speculation Rules API. */ crossOriginPrefetch: false, + /** + * Enable View Transition API integration with client-side router. + * + * @see https://developer.chrome.com/docs/web-platform/view-transitions + */ + viewTransition: false, + /** * Write early hints when using node server. *