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

Add <ViewTransition> Component #31975

Merged
merged 32 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
867d341
Add Flag
sebmarkbage Dec 24, 2024
b7f938a
Add ViewTransition Component Type
sebmarkbage Dec 26, 2024
5e79ee3
Add View Transition fixture
sebmarkbage Dec 26, 2024
8ffa597
ViewTransitionComponent Fiber type
sebmarkbage Dec 26, 2024
ec28bef
Assign a stateful autogenerated name if an explicit one is not provided
sebmarkbage Dec 26, 2024
7102ba1
Add ViewTransitionStatic flag
sebmarkbage Dec 26, 2024
73d0cc8
Use snapshot phase to find affected ViewTransitionComponents
sebmarkbage Dec 26, 2024
7dddece
Trigger enter/exit when switching between Offscreen modes
sebmarkbage Dec 27, 2024
4566bb9
Apply view transition name
sebmarkbage Dec 27, 2024
c3904ed
Restore view-transition-name after committing the transition
sebmarkbage Dec 28, 2024
f610428
Treat the name "auto" as the default auto-generated name
sebmarkbage Dec 27, 2024
f435f02
Measure size of ViewTransition bounding boxes before updates
sebmarkbage Dec 27, 2024
7f8efb3
Call startViewTransition
sebmarkbage Dec 28, 2024
0fef325
Track whether a committed update actually performed any mutations
sebmarkbage Dec 31, 2024
916719a
Add AfterMutationPhase
sebmarkbage Dec 31, 2024
a5bfbf6
Cancel specific view transitions if they ended up unchanged after layout
sebmarkbage Jan 1, 2025
d33467b
Cancel the root transition when nothing inside it leaks
sebmarkbage Jan 2, 2025
b22b047
isInstanceInViewport -> wasInstanceInViewport
sebmarkbage Jan 2, 2025
957c64f
Cancel a transition if both old and new state are out of the viewport
sebmarkbage Jan 2, 2025
6ab746e
Since we're visiting all updated view transition host instances in th…
sebmarkbage Jan 3, 2025
f7613a3
Track whether a ViewTransition Component was mounted with an explicit…
sebmarkbage Jan 2, 2025
225e2ce
Track appearing view transition components whether they were new or r…
sebmarkbage Jan 3, 2025
44ab49c
Pair deleted ViewTransitions with the newly inserted ViewTransitions …
sebmarkbage Jan 3, 2025
260b23b
Assign names to paired new states
sebmarkbage Jan 4, 2025
46d5486
Restore those pairs after the animation starts
sebmarkbage Jan 4, 2025
ad92c62
Move commitEnterViewTransitions to the after mutation phase
sebmarkbage Jan 4, 2025
a54c8b0
Don't bother applying transitions to enter/exit that are outside the …
sebmarkbage Jan 4, 2025
4275b51
Don't pair up named transitions if one side of out of the viewport
sebmarkbage Jan 4, 2025
bcf40a7
Add button to fixture
sebmarkbage Jan 6, 2025
225ee01
Update fixtures/view-transition/README.md
sebmarkbage Jan 6, 2025
cb3f0c6
Use a different namespace from useId to avoid conflict with #32001
sebmarkbage Jan 7, 2025
7f587b3
Fix measurement bug
sebmarkbage Jan 7, 2025
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
30 changes: 30 additions & 0 deletions fixtures/view-transition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# View Transition

A test case for View Transitions.

## Setup

To reference a local build of React, first run `npm run build` at the root
of the React project. Then:

```
cd fixtures/view-transition
yarn
yarn start
```

The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.

**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:**

```
yarn
```

If you want to try the production mode instead run:

```
yarn start:prod
```

