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

New events for Astro's view transition API #9090

Merged
merged 14 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
26 changes: 23 additions & 3 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,30 @@ declare module 'astro:transitions' {

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
10 changes: 7 additions & 3 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ type Fallback = 'none' | 'animate' | 'swap';
export interface Props {
fallback?: Fallback;
handleForms?: boolean;
intraPageTransitions?: boolean;
martrapp marked this conversation as resolved.
Show resolved Hide resolved
}

const { fallback = 'animate', handleForms } = Astro.props;
const { fallback = 'animate', handleForms, intraPageTransitions } = Astro.props;
---

<style is:global>
Expand All @@ -26,13 +27,15 @@ const { fallback = 'animate', handleForms } = Astro.props;
<meta name="astro-view-transitions-enabled" content="true" />
<meta name="astro-view-transitions-fallback" content={fallback} />
{handleForms ? <meta name="astro-view-transitions-forms" content="true" /> : ''}
{intraPageTransitions && <meta name="astro-view-transitions-intra-page" content="true" />}
<script>
import type { Options } from 'astro:transitions/client';
import { supportsViewTransitions, navigate } from 'astro:transitions/client';
// NOTE: import from `astro/prefetch` as `astro:prefetch` requires the `prefetch` config to be enabled
// @ts-ignore
martrapp marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -77,6 +80,7 @@ const { fallback = 'animate', handleForms } = Astro.props;
ev.preventDefault();
navigate(link.href, {
history: link.dataset.astroHistory === 'replace' ? 'replace' : 'auto',
sourceElement: link,
});
});

Expand All @@ -94,7 +98,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 };
martrapp marked this conversation as resolved.
Show resolved Hide resolved
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/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ test.describe('View Transitions', () => {
await page.goBack();
locator = page.locator('#click-one-again');
await expect(locator).toBeInViewport();

await page.goForward(); // prevent preemptive close of browser
});

test('Scroll position restored when transitioning back to fragment', async ({ page, astro }) => {
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
7 changes: 5 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,20 @@ export interface TransitionAnimationPair {
new: TransitionAnimation | TransitionAnimation[];
}

export interface TransitionDirectionalAnimations {
export interface TransitionStrictDirectionalAnimations {
forwards: TransitionAnimationPair;
backwards: TransitionAnimationPair;
}

export type TransitionFreeDirectionalAnimations = Record<string, TransitionAnimationPair>;

export type TransitionAnimationValue =
| 'initial'
| 'slide'
| 'fade'
| 'none'
| TransitionDirectionalAnimations;
martrapp marked this conversation as resolved.
Show resolved Hide resolved
| TransitionStrictDirectionalAnimations
| TransitionFreeDirectionalAnimations;

// Allow users to extend this for astro-jsx.d.ts
// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down
9 changes: 7 additions & 2 deletions packages/astro/src/runtime/server/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,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;
}
9 changes: 6 additions & 3 deletions packages/astro/src/transitions/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { TransitionAnimationPair, TransitionDirectionalAnimations } from '../@types/astro.js';
import type {
TransitionAnimationPair,
TransitionStrictDirectionalAnimations,
} from '../@types/astro.js';

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

export function slide({
duration,
}: {
duration?: string | number;
} = {}): TransitionDirectionalAnimations {
} = {}): TransitionStrictDirectionalAnimations {
return {
forwards: {
old: [
Expand Down Expand Up @@ -50,7 +53,7 @@ export function fade({
duration,
}: {
duration?: string | number;
} = {}): TransitionDirectionalAnimations {
} = {}): TransitionStrictDirectionalAnimations {
const anim = {
old: {
name: 'astroFadeOut',
Expand Down
Loading
Loading