Add View Transitions announcer (#8621)
* Add View Transitions announcer * fix astro check * Append the text in a setTimeout * Use 60 for the timeout * Add comment on magic number * Add a changeset * Update .changeset/small-rules-relax.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Bring back announce logic * Remove mention of env file --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
parent
5121740de7
commit
e6be2d8146
42 changed files with 787 additions and 0 deletions
11
.changeset/small-rules-relax.md
Normal file
11
.changeset/small-rules-relax.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Route Announcer in `<ViewTransitions />`
|
||||||
|
|
||||||
|
The View Transitions router now does route announcement. When transitioning between pages with a traditional MPA approach, assistive technologies will announce the page title when the page finishes loading. This does not automatically happen during client-side routing, so visitors relying on these technologies to announce routes are not aware when a page has changed.
|
||||||
|
|
||||||
|
The view transitions route announcer runs after the `astro:page-load` event, looking for the page `<title>` to announce. If one cannot be found, the announcer falls back to the first `<h1>` it finds, or otherwise announces the pathname. We recommend you always include a `<title>` in each page for accessibility.
|
||||||
|
|
||||||
|
See the [View Transitions docs](https://docs.astro.build/en/guides/view-transitions/) for more on how accessibility is handled.
|
30
examples/view-transitions/README.md
Normal file
30
examples/view-transitions/README.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Astro Movies View Transitions Demo
|
||||||
|
|
||||||
|
### 👉🏽 [Live Demo](https://astro-movies.pages.dev/)
|
||||||
|
|
||||||
|
![Screenshot](./screenshot.png)
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
1. Clone this repository and install dependencies with `npm install`.
|
||||||
|
2. Start the project locally with npm run dev, or deploy it to your favorite server.
|
||||||
|
3. Have fun! ✨
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :--------------------- | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Check out [Astro's documentation](https://docs.astro.build) or jump into their [Discord server](https://astro.build/chat).
|
||||||
|
|
||||||
|
You can also reach out to [Maxi on Twitter](https://twitter.com/charca).
|
15
examples/view-transitions/astro.config.mjs
Normal file
15
examples/view-transitions/astro.config.mjs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineConfig } from 'astro/config'
|
||||||
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
import nodejs from '@astrojs/node';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
integrations: [tailwind()],
|
||||||
|
output: 'server',
|
||||||
|
adapter: nodejs({ mode: 'standalone' }),
|
||||||
|
vite: {
|
||||||
|
define: {
|
||||||
|
'process.env.TMDB_API_KEY': JSON.stringify(process.env.TMDB_API_KEY),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
17
examples/view-transitions/package.json
Normal file
17
examples/view-transitions/package.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "@example/view-transitions",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/tailwind": "^5.0.0",
|
||||||
|
"@astrojs/node": "^6.0.0",
|
||||||
|
"astro": "^3.1.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
examples/view-transitions/public/favicon.ico
Normal file
BIN
examples/view-transitions/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
examples/view-transitions/src/components/Footer.astro
Normal file
17
examples/view-transitions/src/components/Footer.astro
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<footer class="border border-t border-gray-800">
|
||||||
|
<div class="container mx-auto text-sm px-4 py-6">
|
||||||
|
Made with ❤️ by <a
|
||||||
|
href="https://www.twitter.com/charca"
|
||||||
|
target="_blank"
|
||||||
|
class="underline hover:text-gray-300">Maxi Ferreira</a
|
||||||
|
> — Powered by <a
|
||||||
|
href="https://astro.build"
|
||||||
|
target="_blank"
|
||||||
|
class="underline hover:text-gray-300">Astro</a
|
||||||
|
> and <a
|
||||||
|
href="https://www.themoviedb.org/documentation/api"
|
||||||
|
target="_blank"
|
||||||
|
class="underline hover:text-gray-300">TMDb API</a
|
||||||
|
>.
|
||||||
|
</div>
|
||||||
|
</footer>
|
23
examples/view-transitions/src/components/MovieCard.astro
Normal file
23
examples/view-transitions/src/components/MovieCard.astro
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
const { movie } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<a href={`/movies/${movie.id}`}>
|
||||||
|
<img src={`https://image.tmdb.org/t/p/w500${movie.poster_path}`}
|
||||||
|
alt={`${movie.title} Poster`}
|
||||||
|
class="thumbnail hover:opacity-75 transition ease-in-out duration-150"
|
||||||
|
id={`movie-poster-${movie.id}`}
|
||||||
|
transition:name={`poster-${movie.id}`}>
|
||||||
|
</a>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href={`/movies/${movie.id}`} class="text-lg mt-2 hover:text-gray-300">{movie.title}</a>
|
||||||
|
<div class="flex items-center text-gray-400 text-sm mt-1">
|
||||||
|
<svg class="fill-current text-orange-500 w-4" viewBox="0 0 24 24"><g data-name="Layer 2"><path d="M17.56 21a1 1 0 01-.46-.11L12 18.22l-5.1 2.67a1 1 0 01-1.45-1.06l1-5.63-4.12-4a1 1 0 01-.25-1 1 1 0 01.81-.68l5.7-.83 2.51-5.13a1 1 0 011.8 0l2.54 5.12 5.7.83a1 1 0 01.81.68 1 1 0 01-.25 1l-4.12 4 1 5.63a1 1 0 01-.4 1 1 1 0 01-.62.18z" data-name="star"/></g></svg>
|
||||||
|
<span class="ml-1">{movie.vote_average}</span>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<span>{movie.release_date}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-400 text-sm">{movie.genres}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
100
examples/view-transitions/src/components/MovieDetails.astro
Normal file
100
examples/view-transitions/src/components/MovieDetails.astro
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
const { data } = Astro.props;
|
||||||
|
|
||||||
|
const movie = {
|
||||||
|
...data,
|
||||||
|
poster_path: data.poster_path
|
||||||
|
? 'https://image.tmdb.org/t/p/w500/' + data.poster_path
|
||||||
|
: 'https://via.placeholder.com/500x750',
|
||||||
|
vote_average: (data.vote_average * 10).toFixed(2) + '%',
|
||||||
|
release_date: new Date(data.release_date).toLocaleDateString('en-us', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
}),
|
||||||
|
genres: data.genres.map((g: any) => g.name).join(', '),
|
||||||
|
crew: data.credits.crew.slice(0,3),
|
||||||
|
cast: data.credits.cast.slice(0,5).map((c: any) => ({
|
||||||
|
...c,
|
||||||
|
profile_path: c.profile_path
|
||||||
|
? 'https://image.tmdb.org/t/p/w300/' + c.profile_path
|
||||||
|
: 'https://via.placeholder.com/300x450'
|
||||||
|
})),
|
||||||
|
images: data.images.backdrops.slice(0, 9),
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="movie-info border-b border-gray-800">
|
||||||
|
<div class="container mx-auto px-4 py-16 flex flex-col md:flex-row">
|
||||||
|
<div class="flex-none">
|
||||||
|
<img src={movie.poster_path}
|
||||||
|
alt={`${movie.title} Poster`}
|
||||||
|
class="movie-poster w-64 lg:w-96"
|
||||||
|
id="movie-poster"
|
||||||
|
transition:name={`poster-${movie.id}`}>
|
||||||
|
</div>
|
||||||
|
<div class="md:ml-24">
|
||||||
|
<h2 class="text-4xl mt-4 md:mt-0 mb-2 font-semibold">{movie.title}</h2>
|
||||||
|
<div class="flex flex-wrap items-center text-gray-400 text-sm">
|
||||||
|
<svg class="fill-current text-orange-500 w-4" viewBox="0 0 24 24"><g data-name="Layer 2"><path d="M17.56 21a1 1 0 01-.46-.11L12 18.22l-5.1 2.67a1 1 0 01-1.45-1.06l1-5.63-4.12-4a1 1 0 01-.25-1 1 1 0 01.81-.68l5.7-.83 2.51-5.13a1 1 0 011.8 0l2.54 5.12 5.7.83a1 1 0 01.81.68 1 1 0 01-.25 1l-4.12 4 1 5.63a1 1 0 01-.4 1 1 1 0 01-.62.18z" data-name="star"/></g></svg>
|
||||||
|
<span class="ml-1">{movie.vote_average}</span>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<span>{movie.release_date}</span>
|
||||||
|
<span class="mx-2">|</span>
|
||||||
|
<span>{movie.genres}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-300 mt-8">
|
||||||
|
{movie.overview}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-12">
|
||||||
|
<h4 class="text-white font-semibold">Featured Crew</h4>
|
||||||
|
<div class="flex mt-4">
|
||||||
|
{movie.crew.map((crew: any) => (
|
||||||
|
<div class="mr-8">
|
||||||
|
<div>{crew.name}</div>
|
||||||
|
<div class="text-gray-400 text-sm">{crew.job}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- end movie-info -->
|
||||||
|
|
||||||
|
<div class="movie-cast border-b border-gray-800">
|
||||||
|
<div class="container mx-auto px-4 py-16">
|
||||||
|
<h2 class="text-4xl font-semibold">Cast</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8">
|
||||||
|
{movie.cast.map((cast: any) => (
|
||||||
|
<div class="mt-8">
|
||||||
|
<span>
|
||||||
|
<img id={`person-photo-${cast.id}`} src={cast.profile_path} alt={cast.name} class="thumbnail hover:opacity-75 transition ease-in-out duration-150">
|
||||||
|
</span>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-lg mt-2 hover:text-gray:300">{cast.name}</span>
|
||||||
|
<div class="text-sm text-gray-400">
|
||||||
|
{cast.character}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- end movie-cast -->
|
||||||
|
|
||||||
|
<div class="movie-images">
|
||||||
|
<div class="container mx-auto px-4 py-16">
|
||||||
|
<h2 class="text-4xl font-semibold">Images</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||||
|
{movie.images.map((image: any) => (
|
||||||
|
<div class="mt-8">
|
||||||
|
<span>
|
||||||
|
<img src={`https://image.tmdb.org/t/p/w500${image.file_path}`} loading="lazy" alt={movie.name} class="hover:opacity-75 transition ease-in-out duration-150">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- end movie-images -->
|
19
examples/view-transitions/src/components/MovieList.astro
Normal file
19
examples/view-transitions/src/components/MovieList.astro
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
import MovieCard from './MovieCard.astro';
|
||||||
|
import movies from '../popular-movies.json';
|
||||||
|
const popularMovies = movies.results;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 pt-16 mb-16">
|
||||||
|
<div class="popular-movies">
|
||||||
|
<h2 class="uppercase tracking-wider text-orange-500 text-lg font-semibold">
|
||||||
|
Popular Movies
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8"
|
||||||
|
>
|
||||||
|
{popularMovies.map((movie) => <MovieCard movie={movie} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- end pouplar-movies -->
|
||||||
|
</div>
|
16
examples/view-transitions/src/components/Nav.astro
Normal file
16
examples/view-transitions/src/components/Nav.astro
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<nav class="nav border-b border-gray-800 sticky top-0 z-30 bg-gray-900">
|
||||||
|
<div class="container mx-auto px-4 flex flex-col md:flex-row items-center justify-between px-4 py-6">
|
||||||
|
<ul class="flex flex-col md:flex-row items-center">
|
||||||
|
<li>
|
||||||
|
<a href="/" class="flex items-center font-bold text-xl">
|
||||||
|
<span>Movies</span>
|
||||||
|
|
||||||
|
<span class="text-orange-500">List</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="md:ml-16 mt-3 md:mt-0">
|
||||||
|
<a href="/" class="hover:text-gray-300">Movies</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
12
examples/view-transitions/src/content/config.ts
Normal file
12
examples/view-transitions/src/content/config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const movies = defineCollection({
|
||||||
|
type: 'data',
|
||||||
|
schema: z.object({
|
||||||
|
data: z.any(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expose your defined collection to Astro
|
||||||
|
// with the `collections` export
|
||||||
|
export const collections = { movies };
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/1880.json
Normal file
1
examples/view-transitions/src/content/movies/1880.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/298618.json
Normal file
1
examples/view-transitions/src/content/movies/298618.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/335977.json
Normal file
1
examples/view-transitions/src/content/movies/335977.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/346698.json
Normal file
1
examples/view-transitions/src/content/movies/346698.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/385687.json
Normal file
1
examples/view-transitions/src/content/movies/385687.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/565770.json
Normal file
1
examples/view-transitions/src/content/movies/565770.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/569094.json
Normal file
1
examples/view-transitions/src/content/movies/569094.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/606403.json
Normal file
1
examples/view-transitions/src/content/movies/606403.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/614930.json
Normal file
1
examples/view-transitions/src/content/movies/614930.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/615656.json
Normal file
1
examples/view-transitions/src/content/movies/615656.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/667538.json
Normal file
1
examples/view-transitions/src/content/movies/667538.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/678512.json
Normal file
1
examples/view-transitions/src/content/movies/678512.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/717930.json
Normal file
1
examples/view-transitions/src/content/movies/717930.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/762430.json
Normal file
1
examples/view-transitions/src/content/movies/762430.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/820525.json
Normal file
1
examples/view-transitions/src/content/movies/820525.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/968051.json
Normal file
1
examples/view-transitions/src/content/movies/968051.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/976573.json
Normal file
1
examples/view-transitions/src/content/movies/976573.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/view-transitions/src/content/movies/990140.json
Normal file
1
examples/view-transitions/src/content/movies/990140.json
Normal file
File diff suppressed because one or more lines are too long
36
examples/view-transitions/src/layouts/Layout.astro
Normal file
36
examples/view-transitions/src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
import '../styles/styles.css';
|
||||||
|
import { ViewTransitions } from 'astro:transitions';
|
||||||
|
import Footer from "../components/Footer.astro";
|
||||||
|
import Nav from "../components/Nav.astro";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title } = Astro.props as Props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<meta name="view-transition" content="same-origin" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<ViewTransitions />
|
||||||
|
</head>
|
||||||
|
<body class="font-sans bg-gray-900 text-white">
|
||||||
|
<div class="h-screen overflow-hidden flex flex-col">
|
||||||
|
<Nav />
|
||||||
|
<div id="container" class="h-full flex-1 overflow-y-auto">
|
||||||
|
<div id="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
examples/view-transitions/src/pages/index.astro
Normal file
8
examples/view-transitions/src/pages/index.astro
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import MovieList from '../components/MovieList.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Movies List">
|
||||||
|
<MovieList />
|
||||||
|
</Layout>
|
14
examples/view-transitions/src/pages/movies/[id].astro
Normal file
14
examples/view-transitions/src/pages/movies/[id].astro
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
import MovieDetails from '../../components/MovieDetails.astro';
|
||||||
|
import { getDataEntryById } from 'astro:content';
|
||||||
|
|
||||||
|
// Data collection bug?
|
||||||
|
const id: any = '/src/content/movies/'+Astro.params.id;
|
||||||
|
const result = await getDataEntryById('movies', id);
|
||||||
|
const data = result.data.data;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`${data.title} on Movies List`}>
|
||||||
|
<MovieDetails data={data} />
|
||||||
|
</Layout>
|
1
examples/view-transitions/src/popular-movies.json
Normal file
1
examples/view-transitions/src/popular-movies.json
Normal file
File diff suppressed because one or more lines are too long
262
examples/view-transitions/src/scripts/spa-navigation.js
Normal file
262
examples/view-transitions/src/scripts/spa-navigation.js
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
import {
|
||||||
|
getNavigationType,
|
||||||
|
getPathId,
|
||||||
|
isBackNavigation,
|
||||||
|
shouldNotIntercept,
|
||||||
|
updateTheDOMSomehow,
|
||||||
|
useTvFragment,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
// View Transitions support cross-document navigations.
|
||||||
|
// Should compare performace.
|
||||||
|
// https://github.com/WICG/view-transitions/blob/main/explainer.md#cross-document-same-origin-transitions
|
||||||
|
// https://github.com/WICG/view-transitions/blob/main/explainer.md#script-events
|
||||||
|
function shouldDisableSpa() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation.addEventListener('navigate', (navigateEvent) => {
|
||||||
|
if (shouldDisableSpa()) return
|
||||||
|
if (shouldNotIntercept(navigateEvent)) return
|
||||||
|
|
||||||
|
const toUrl = new URL(navigateEvent.destination.url)
|
||||||
|
const toPath = toUrl.pathname
|
||||||
|
const fromPath = location.pathname
|
||||||
|
const navigationType = getNavigationType(fromPath, toPath)
|
||||||
|
|
||||||
|
if (location.origin !== toUrl.origin) return
|
||||||
|
|
||||||
|
switch (navigationType) {
|
||||||
|
case 'home-to-movie':
|
||||||
|
case 'tv-to-show':
|
||||||
|
handleHomeToMovieTransition(navigateEvent, getPathId(toPath))
|
||||||
|
break
|
||||||
|
case 'movie-to-home':
|
||||||
|
case 'show-to-tv':
|
||||||
|
handleMovieToHomeTransition(navigateEvent, getPathId(fromPath))
|
||||||
|
break
|
||||||
|
case 'movie-to-person':
|
||||||
|
handleMovieToPersonTransition(
|
||||||
|
navigateEvent,
|
||||||
|
getPathId(fromPath),
|
||||||
|
getPathId(toPath)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'person-to-movie':
|
||||||
|
case 'person-to-show':
|
||||||
|
handlePersonToMovieTransition(
|
||||||
|
navigateEvent,
|
||||||
|
getPathId(fromPath),
|
||||||
|
getPathId(toPath)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: https://developer.chrome.com/docs/web-platform/view-transitions/#transitions-as-an-enhancement
|
||||||
|
function handleHomeToMovieTransition(navigateEvent, movieId) {
|
||||||
|
navigateEvent.intercept({
|
||||||
|
async handler() {
|
||||||
|
const fragmentUrl = useTvFragment(navigateEvent)
|
||||||
|
? '/fragments/TvDetails'
|
||||||
|
: '/fragments/MovieDetails'
|
||||||
|
const response = await fetch(`${fragmentUrl}/${movieId}`)
|
||||||
|
const data = await response.text()
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
updateTheDOMSomehow(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnail = document.getElementById(`movie-poster-${movieId}`)
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.style.viewTransitionName = 'movie-poster'
|
||||||
|
}
|
||||||
|
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
document.getElementById('container').scrollTop = 0
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
await transition.finished
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMovieToHomeTransition(navigateEvent, movieId) {
|
||||||
|
navigateEvent.intercept({
|
||||||
|
scroll: 'manual',
|
||||||
|
async handler() {
|
||||||
|
const fragmentUrl = useTvFragment(navigateEvent)
|
||||||
|
? '/fragments/TvList'
|
||||||
|
: '/fragments/MovieList'
|
||||||
|
const response = await fetch(fragmentUrl)
|
||||||
|
const data = await response.text()
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempHomePage = document.createElement('div')
|
||||||
|
const moviePoster = document.getElementById(`movie-poster`)
|
||||||
|
let thumbnail
|
||||||
|
|
||||||
|
// If the movie poster is not in the home page, removes the transition style so that
|
||||||
|
// the poster doesn't stay on the page while transitioning
|
||||||
|
tempHomePage.innerHTML = data
|
||||||
|
if (!tempHomePage.querySelector(`#movie-poster-${movieId}`)) {
|
||||||
|
moviePoster?.classList.remove('movie-poster')
|
||||||
|
}
|
||||||
|
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
|
||||||
|
thumbnail = document.getElementById(`movie-poster-${movieId}`)
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.scrollIntoViewIfNeeded()
|
||||||
|
thumbnail.style.viewTransitionName = 'movie-poster'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await transition.finished
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMovieToPersonTransition(navigateEvent, movieId, personId) {
|
||||||
|
// TODO: https://developer.chrome.com/docs/web-platform/view-transitions/#not-a-polyfill
|
||||||
|
// ...has example of `back-transition` class applied to document
|
||||||
|
const isBack = isBackNavigation(navigateEvent)
|
||||||
|
|
||||||
|
navigateEvent.intercept({
|
||||||
|
async handler() {
|
||||||
|
const response = await fetch('/fragments/PersonDetails/' + personId)
|
||||||
|
const data = await response.text()
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let personThumbnail
|
||||||
|
let moviePoster
|
||||||
|
let movieThumbnail
|
||||||
|
|
||||||
|
if (!isBack) {
|
||||||
|
// We're transitioning the person photo; we need to remove the transition of the poster
|
||||||
|
// so that it doesn't stay on the page while transitioning
|
||||||
|
moviePoster = document.getElementById(`movie-poster`)
|
||||||
|
if (moviePoster) {
|
||||||
|
moviePoster.classList.remove('movie-poster')
|
||||||
|
}
|
||||||
|
|
||||||
|
personThumbnail = document.getElementById(`person-photo-${personId}`)
|
||||||
|
if (personThumbnail) {
|
||||||
|
personThumbnail.style.viewTransitionName = 'person-photo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
|
||||||
|
if (personThumbnail) {
|
||||||
|
personThumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBack) {
|
||||||
|
// If we're coming back to the person page, we're transitioning
|
||||||
|
// into the movie poster thumbnail, so we need to add the tag to it
|
||||||
|
movieThumbnail = document.getElementById(`movie-poster-${movieId}`)
|
||||||
|
if (movieThumbnail) {
|
||||||
|
movieThumbnail.scrollIntoViewIfNeeded()
|
||||||
|
movieThumbnail.style.viewTransitionName = 'movie-poster'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('container').scrollTop = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
await transition.finished
|
||||||
|
|
||||||
|
if (movieThumbnail) {
|
||||||
|
movieThumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePersonToMovieTransition(navigateEvent, personId, movieId) {
|
||||||
|
const isBack = isBackNavigation(navigateEvent)
|
||||||
|
|
||||||
|
navigateEvent.intercept({
|
||||||
|
scroll: 'manual',
|
||||||
|
async handler() {
|
||||||
|
const fragmentUrl = useTvFragment(navigateEvent)
|
||||||
|
? '/fragments/TvDetails'
|
||||||
|
: '/fragments/MovieDetails'
|
||||||
|
const response = await fetch(`${fragmentUrl}/${movieId}`)
|
||||||
|
const data = await response.text()
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail
|
||||||
|
let moviePoster
|
||||||
|
let movieThumbnail
|
||||||
|
|
||||||
|
if (!isBack) {
|
||||||
|
movieThumbnail = document.getElementById(`movie-poster-${movieId}`)
|
||||||
|
if (movieThumbnail) {
|
||||||
|
movieThumbnail.style.viewTransitionName = 'movie-poster'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
updateTheDOMSomehow(data)
|
||||||
|
|
||||||
|
if (isBack) {
|
||||||
|
moviePoster = document.getElementById(`movie-poster`)
|
||||||
|
if (moviePoster) {
|
||||||
|
moviePoster.classList.remove('movie-poster')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (personId) {
|
||||||
|
thumbnail = document.getElementById(`person-photo-${personId}`)
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.scrollIntoViewIfNeeded()
|
||||||
|
thumbnail.style.viewTransitionName = 'person-photo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('container').scrollTop = 0
|
||||||
|
|
||||||
|
if (movieThumbnail) {
|
||||||
|
movieThumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await transition.finished
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
thumbnail.style.viewTransitionName = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moviePoster) {
|
||||||
|
moviePoster.classList.add('movie-poster')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
79
examples/view-transitions/src/scripts/utils.js
Normal file
79
examples/view-transitions/src/scripts/utils.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
export function getNavigationType(fromPath, toPath) {
|
||||||
|
if (fromPath.startsWith('/movies') && toPath === '/') {
|
||||||
|
return 'movie-to-home'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromPath === '/tv' && toPath.startsWith('/tv/')) {
|
||||||
|
return 'tv-to-show'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromPath === '/' && toPath.startsWith('/movies')) {
|
||||||
|
return 'home-to-movie'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromPath.startsWith('/tv/') && toPath === '/tv') {
|
||||||
|
return 'show-to-tv'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(fromPath.startsWith('/movies') || fromPath.startsWith('/tv')) &&
|
||||||
|
toPath.startsWith('/people')
|
||||||
|
) {
|
||||||
|
return 'movie-to-person'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
fromPath.startsWith('/people') &&
|
||||||
|
(toPath.startsWith('/movies') || toPath.startsWith('/tv/'))
|
||||||
|
) {
|
||||||
|
return 'person-to-movie'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'other'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBackNavigation(navigateEvent) {
|
||||||
|
if (
|
||||||
|
navigateEvent.navigationType === 'push' ||
|
||||||
|
navigateEvent.navigationType === 'replace'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
navigateEvent.destination.index !== -1 &&
|
||||||
|
navigateEvent.destination.index < navigation.currentEntry.index
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldNotIntercept(navigationEvent) {
|
||||||
|
return (
|
||||||
|
navigationEvent.canIntercept === false ||
|
||||||
|
// If this is just a hashChange,
|
||||||
|
// just let the browser handle scrolling to the content.
|
||||||
|
navigationEvent.hashChange ||
|
||||||
|
// If this is a download,
|
||||||
|
// let the browser perform the download.
|
||||||
|
navigationEvent.downloadRequest ||
|
||||||
|
// If this is a form submission,
|
||||||
|
// let that go to the server.
|
||||||
|
navigationEvent.formData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTvFragment(navigateEvent) {
|
||||||
|
const toUrl = new URL(navigateEvent.destination.url)
|
||||||
|
const toPath = toUrl.pathname
|
||||||
|
|
||||||
|
return toPath.startsWith('/tv')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPathId(path) {
|
||||||
|
return path.split('/')[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTheDOMSomehow(data) {
|
||||||
|
document.getElementById('content').innerHTML = data
|
||||||
|
}
|
61
examples/view-transitions/src/styles/styles.css
Normal file
61
examples/view-transitions/src/styles/styles.css
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-from-right {
|
||||||
|
from {
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-to-left {
|
||||||
|
to {
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
|
||||||
|
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
|
||||||
|
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(movie-poster),
|
||||||
|
::view-transition-new(movie-poster) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-image-pair(movie-poster) {
|
||||||
|
isolation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
view-transition-name: main-header;
|
||||||
|
contain: paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-poster {
|
||||||
|
contain: paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-photo {
|
||||||
|
view-transition-name: person-photo;
|
||||||
|
contain: paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
contain: paint;
|
||||||
|
}
|
12
examples/view-transitions/tailwind.config.cjs
Normal file
12
examples/view-transitions/tailwind.config.cjs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
width: {
|
||||||
|
96: '24rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
6
examples/view-transitions/tsconfig.json
Normal file
6
examples/view-transitions/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,21 @@ const samePage = (otherLocation: URL) =>
|
||||||
location.pathname === otherLocation.pathname && location.search === otherLocation.search;
|
location.pathname === otherLocation.pathname && location.search === otherLocation.search;
|
||||||
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
||||||
const onPageLoad = () => triggerEvent('astro:page-load');
|
const onPageLoad = () => triggerEvent('astro:page-load');
|
||||||
|
const announce = () => {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.setAttribute('aria-live', 'assertive');
|
||||||
|
div.setAttribute('aria-atomic', 'true');
|
||||||
|
div.setAttribute('style', 'position:absolute;left:0;top:0;clip:rect(0 0 0 0);clip-path:inset(50%);overflow:hidden;white-space:nowrap;width:1px;height:1px');
|
||||||
|
document.body.append(div);
|
||||||
|
setTimeout(() => {
|
||||||
|
let title = document.title || document.querySelector('h1')?.textContent || location.pathname;
|
||||||
|
div.textContent = title;
|
||||||
|
},
|
||||||
|
// Much thought went into this magic number; the gist is that screen readers
|
||||||
|
// need to see that the element changed and might not do so if it happens
|
||||||
|
// too quickly.
|
||||||
|
60);
|
||||||
|
};
|
||||||
const PERSIST_ATTR = 'data-astro-transition-persist';
|
const PERSIST_ATTR = 'data-astro-transition-persist';
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
// explained at its usage
|
// explained at its usage
|
||||||
|
@ -359,6 +374,7 @@ async function transition(
|
||||||
await runScripts();
|
await runScripts();
|
||||||
markScriptsExec();
|
markScriptsExec();
|
||||||
onPageLoad();
|
onPageLoad();
|
||||||
|
announce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -360,6 +360,18 @@ importers:
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
|
|
||||||
|
examples/view-transitions:
|
||||||
|
devDependencies:
|
||||||
|
'@astrojs/node':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: link:../../packages/integrations/node
|
||||||
|
'@astrojs/tailwind':
|
||||||
|
specifier: ^5.0.0
|
||||||
|
version: link:../../packages/integrations/tailwind
|
||||||
|
astro:
|
||||||
|
specifier: ^3.1.1
|
||||||
|
version: link:../../packages/astro
|
||||||
|
|
||||||
examples/with-markdoc:
|
examples/with-markdoc:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/markdoc':
|
'@astrojs/markdoc':
|
||||||
|
|
Loading…
Reference in a new issue