This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading).
28 changes: 28 additions & 0 deletions fixtures/view-transition/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "react-fixtures-view-transition",
"version": "0.1.0",
"private": true,
"devDependencies": {
"concurrently": "3.1.0",
"http-proxy-middleware": "0.17.3",
"react-scripts": "0.9.5"
},
"dependencies": {
"express": "^4.14.0",
"ignore-styles": "^5.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prestart": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:client": "PORT=3001 react-scripts start",
"dev:server": "NODE_ENV=development node server",
"start": "react-scripts build && NODE_ENV=production node server",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
Binary file added fixtures/view-transition/public/favicon.ico
Binary file not shown.
13 changes: 13 additions & 0 deletions fixtures/view-transition/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<body>
<script>
/*
This is just a placeholder to make react-scripts happy.
We're not using it. If we end up here, redirect to the
primary server.
*/
location.href = '//localhost:3000/';
</script>
</body>
</html>
70 changes: 70 additions & 0 deletions fixtures/view-transition/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require('ignore-styles');
const babelRegister = require('babel-register');
const proxy = require('http-proxy-middleware');

babelRegister({
ignore: /\/(build|node_modules)\//,
presets: ['react-app'],
});

const express = require('express');
const path = require('path');

const app = express();

// Application
if (process.env.NODE_ENV === 'development') {
app.get('/', function (req, res) {
// In development mode we clear the module cache between each request to
// get automatic hot reloading.
for (var key in require.cache) {
delete require.cache[key];
}
const render = require('./render').default;
render(req.url, res);
});
} else {
const render = require('./render').default;
app.get('/', function (req, res) {
render(req.url, res);
});
}

// Static resources
app.use(express.static(path.resolve(__dirname, '..', 'build')));

// Proxy everything else to create-react-app's webpack development server
if (process.env.NODE_ENV === 'development') {
app.use(
'/',
proxy({
ws: true,
target: 'http://localhost:3001',
})
);
}

app.listen(3000, () => {
console.log('Listening on port 3000...');
});

app.on('error', function (error) {
if (error.syscall !== 'listen') {
throw error;
}

var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;

switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});
44 changes: 44 additions & 0 deletions fixtures/view-transition/server/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';

import App from '../src/components/App';

let assets;
if (process.env.NODE_ENV === 'development') {
// Use the bundle from create-react-app's server in development mode.
assets = {
'main.js': '/static/js/bundle.js',
// 'main.css': '',
};
} else {
assets = require('../build/asset-manifest.json');
}

export default function render(url, res) {
res.socket.on('error', error => {
// Log fatal errors
console.error('Fatal', error);
});
let didError = false;
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send('<!doctype><p>Error</p>');
},
onError(x) {
didError = true;
console.error(x);
},
});
// Abandon and switch to client rendering after 5 seconds.
// Try lowering this to see the client recover.
setTimeout(abort, 5000);
}
12 changes: 12 additions & 0 deletions fixtures/view-transition/src/components/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

import Chrome from './Chrome';
import Page from './Page';

export default function App({assets}) {
return (
<Chrome title="Hello World" assets={assets}>
<Page />
</Chrome>
);
}
5 changes: 5 additions & 0 deletions fixtures/view-transition/src/components/Chrome.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
body {
margin: 10px;
padding: 0;
font-family: sans-serif;
}
33 changes: 33 additions & 0 deletions fixtures/view-transition/src/components/Chrome.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, {Component} from 'react';

import './Chrome.css';

export default class Chrome extends Component {
render() {
const assets = this.props.assets;
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="stylesheet" href={assets['main.css']} />
<title>{this.props.title}</title>
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>
{this.props.children}
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,
}}
/>
</body>
</html>
);
}
}
Empty file.
79 changes: 79 additions & 0 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {
unstable_ViewTransition as ViewTransition,
startTransition,
useEffect,
useState,
unstable_Activity as Activity,
} from 'react';

import './Page.css';

const a = (
<div key="a">
<ViewTransition>
<div>a</div>
</ViewTransition>
</div>
);

const b = (
<div key="b">
<ViewTransition>
<div>b</div>
</ViewTransition>
</div>
);

export default function Page() {
const [show, setShow] = useState(false);
useEffect(() => {
startTransition(() => {
setShow(true);
});
}, []);
const exclamation = (
<ViewTransition name="exclamation">
<span>!</span>
</ViewTransition>
);
return (
<div>
<button
onClick={() => {
startTransition(() => {
setShow(show => !show);
});
}}>
{show ? 'A' : 'B'}
</button>
<ViewTransition>
<div>
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
<ViewTransition>
{show ? <div>hello{exclamation}</div> : <section>Loading</section>}
</ViewTransition>
{show ? null : (
<ViewTransition>
<div>world{exclamation}</div>
</ViewTransition>
)}
<Activity mode={show ? 'visible' : 'hidden'}>
<ViewTransition>
<div>!!</div>
</ViewTransition>
</Activity>
</div>
</ViewTransition>
</div>
);
}
6 changes: 6 additions & 0 deletions fixtures/view-transition/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import {hydrateRoot} from 'react-dom/client';

import App from './components/App';

hydrateRoot(document, <App assets={window.assetManifest} />);
Loading
Loading