Skip to content

Commit

Permalink
New events for Astro's view transition API (#9090)
Browse files Browse the repository at this point in the history
* draft new view transition events

* initial state for PR

* remove intraPageTransitions flag based on review comments

* add createAnimationScope after review comments

* remove style elements from styles after review comments

* remove quotes from animation css to enable set:text

* added changeset

* move scrollRestoration call from popstate handler to scroll update

* Update .changeset/few-keys-heal.md

Co-authored-by: Sarah Rainsberger <[email protected]>

* Less confusing after following review comments

* Less confusing after following review comments

---------

Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
martrapp and sarah11918 authored Nov 22, 2023
1 parent ac908b7 commit c87223c
Show file tree
Hide file tree
Showing 10 changed files with 528 additions and 171 deletions.
16 changes: 16 additions & 0 deletions .changeset/few-keys-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'astro': minor
---
Take full control over the behavior of view transitions!

Three new events now complement the existing `astro:after-swap` and `astro:page-load` events:

``` javascript
astro:before-preparation // Control how the DOM and other resources of the target page are loaded
astro:after-preparation // Last changes before taking off? Remove that loading indicator? Here you go!
astro:before-swap // Control how the DOM is updated to match the new page
```

The `astro:before-*` events allow you to change properties and strategies of the view transition implementation.
The `astro:after-*` events are notifications that a phase is complete.
Head over to docs to see [the full view transitions lifecycle](https://docs.astro.build/en/guides/view-transitions/#lifecycle-events) including these new events!
27 changes: 24 additions & 3 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,38 @@ declare module 'astro:transitions' {
type TransitionModule = typeof import('./dist/transitions/index.js');
export const slide: TransitionModule['slide'];
export const fade: TransitionModule['fade'];
export const createAnimationScope: TransitionModule['createAnimationScope'];

type ViewTransitionsModule = typeof import('./components/ViewTransitions.astro');
export const ViewTransitions: ViewTransitionsModule['default'];
}

declare module 'astro:transitions/client' {
type TransitionRouterModule = typeof import('./dist/transitions/router.js');
export const supportsViewTransitions: TransitionRouterModule['supportsViewTransitions'];
export const transitionEnabledOnThisPage: TransitionRouterModule['transitionEnabledOnThisPage'];
export const navigate: TransitionRouterModule['navigate'];
export type Options = import('./dist/transitions/router.js').Options;

type TransitionUtilModule = typeof import('./dist/transitions/util.js');
export const supportsViewTransitions: TransitionUtilModule['supportsViewTransitions'];
export const getFallback: TransitionUtilModule['getFallback'];
export const transitionEnabledOnThisPage: TransitionUtilModule['transitionEnabledOnThisPage'];

export type Fallback = import('./dist/transitions/types.ts').Fallback;
export type Direction = import('./dist/transitions/types.ts').Direction;
export type NavigationTypeString = import('./dist/transitions/types.ts').NavigationTypeString;
export type Options = import('./dist/transitions/types.ts').Options;

type EventModule = typeof import('./dist/transitions/events.js');
export const TRANSITION_BEFORE_PREPARATION: EventModule['TRANSITION_BEFORE_PREPARATION'];
export const TRANSITION_AFTER_PREPARATION: EventModule['TRANSITION_AFTER_PREPARATION'];
export const TRANSITION_BEFORE_SWAP: EventModule['TRANSITION_BEFORE_SWAP'];
export const TRANSITION_AFTER_SWAP: EventModule['TRANSITION_AFTER_SWAP'];
export const TRANSITION_PAGE_LOAD: EventModule['TRANSITION_PAGE_LOAD'];
export type TransitionBeforePreparationEvent =
import('./dist/transitions/events.ts').TransitionBeforePreparationEvent;
export type TransitionBeforeSwapEvent =
import('./dist/transitions/events.ts').TransitionBeforeSwapEvent;
export const isTransitionBeforePreparationEvent: EventModule['isTransitionBeforePreparationEvent'];
export const isTransitionBeforeSwapEvent: EventModule['isTransitionBeforeSwapEvent'];
}

declare module 'astro:prefetch' {
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
// @ts-ignore
import { init } from 'astro/prefetch';

export type Fallback = 'none' | 'animate' | 'swap';
type Fallback = 'none' | 'animate' | 'swap';

function getFallback(): Fallback {
const el = document.querySelector('[name="astro-view-transitions-fallback"]');
Expand Down Expand Up @@ -85,6 +85,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
ev.preventDefault();
navigate(href, {
history: link.dataset.astroHistory === 'replace' ? 'replace' : 'auto',
sourceElement: link,
});
});

Expand All @@ -102,7 +103,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
let action = submitter?.getAttribute('formaction') ?? form.action ?? location.pathname;
const method = submitter?.getAttribute('formmethod') ?? form.method;

const options: Options = {};
const options: Options = { sourceElement: submitter ?? form };
if (method === 'get') {
const params = new URLSearchParams(formData as any);
const url = new URL(action);
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@
"default": "./dist/core/middleware/namespace.js"
},
"./transitions": "./dist/transitions/index.js",
"./transitions/events": "./dist/transitions/events.js",
"./transitions/router": "./dist/transitions/router.js",
"./transitions/types": "./dist/transitions/types.js",
"./prefetch": "./dist/prefetch/index.js",
"./i18n": "./dist/i18n/index.js"
},
Expand Down
45 changes: 36 additions & 9 deletions packages/astro/src/runtime/server/transition.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type {
SSRResult,
TransitionAnimation,
TransitionAnimationPair,
TransitionAnimationValue,
TransitionDirectionalAnimations,
} from '../../@types/astro.js';
import { fade, slide } from '../../transitions/index.js';
import { markHTMLString } from './escape.js';
Expand Down Expand Up @@ -34,6 +36,19 @@ const getAnimations = (name: TransitionAnimationValue) => {
if (typeof name === 'object') return name;
};

