From 5b84fed0cbbe54a749caea1d255fa110fc70b9c3 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Thu, 9 Jan 2025 20:23:15 +0100 Subject: [PATCH 01/10] feat: dispatch q:viewTransition when started --- packages/qwik/src/core/render/dom/visitor.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/qwik/src/core/render/dom/visitor.ts b/packages/qwik/src/core/render/dom/visitor.ts index a78265db2fe..91cdd3417e6 100644 --- a/packages/qwik/src/core/render/dom/visitor.ts +++ b/packages/qwik/src/core/render/dom/visitor.ts @@ -1172,10 +1172,15 @@ export const executeContextWithScrollAndTransition = async (ctx: RenderStaticCon if (document.__q_view_transition__) { document.__q_view_transition__ = undefined; if (document.startViewTransition) { - await document.startViewTransition(() => { + const transition = document.startViewTransition(() => { executeDOMRender(ctx); restoreScroll(); - }).finished; + }); + const event = new CustomEvent('q:viewTransition', { + detail: transition, + }); + document.dispatchEvent(event); + await transition.finished; return; } } From 0bcac97476c6f3f4ad7950fcc508311c9d7294e0 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Thu, 9 Jan 2025 20:40:06 +0100 Subject: [PATCH 02/10] add changeset --- .changeset/plenty-books-sin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-books-sin.md diff --git a/.changeset/plenty-books-sin.md b/.changeset/plenty-books-sin.md new file mode 100644 index 00000000000..0cc78bcc852 --- /dev/null +++ b/.changeset/plenty-books-sin.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik': patch +--- + +Emit an CustomEvent `q:viewTransition` when view transition starts. From 2e8be1c82bbdd6e9454b5bdff06426320b03980f Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Thu, 9 Jan 2025 21:17:43 +0100 Subject: [PATCH 03/10] add documentation --- .../docs/cookbook/view-transition/index.mdx | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 packages/docs/src/routes/docs/cookbook/view-transition/index.mdx diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx new file mode 100644 index 00000000000..b99a67db2af --- /dev/null +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -0,0 +1,116 @@ +--- +title: Cookbook | View Transition API +contributors: + - GrandSchtroumpf +--- + +# View Transition API +By default Qwik will start a view transition when SPA navigation. We can run animation either with CSS or WAAPI + +## CSS +```tsx +export default component$(({ list }) => { + return ( +
    + {list.map((item) => ( + // Create a name per item +
  • ...
  • + ))} +
+ ) +}) +``` + +```css +.item { + /* Alias to target all .item with a view-transition-name */ + view-transition-class: animated-item; +} +/* Animate when item didn't exist in the previous page */ +::view-transition-new(.animated-item)::only-child { + animation: fade-in 200ms; +} +/* Animate when item doesn't exist in the next page */ +::view-transition-old(.animated-item)::only-child { + animation: fade-out 200ms; +} +``` + +Sometime we need to have some specific logic before the animation start. In this case you can listen to the `q:viewTransition` event. + +For example if you want to only animate visible element: +```tsx +export default component$(() => { + useOnDocument('q:viewTransition', $(async (event: CustomEvent) => { + const transition = event.detail; + await transition.ready; // Wait for ::view-transition pseudo class exist + const items = document.querySelectorAll('.item'); + for (const item of items) { + if (!item.checkVisibility()) continue; + item.dataset.hasViewTransition = true; + } + })) + return ( +
    + {list.map((item) => ( + // Create a name per item +
  • ...
  • + ))} +
+ ) +}) +``` + +```css +.item[data-has-view-transition="true"] { + view-transition-class: animated-item; +} +::view-transition-new(.animated-item)::only-child { + animation: fade-in 200ms; +} +::view-transition-old(.animated-item)::only-child { + animation: fade-out 200ms; +} +``` + +## WAAPI +With Web Animation API you can get more precise, but for that we need to wait for the ::view-transition pseudo-class to exist in the DOM. To achieve that you can wait the `transition.ready` promise. + +In this example we add some delay for each item : +```tsx +export default component$(() => { + useOnDocument('q:viewTransition', $(async (event: CustomEvent) => { + // Get visible item + const items = document.querySelectorAll('.item'); + const visible = Array.from(items).map((item) => item.checkVisibility()); + + // Wait for ::view-transition pseudo-class to exist + const transition = event.detail; + await transition.ready; + + // Animate each leaving item + for (let i = 0; i < visible.lenth; i++) { + const name = visible.style.viewTransitionName; + // Note: we animate the element + document.documentElement.animate({ + opacity: 0 + }, { + // Target the pseudo-class inside the element + pseudoElement: `::view-transition-old(${name})::only-child`, + duration: 200, + fill: "forward", + delay: i * 50, // Add delay for each pseudo-class + + }) + } + })) + return ( +
    + {list.map((item) => ( + // Create a name per item +
  • ...
  • + ))} +
+ ) +}) +``` \ No newline at end of file From 2075fc0e43657b1ba9657357c007fdab1c861d76 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 10:27:16 +0100 Subject: [PATCH 04/10] replace q:viewTransition for qviewTransition --- .changeset/plenty-books-sin.md | 2 +- .../src/routes/docs/cookbook/view-transition/index.mdx | 9 ++++----- packages/qwik/src/core/render/dom/visitor.ts | 2 +- .../src/core/render/jsx/types/jsx-qwik-attributes.ts | 2 ++ .../qwik/src/core/render/jsx/types/jsx-qwik-events.ts | 2 ++ 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.changeset/plenty-books-sin.md b/.changeset/plenty-books-sin.md index 0cc78bcc852..4294bd000a9 100644 --- a/.changeset/plenty-books-sin.md +++ b/.changeset/plenty-books-sin.md @@ -2,4 +2,4 @@ '@builder.io/qwik': patch --- -Emit an CustomEvent `q:viewTransition` when view transition starts. +Emit an CustomEvent `qviewTransition` when view transition starts. diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx index b99a67db2af..692988109bb 100644 --- a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -36,12 +36,12 @@ export default component$(({ list }) => { } ``` -Sometime we need to have some specific logic before the animation start. In this case you can listen to the `q:viewTransition` event. +Sometime we need to have some specific logic before the animation start. In this case you can listen to the `qviewTransition` event. For example if you want to only animate visible element: ```tsx export default component$(() => { - useOnDocument('q:viewTransition', $(async (event: CustomEvent) => { + useOnDocument('qviewTransition', $(async (event: CustomEvent) => { const transition = event.detail; await transition.ready; // Wait for ::view-transition pseudo class exist const items = document.querySelectorAll('.item'); @@ -79,7 +79,7 @@ With Web Animation API you can get more precise, but for that we need to wait fo In this example we add some delay for each item : ```tsx export default component$(() => { - useOnDocument('q:viewTransition', $(async (event: CustomEvent) => { + useOnDocument('qviewTransition', $(async (event: CustomEvent) => { // Get visible item const items = document.querySelectorAll('.item'); const visible = Array.from(items).map((item) => item.checkVisibility()); @@ -90,7 +90,7 @@ export default component$(() => { // Animate each leaving item for (let i = 0; i < visible.lenth; i++) { - const name = visible.style.viewTransitionName; + const name = visible[i].style.viewTransitionName; // Note: we animate the element document.documentElement.animate({ opacity: 0 @@ -100,7 +100,6 @@ export default component$(() => { duration: 200, fill: "forward", delay: i * 50, // Add delay for each pseudo-class - }) } })) diff --git a/packages/qwik/src/core/render/dom/visitor.ts b/packages/qwik/src/core/render/dom/visitor.ts index 91cdd3417e6..401749980ba 100644 --- a/packages/qwik/src/core/render/dom/visitor.ts +++ b/packages/qwik/src/core/render/dom/visitor.ts @@ -1176,7 +1176,7 @@ export const executeContextWithScrollAndTransition = async (ctx: RenderStaticCon executeDOMRender(ctx); restoreScroll(); }); - const event = new CustomEvent('q:viewTransition', { + const event = new CustomEvent('qviewTransition', { detail: transition, }); document.dispatchEvent(event); diff --git a/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts b/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts index c1af2d50c34..b713873224c 100644 --- a/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts +++ b/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts @@ -5,6 +5,7 @@ import type { QwikIdleEvent, QwikInitEvent, QwikSymbolEvent, + QwikViewTransitionEvent, QwikVisibleEvent, } from './jsx-qwik-events'; @@ -113,6 +114,7 @@ type AllEventMapRaw = HTMLElementEventMap & qinit: QwikInitEvent; qsymbol: QwikSymbolEvent; qvisible: QwikVisibleEvent; + qviewTransition: QwikViewTransitionEvent; }; /** This corrects the TS definition for ToggleEvent @public */ diff --git a/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts b/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts index 29c992a2aba..a4e020ae00e 100644 --- a/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts +++ b/packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts @@ -8,6 +8,8 @@ export type QwikSymbolEvent = CustomEvent<{ symbol: string; element: Element; re export type QwikInitEvent = CustomEvent<{}>; /** Emitted by qwik-loader on document when the document first becomes idle @public */ export type QwikIdleEvent = CustomEvent<{}>; +/** Emitted by qwik-core on document when the a view transition start @public */ +export type QwikViewTransitionEvent = CustomEvent; // Utility types for supporting autocompletion in union types From 783d3677e42854da3d5d15d5dd0f21cbb4059df2 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 10:30:02 +0100 Subject: [PATCH 05/10] move QwikTransitionEvent up --- packages/qwik/src/core/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index 305f8e108be..7fb3c218639 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -139,6 +139,7 @@ export type { QwikVisibleEvent, QwikIdleEvent, QwikInitEvent, + QwikTransitionEvent, // old NativeAnimationEvent, NativeClipboardEvent, @@ -166,7 +167,6 @@ export type { QwikTouchEvent, QwikUIEvent, QwikWheelEvent, - QwikTransitionEvent, } from './render/jsx/types/jsx-qwik-events'; ////////////////////////////////////////////////////////////////////////////////////////// From 82282a1ab51cc1ec4a0cc0d04f09d00a32076228 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 10:38:00 +0100 Subject: [PATCH 06/10] doc: fix example --- .../src/routes/docs/cookbook/view-transition/index.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx index 692988109bb..b67d47f15c0 100644 --- a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -27,11 +27,11 @@ export default component$(({ list }) => { view-transition-class: animated-item; } /* Animate when item didn't exist in the previous page */ -::view-transition-new(.animated-item)::only-child { +::view-transition-new(.animated-item):only-child { animation: fade-in 200ms; } /* Animate when item doesn't exist in the next page */ -::view-transition-old(.animated-item)::only-child { +::view-transition-old(.animated-item):only-child { animation: fade-out 200ms; } ``` @@ -65,10 +65,10 @@ export default component$(() => { .item[data-has-view-transition="true"] { view-transition-class: animated-item; } -::view-transition-new(.animated-item)::only-child { +::view-transition-new(.animated-item):only-child { animation: fade-in 200ms; } -::view-transition-old(.animated-item)::only-child { +::view-transition-old(.animated-item):only-child { animation: fade-out 200ms; } ``` @@ -96,7 +96,7 @@ export default component$(() => { opacity: 0 }, { // Target the pseudo-class inside the element - pseudoElement: `::view-transition-old(${name})::only-child`, + pseudoElement: `::view-transition-old(${name}):only-child`, duration: 200, fill: "forward", delay: i * 50, // Add delay for each pseudo-class From dcf30f459489d502f6f6e4d382b95eb354016271 Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Fri, 10 Jan 2025 11:14:28 +0100 Subject: [PATCH 07/10] doc: improve examples --- .../docs/cookbook/view-transition/index.mdx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx index b67d47f15c0..b49e7717747 100644 --- a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -41,9 +41,9 @@ Sometime we need to have some specific logic before the animation start. In this For example if you want to only animate visible element: ```tsx export default component$(() => { - useOnDocument('qviewTransition', $(async (event: CustomEvent) => { + // In this case we need the callback to be sync, else the transition might have already happened + useOnDocument('qviewTransition', sync$((event: CustomEvent) => { const transition = event.detail; - await transition.ready; // Wait for ::view-transition pseudo class exist const items = document.querySelectorAll('.item'); for (const item of items) { if (!item.checkVisibility()) continue; @@ -80,23 +80,24 @@ In this example we add some delay for each item : ```tsx export default component$(() => { useOnDocument('qviewTransition', $(async (event: CustomEvent) => { - // Get visible item + // Get visible item's viewTransitionName (should happen before transition is ready) const items = document.querySelectorAll('.item'); - const visible = Array.from(items).map((item) => item.checkVisibility()); + const names = Array.from(items) + .filter((item) => item.checkVisibility()) + .map((item) => item.style.viewTransitionName); // Wait for ::view-transition pseudo-class to exist const transition = event.detail; await transition.ready; // Animate each leaving item - for (let i = 0; i < visible.lenth; i++) { - const name = visible[i].style.viewTransitionName; + for (let i = 0; i < names.lenth; i++) { // Note: we animate the element document.documentElement.animate({ opacity: 0 }, { // Target the pseudo-class inside the element - pseudoElement: `::view-transition-old(${name}):only-child`, + pseudoElement: `::view-transition-old(${names[i]}):only-child`, duration: 200, fill: "forward", delay: i * 50, // Add delay for each pseudo-class From 25eed2b4000e5f73eddecf4c95a30e3062ebbdee Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Tue, 14 Jan 2025 16:05:41 +0100 Subject: [PATCH 08/10] doc: expose new page --- packages/docs/src/routes/docs/cookbook/index.mdx | 1 + packages/docs/src/routes/docs/menu.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/docs/src/routes/docs/cookbook/index.mdx b/packages/docs/src/routes/docs/cookbook/index.mdx index bee807e0782..2166b350fc3 100644 --- a/packages/docs/src/routes/docs/cookbook/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/index.mdx @@ -32,3 +32,4 @@ Examples: - [Synchronous Events with State](./sync-events/) - [Theme Management](./theme-management/) - [Drag & Drop](./drag&drop/) +- [View Transition](./view-transition/) \ No newline at end of file diff --git a/packages/docs/src/routes/docs/menu.md b/packages/docs/src/routes/docs/menu.md index 98b58646ff3..2d4d06e6d47 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -52,6 +52,7 @@ - [Sync events w state](/docs/cookbook/sync-events/index.mdx) - [Theme Management](/docs/cookbook/theme-management/index.mdx) - [Drag & Drop](/docs/cookbook/drag&drop/index.mdx) +- [View Transition](/docs/cookbook/view-transition/index.mdx) ## Integrations From f583ec9dbdf90b89f9563ce808347d5b296f103e Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Tue, 14 Jan 2025 17:52:05 +0100 Subject: [PATCH 09/10] doc: add indication about ViewTransition interface --- .../docs/src/routes/docs/cookbook/view-transition/index.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx index b49e7717747..3dc29445d82 100644 --- a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -73,6 +73,8 @@ export default component$(() => { } ``` +> **Note**: `ViewTransition` interface is available with Typescript >5.6. + ## WAAPI With Web Animation API you can get more precise, but for that we need to wait for the ::view-transition pseudo-class to exist in the DOM. To achieve that you can wait the `transition.ready` promise. From 5e7dee5ca2b95bb3f65e2ef5e5f76b5754d3991a Mon Sep 17 00:00:00 2001 From: Grandschtroumpf Date: Mon, 27 Jan 2025 10:24:01 +0100 Subject: [PATCH 10/10] doc: fix doc typo and add explaination --- .../docs/cookbook/view-transition/index.mdx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx index 3dc29445d82..d0170bb0238 100644 --- a/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/view-transition/index.mdx @@ -5,7 +5,7 @@ contributors: --- # View Transition API -By default Qwik will start a view transition when SPA navigation. We can run animation either with CSS or WAAPI +By default Qwik will start a view transition when SPA navigation. We can run animation either with CSS or WAAPI. ## CSS ```tsx @@ -76,33 +76,43 @@ export default component$(() => { > **Note**: `ViewTransition` interface is available with Typescript >5.6. ## WAAPI -With Web Animation API you can get more precise, but for that we need to wait for the ::view-transition pseudo-class to exist in the DOM. To achieve that you can wait the `transition.ready` promise. +With Web Animation API you can get more precise, but for that we need to wait for the ::view-transition pseudo-element to exist in the DOM. To achieve that you can wait the `transition.ready` promise. In this example we add some delay for each item : ```tsx export default component$(() => { + // Remove default style on the pseudo-element. + useStyles$(` + li { + view-transition-class: items; + } + ::view-transition-old(.items) { + animation: none; + } + `); useOnDocument('qviewTransition', $(async (event: CustomEvent) => { // Get visible item's viewTransitionName (should happen before transition is ready) - const items = document.querySelectorAll('.item'); + const items = document.querySelectorAll('.item'); const names = Array.from(items) .filter((item) => item.checkVisibility()) .map((item) => item.style.viewTransitionName); - // Wait for ::view-transition pseudo-class to exist + // Wait for ::view-transition pseudo-element to exist const transition = event.detail; await transition.ready; // Animate each leaving item - for (let i = 0; i < names.lenth; i++) { + for (let i = 0; i < names.length; i++) { // Note: we animate the element document.documentElement.animate({ - opacity: 0 + opacity: 0, + transform: 'scale(0.9)' }, { - // Target the pseudo-class inside the element - pseudoElement: `::view-transition-old(${names[i]}):only-child`, + // Target the pseudo-element inside the element + pseudoElement: `::view-transition-old(${names[i]})`, duration: 200, - fill: "forward", - delay: i * 50, // Add delay for each pseudo-class + fill: "forwards", + delay: i * 50, // Add delay for each pseudo-element }) } })) @@ -115,4 +125,6 @@ export default component$(() => { ) }) -``` \ No newline at end of file +``` + +> **Note**: For it to work correctly, we need to **remove the default view transition** animation else it happens on top of the `.animate()`. I'm using `view-transition-class` which is only working with Chrome right now.