mirror of
https://github.com/withastro/astro.git
synced 2024-12-16 21:46:22 -05:00
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;
|
||||
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
|
||||
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 parser = new DOMParser();
|
||||
// explained at its usage
|
||||
|
@ -359,6 +374,7 @@ async function transition(
|
|||
await runScripts();
|
||||
markScriptsExec();
|
||||
onPageLoad();
|
||||
announce();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -360,6 +360,18 @@ importers:
|
|||
specifier: ^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:
|
||||
dependencies:
|
||||
'@astrojs/markdoc':
|
||||
|
|
Loading…
Reference in a new issue