mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Fiber] Use className on <ViewTransition> to assign view-transition-class (#31999)
Stacked on #31975.
This is the primary way we recommend styling your View Transitions since
it allows for reusable styling such as a CSS library specializing in
View Transitions in a way that's composable and without naming
conflicts. E.g.
```js
<ViewTransition className="enter-slide-in exit-fade-out update-cross-fade">
```
This doesn't change the HTML `class` attribute. It's not a CSS class.
Instead it assign the `view-transition-class` style prop of the
underlying DOM node while it's transitioning.
You can also just use `<div style={{viewTransitionClass: ...}}>` on the
DOM node but it's convenient to control the Transition completely from
the outside and conceptually we're transitioning the whole fragment. You
can even make Transition components that just wraps existing components.
`<RevealTransition><Component /></RevealTransition>` this way.
Since you can also have multiple wrappers for different circumstances it
allows React's heuristics to use different classes for different
scenarios. We'll likely add more options like configuring different
classes for different `types` or scenarios that can't be described by
CSS alone.
## CSS Modules
```js
import transitions from './transitions.module.css';
<ViewTransition className={transitions.bounceIn}>...</ViewTransition>
```
CSS Modules works well with this strategy because you can have globally
unique namespaces and define your transitions in the CSS modules as a
library that you can import. [As seen in the fixture
here.](8b91b37bb8 (diff-b4d9854171ffdac4d2c01be92a5eff4f8e9e761e6af953094f99ca243b054a85R11))
I did notice an unfortunate bug in how CSS Modules (at least in Webpack)
generates class names. Sometimes the `+` character is used in the hash
of the class name which is not valid for `view-transition-class` and so
it breaks. I had to rename my class names until the hash yielded
something different to work around it. Ideally that bug gets fixed soon.
## className, rly?
`className` isn't exactly the most loved property name, however, I'm
using `className` here too for consistency. Even though in this case
there's no direct equivalent DOM property name. The CSS property is
named `viewTransitionClass`, but the "viewTransition" prefix is implied
by the Component it is on in this case. For most people the fact that
this is actually a different namespace than other CSS classes doesn't
matter. You'll most just use a CSS library anyway and conceptually
you're just assigning classes the same way as `className` on a DOM node.
But if we ever rename the `class` prop then we can do that for this one
as well.
This commit is contained in:
parent
a4d122f2d1
commit
3a5496b3f5
|
|
@ -4,15 +4,23 @@
|
|||
"private": true,
|
||||
"devDependencies": {
|
||||
"concurrently": "3.1.0",
|
||||
"http-proxy-middleware": "0.17.3",
|
||||
"react-scripts": "0.9.5"
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"@babel/plugin-proposal-private-property-in-object": "7.21.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/register": "^7.25.9",
|
||||
"express": "^4.14.0",
|
||||
"ignore-styles": "^5.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
"prestart": "cp -r ../../build/oss-experimental/* ./node_modules/",
|
||||
|
|
@ -24,5 +32,17 @@
|
|||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
require('ignore-styles');
|
||||
const babelRegister = require('babel-register');
|
||||
const babelRegister = require('@babel/register');
|
||||
const proxy = require('http-proxy-middleware');
|
||||
|
||||
babelRegister({
|
||||
ignore: /\/(build|node_modules)\//,
|
||||
ignore: [/\/(build|node_modules)\//],
|
||||
presets: ['react-app'],
|
||||
});
|
||||
|
||||
|
|
@ -37,9 +37,10 @@ app.use(express.static(path.resolve(__dirname, '..', 'build')));
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(
|
||||
'/',
|
||||
proxy({
|
||||
proxy.createProxyMiddleware({
|
||||
ws: true,
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
target: 'http://127.0.0.1:3001',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if (process.env.NODE_ENV === 'development') {
|
|||
// 'main.css': '',
|
||||
};
|
||||
} else {
|
||||
assets = require('../build/asset-manifest.json');
|
||||
assets = require('../build/asset-manifest.json').files;
|
||||
}
|
||||
|
||||
export default function render(url, res) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import React, {
|
|||
|
||||
import './Page.css';
|
||||
|
||||
import transitions from './Transitions.module.css';
|
||||
|
||||
const a = (
|
||||
<div key="a">
|
||||
<ViewTransition>
|
||||
|
|
@ -24,6 +26,17 @@ const b = (
|
|||
</div>
|
||||
);
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<ViewTransition
|
||||
className={
|
||||
transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
|
||||
}>
|
||||
<p>Slide In from Left, Slide Out to Right</p>
|
||||
</ViewTransition>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [show, setShow] = useState(false);
|
||||
useEffect(() => {
|
||||
|
|
@ -72,6 +85,7 @@ export default function Page() {
|
|||
<div>!!</div>
|
||||
</ViewTransition>
|
||||
</Activity>
|
||||
{show ? <Component /> : <p> </p>}
|
||||
</div>
|
||||
</ViewTransition>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
@keyframes enter-slide-right {
|
||||
0% {
|
||||
opacity: 0;
|
||||
translate: -200px 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes exit-slide-left {
|
||||
0% {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
translate: 200px 0;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-new(.enter-slide-right):only-child {
|
||||
animation: enter-slide-right ease-in 0.25s;
|
||||
}
|
||||
::view-transition-old(.exit-slide-left):only-child {
|
||||
animation: exit-slide-left ease-in 0.25s;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -455,7 +455,7 @@ export function unhideTextInstance(textInstance, text): void {
|
|||
// Noop
|
||||
}
|
||||
|
||||
export function applyViewTransitionName(instance, name) {
|
||||
export function applyViewTransitionName(instance, name, className) {
|
||||
// Noop
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ export type Props = {
|
|||
display?: string,
|
||||
viewTransitionName?: string,
|
||||
'view-transition-name'?: string,
|
||||
viewTransitionClass?: string,
|
||||
'view-transition-class'?: string,
|
||||
...
|
||||
},
|
||||
bottom?: null | number,
|
||||
|
|
@ -987,10 +989,15 @@ export function unhideTextInstance(
|
|||
export function applyViewTransitionName(
|
||||
instance: Instance,
|
||||
name: string,
|
||||
className: ?string,
|
||||
): void {
|
||||
instance = ((instance: any): HTMLElement);
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionName = name;
|
||||
if (className != null) {
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass = className;
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreViewTransitionName(
|
||||
|
|
@ -1000,7 +1007,7 @@ export function restoreViewTransitionName(
|
|||
instance = ((instance: any): HTMLElement);
|
||||
const styleProp = props[STYLE];
|
||||
const viewTransitionName =
|
||||
styleProp !== undefined && styleProp !== null
|
||||
styleProp != null
|
||||
? styleProp.hasOwnProperty('viewTransitionName')
|
||||
? styleProp.viewTransitionName
|
||||
: styleProp.hasOwnProperty('view-transition-name')
|
||||
|
|
@ -1014,6 +1021,21 @@ export function restoreViewTransitionName(
|
|||
: // The value would've errored already if it wasn't safe.
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
('' + viewTransitionName).trim();
|
||||
const viewTransitionClass =
|
||||
styleProp != null
|
||||
? styleProp.hasOwnProperty('viewTransitionClass')
|
||||
? styleProp.viewTransitionClass
|
||||
: styleProp.hasOwnProperty('view-transition-class')
|
||||
? styleProp['view-transition-class']
|
||||
: null
|
||||
: null;
|
||||
// $FlowFixMe[prop-missing]
|
||||
instance.style.viewTransitionClass =
|
||||
viewTransitionClass == null || typeof viewTransitionClass === 'boolean'
|
||||
? ''
|
||||
: // The value would've errored already if it wasn't safe.
|
||||
// eslint-disable-next-line react-internal/safe-string-coercion
|
||||
('' + viewTransitionClass).trim();
|
||||
}
|
||||
|
||||
export function cancelViewTransitionName(
|
||||
|
|
|
|||
|
|
@ -525,6 +525,7 @@ export function unhideInstance(instance: Instance, props: Props): void {
|
|||
export function applyViewTransitionName(
|
||||
instance: Instance,
|
||||
name: string,
|
||||
className: ?string,
|
||||
): void {
|
||||
// Not yet implemented
|
||||
}
|
||||
|
|
|
|||
|
|
@ -732,7 +732,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
|
|||
textInstance.hidden = false;
|
||||
},
|
||||
|
||||
applyViewTransitionName(instance: Instance, name: string): void {},
|
||||
applyViewTransitionName(
|
||||
instance: Instance,
|
||||
name: string,
|
||||
className: ?string,
|
||||
): void {},
|
||||
|
||||
restoreViewTransitionName(instance: Instance, props: Props): void {},
|
||||
|
||||
|
|
|
|||
|
|
@ -545,6 +545,7 @@ let viewTransitionHostInstanceIdx = 0;
|
|||
function applyViewTransitionToHostInstances(
|
||||
child: null | Fiber,
|
||||
name: string,
|
||||
className: ?string,
|
||||
collectMeasurements: null | Array<InstanceMeasurement>,
|
||||
stopAtNestedViewTransitions: boolean,
|
||||
): boolean {
|
||||
|
|
@ -574,6 +575,7 @@ function applyViewTransitionToHostInstances(
|
|||
: // If we have multiple Host Instances below, we add a suffix to the name to give
|
||||
// each one a unique name.
|
||||
name + '_' + viewTransitionHostInstanceIdx,
|
||||
className,
|
||||
);
|
||||
viewTransitionHostInstanceIdx++;
|
||||
} else if (
|
||||
|
|
@ -592,6 +594,7 @@ function applyViewTransitionToHostInstances(
|
|||
applyViewTransitionToHostInstances(
|
||||
child.child,
|
||||
name,
|
||||
className,
|
||||
collectMeasurements,
|
||||
stopAtNestedViewTransitions,
|
||||
)
|
||||
|
|
@ -664,6 +667,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
|
|||
const inViewport = applyViewTransitionToHostInstances(
|
||||
child.child,
|
||||
props.name,
|
||||
props.className,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
|
@ -686,14 +690,13 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
|
|||
|
||||
function commitEnterViewTransitions(placement: Fiber): void {
|
||||
if (placement.tag === ViewTransitionComponent) {
|
||||
const name = getViewTransitionName(
|
||||
placement.memoizedProps,
|
||||
placement.stateNode,
|
||||
);
|
||||
const props: ViewTransitionProps = placement.memoizedProps;
|
||||
const name = getViewTransitionName(props, placement.stateNode);
|
||||
viewTransitionHostInstanceIdx = 0;
|
||||
const inViewport = applyViewTransitionToHostInstances(
|
||||
placement.child,
|
||||
name,
|
||||
props.className,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
|
@ -747,6 +750,7 @@ function commitDeletedPairViewTransitions(
|
|||
const inViewport = applyViewTransitionToHostInstances(
|
||||
child.child,
|
||||
name,
|
||||
props.className,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
|
@ -787,6 +791,7 @@ function commitExitViewTransitions(
|
|||
const inViewport = applyViewTransitionToHostInstances(
|
||||
deletion.child,
|
||||
name,
|
||||
props.className,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
|
@ -840,11 +845,13 @@ function commitBeforeUpdateViewTransition(current: Fiber): void {
|
|||
// be unexpected but it is in line with the semantics that the ViewTransition is its
|
||||
// own layer that cross-fades its content when it updates. If you want to reorder then
|
||||
// each child needs its own ViewTransition.
|
||||
const name = getViewTransitionName(current.memoizedProps, current.stateNode);
|
||||
const props: ViewTransitionProps = current.memoizedProps;
|
||||
const name = getViewTransitionName(props, current.stateNode);
|
||||
viewTransitionHostInstanceIdx = 0;
|
||||
applyViewTransitionToHostInstances(
|
||||
current.child,
|
||||
name,
|
||||
props.className,
|
||||
(current.memoizedState = []),
|
||||
true,
|
||||
);
|
||||
|
|
@ -856,11 +863,13 @@ function commitNestedViewTransitions(changedParent: Fiber): void {
|
|||
if (child.tag === ViewTransitionComponent) {
|
||||
// In this case the outer ViewTransition component wins but if there
|
||||
// was an update through this component then the inner one wins.
|
||||
const name = getViewTransitionName(child.memoizedProps, child.stateNode);
|
||||
const props: ViewTransitionProps = child.memoizedProps;
|
||||
const name = getViewTransitionName(props, child.stateNode);
|
||||
viewTransitionHostInstanceIdx = 0;
|
||||
applyViewTransitionToHostInstances(
|
||||
child.child,
|
||||
name,
|
||||
props.className,
|
||||
(child.memoizedState = []),
|
||||
false,
|
||||
);
|
||||
|
|
@ -1002,9 +1011,10 @@ function measureViewTransitionHostInstances(
|
|||
parentViewTransition.flags |= AffectedParentLayout;
|
||||
}
|
||||
if ((parentViewTransition.flags & Update) !== NoFlags) {
|
||||
const props: ViewTransitionProps = parentViewTransition.memoizedProps;
|
||||
// We might update this node so we need to apply its new name for the new state.
|
||||
const newName = getViewTransitionName(
|
||||
parentViewTransition.memoizedProps,
|
||||
props,
|
||||
parentViewTransition.stateNode,
|
||||
);
|
||||
applyViewTransitionName(
|
||||
|
|
@ -1014,6 +1024,7 @@ function measureViewTransitionHostInstances(
|
|||
: // If we have multiple Host Instances below, we add a suffix to the name to give
|
||||
// each one a unique name.
|
||||
newName + '_' + viewTransitionHostInstanceIdx,
|
||||
props.className,
|
||||
);
|
||||
}
|
||||
if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {getTreeId} from './ReactFiberTreeContext';
|
|||
|
||||
export type ViewTransitionProps = {
|
||||
name?: string,
|
||||
className?: string,
|
||||
children?: ReactNodeList,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ export function unhideTextInstance(
|
|||
export function applyViewTransitionName(
|
||||
instance: Instance,
|
||||
name: string,
|
||||
className: ?string,
|
||||
): void {
|
||||
// Noop
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user