const addPairs = (
animations: TransitionDirectionalAnimations | Record<string, TransitionAnimationPair>,
stylesheet: ViewTransitionStyleSheet
) => {
for (const [direction, images] of Object.entries(animations) as Entries<typeof animations>) {
for (const [image, rules] of Object.entries(images) as Entries<
(typeof animations)[typeof direction]
>) {
stylesheet.addAnimationPair(direction, image, rules);
}
}
};

export function renderTransition(
result: SSRResult,
hash: string,
Expand All @@ -48,13 +63,7 @@ export function renderTransition(

const animations = getAnimations(animationName);
if (animations) {
for (const [direction, images] of Object.entries(animations) as Entries<typeof animations>) {
for (const [image, rules] of Object.entries(images) as Entries<
(typeof animations)[typeof direction]
>) {
sheet.addAnimationPair(direction, image, rules);
}
}
addPairs(animations, sheet);
} else if (animationName === 'none') {
sheet.addFallback('old', 'animation: none; mix-blend-mode: normal;');
sheet.addModern('old', 'animation: none; opacity: 0; mix-blend-mode: normal;');
Expand All @@ -65,6 +74,19 @@ export function renderTransition(
return scope;
}

export function createAnimationScope(
transitionName: string,
animations: Record<string, TransitionAnimationPair>
) {
const hash = Math.random().toString(36).slice(2, 8);
const scope = `astro-${hash}`;
const sheet = new ViewTransitionStyleSheet(scope, transitionName);

addPairs(animations, sheet);

return { scope, styles: sheet.toString().replaceAll('"', '') };
}

class ViewTransitionStyleSheet {
private modern: string[] = [];
private fallback: string[] = [];
Expand Down Expand Up @@ -113,13 +135,18 @@ class ViewTransitionStyleSheet {
}

addAnimationPair(
direction: 'forwards' | 'backwards',
direction: 'forwards' | 'backwards' | string,
image: 'old' | 'new',
rules: TransitionAnimation | TransitionAnimation[]
) {
const { scope, name } = this;
const animation = stringifyAnimation(rules);
const prefix = direction === 'backwards' ? `[data-astro-transition=back]` : '';
const prefix =
direction === 'backwards'
? `[data-astro-transition=back]`
: direction === 'forwards'
? ''
: `[data-astro-transition=${direction}]`;
this.addRule('modern', `${prefix}::view-transition-${image}(${name}) { ${animation} }`);
this.addRule(
'fallback',
Expand Down
184 changes: 184 additions & 0 deletions packages/astro/src/transitions/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { updateScrollPosition } from './router.js';
import type { Direction, NavigationTypeString } from './types.js';

export const TRANSITION_BEFORE_PREPARATION = 'astro:before-preparation';
export const TRANSITION_AFTER_PREPARATION = 'astro:after-preparation';
export const TRANSITION_BEFORE_SWAP = 'astro:before-swap';
export const TRANSITION_AFTER_SWAP = 'astro:after-swap';
export const TRANSITION_PAGE_LOAD = 'astro:page-load';

type Events =
| typeof TRANSITION_AFTER_PREPARATION
| typeof TRANSITION_AFTER_SWAP
| typeof TRANSITION_PAGE_LOAD;
export const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
export const onPageLoad = () => triggerEvent(TRANSITION_PAGE_LOAD);

/*
* Common stuff
*/
class BeforeEvent extends Event {
readonly from: URL;
to: URL;
direction: Direction | string;
readonly navigationType: NavigationTypeString;
readonly sourceElement: Element | undefined;
readonly info: any;
newDocument: Document;

constructor(
type: string,
eventInitDict: EventInit | undefined,
from: URL,
to: URL,
direction: Direction | string,
navigationType: NavigationTypeString,
sourceElement: Element | undefined,
info: any,
newDocument: Document
) {
super(type, eventInitDict);
this.from = from;
this.to = to;
this.direction = direction;
this.navigationType = navigationType;
this.sourceElement = sourceElement;
this.info = info;
this.newDocument = newDocument;

Object.defineProperties(this, {
from: { enumerable: true },
to: { enumerable: true, writable: true },
direction: { enumerable: true, writable: true },
navigationType: { enumerable: true },
sourceElement: { enumerable: true },
info: { enumerable: true },
newDocument: { enumerable: true, writable: true },
});
}
}

/*
* TransitionBeforePreparationEvent
*/
export const isTransitionBeforePreparationEvent = (
value: any
): value is TransitionBeforePreparationEvent => value.type === TRANSITION_BEFORE_PREPARATION;
export class TransitionBeforePreparationEvent extends BeforeEvent {
formData: FormData | undefined;
loader: () => Promise<void>;
constructor(
from: URL,
to: URL,
direction: Direction | string,
navigationType: NavigationTypeString,
sourceElement: Element | undefined,
info: any,
newDocument: Document,
formData: FormData | undefined,
loader: (event: TransitionBeforePreparationEvent) => Promise<void>
) {
super(
TRANSITION_BEFORE_PREPARATION,
{ cancelable: true },
from,
to,
direction,
navigationType,
sourceElement,
info,
newDocument
);
this.formData = formData;
this.loader = loader.bind(this, this);
Object.defineProperties(this, {
formData: { enumerable: true },
loader: { enumerable: true, writable: true },
});
}
}

/*
* TransitionBeforeSwapEvent
*/

export const isTransitionBeforeSwapEvent = (value: any): value is TransitionBeforeSwapEvent =>
value.type === TRANSITION_BEFORE_SWAP;
export class TransitionBeforeSwapEvent extends BeforeEvent {
readonly direction: Direction | string;
readonly viewTransition: ViewTransition;
swap: () => void;

constructor(
afterPreparation: BeforeEvent,
viewTransition: ViewTransition,
swap: (event: TransitionBeforeSwapEvent) => void
) {
super(
TRANSITION_BEFORE_SWAP,
undefined,
afterPreparation.from,
afterPreparation.to,
afterPreparation.direction,
afterPreparation.navigationType,
afterPreparation.sourceElement,
afterPreparation.info,
afterPreparation.newDocument
);
this.direction = afterPreparation.direction;
this.viewTransition = viewTransition;
this.swap = swap.bind(this, this);

Object.defineProperties(this, {
direction: { enumerable: true },
viewTransition: { enumerable: true },
swap: { enumerable: true, writable: true },
});
}
}

export async function doPreparation(
from: URL,
to: URL,
direction: Direction | string,
navigationType: NavigationTypeString,
sourceElement: Element | undefined,
info: any,
formData: FormData | undefined,
defaultLoader: (event: TransitionBeforePreparationEvent) => Promise<void>
) {
const event = new TransitionBeforePreparationEvent(
from,
to,
direction,
navigationType,
sourceElement,
info,
window.document,
formData,
defaultLoader
);
if (document.dispatchEvent(event)) {
await event.loader();
if (!event.defaultPrevented) {
triggerEvent(TRANSITION_AFTER_PREPARATION);
if (event.navigationType !== 'traverse') {
// save the current scroll position before we change the DOM and transition to the new page
updateScrollPosition({ scrollX, scrollY });
}
}
}
return event;
}

export async function doSwap(
afterPreparation: BeforeEvent,
viewTransition: ViewTransition,
defaultSwap: (event: TransitionBeforeSwapEvent) => void
) {
const event = new TransitionBeforeSwapEvent(afterPreparation, viewTransition, defaultSwap);
document.dispatchEvent(event);
event.swap();
return event;
}
1 change: 1 addition & 0 deletions packages/astro/src/transitions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro.js';
export { createAnimationScope } from '../runtime/server/transition.js';

const EASE_IN_OUT_QUART = 'cubic-bezier(0.76, 0, 0.24, 1)';

Expand Down
Loading

0 comments on commit c87223c

Please sign in to comment.