Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: View Transition hook #7237

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-books-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@builder.io/qwik': patch
---

Emit an CustomEvent `qviewTransition` when view transition starts.
1 change: 1 addition & 0 deletions packages/docs/src/routes/docs/cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ Examples:
- [Synchronous Events with State](./sync-events/)
- [Theme Management](./theme-management/)
- [Drag & Drop](./drag&drop/)
- [View Transition](./view-transition/)
130 changes: 130 additions & 0 deletions packages/docs/src/routes/docs/cookbook/view-transition/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
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 (
<ul>
{list.map((item) => (
// Create a name per item
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
))}
</ul>
)
})
```

```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 `qviewTransition` event.

For example if you want to only animate visible element:
```tsx
export default component$(() => {
// In this case we need the callback to be sync, else the transition might have already happened
useOnDocument('qviewTransition', sync$((event: CustomEvent<ViewTransition>) => {
const transition = event.detail;
const items = document.querySelectorAll('.item');
for (const item of items) {
if (!item.checkVisibility()) continue;
item.dataset.hasViewTransition = true;
}
}))
return (
<ul>
{list.map((item) => (
// Create a name per item
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
))}
</ul>
)
})
```

```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;
}
```

> **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-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<ViewTransition>) => {
// Get visible item's viewTransitionName (should happen before transition is ready)
const items = document.querySelectorAll<HTMLElement>('.item');
const names = Array.from(items)
.filter((item) => item.checkVisibility())
.map((item) => item.style.viewTransitionName);

// 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.length; i++) {
// Note: we animate the <html> element
document.documentElement.animate({
opacity: 0,
transform: 'scale(0.9)'
}, {
// Target the pseudo-element inside the <html> element
pseudoElement: `::view-transition-old(${names[i]})`,
duration: 200,
fill: "forwards",
delay: i * 50, // Add delay for each pseudo-element
})
}
}))
return (
<ul>
{list.map((item) => (
// Create a name per item
<li key={item.id} class="item" style={{viewTransitionName: `_${item.id}_`}}>...</li>
))}
</ul>
)
})
```

> **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.
1 change: 1 addition & 0 deletions packages/docs/src/routes/docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export type {
QwikVisibleEvent,
QwikIdleEvent,
QwikInitEvent,
QwikTransitionEvent,
// old
NativeAnimationEvent,
NativeClipboardEvent,
Expand Down Expand Up @@ -166,7 +167,6 @@ export type {
QwikTouchEvent,
QwikUIEvent,
QwikWheelEvent,
QwikTransitionEvent,
} from './render/jsx/types/jsx-qwik-events';

//////////////////////////////////////////////////////////////////////////////////////////
Expand Down
9 changes: 7 additions & 2 deletions packages/qwik/src/core/render/dom/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('qviewTransition', {
detail: transition,
});
document.dispatchEvent(event);
await transition.finished;
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
QwikIdleEvent,
QwikInitEvent,
QwikSymbolEvent,
QwikViewTransitionEvent,
QwikVisibleEvent,
} from './jsx-qwik-events';

Expand Down Expand Up @@ -113,6 +114,7 @@ type AllEventMapRaw = HTMLElementEventMap &
qinit: QwikInitEvent;
qsymbol: QwikSymbolEvent;
qvisible: QwikVisibleEvent;
qviewTransition: QwikViewTransitionEvent;
};

/** This corrects the TS definition for ToggleEvent @public */
Expand Down
2 changes: 2 additions & 0 deletions packages/qwik/src/core/render/jsx/types/jsx-qwik-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ViewTransition>;

// Utility types for supporting autocompletion in union types

Expand Down
Loading