mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ebfeae98084dfeab97bd8dbe72133e14eb0dbd60
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
42a57ea802 |
Merge ViewTransition layout/onLayout props into update/onUpdate (#32723)
We currently have the ability to have a separate animation for a ViewTransition that relayouts but doesn't actually have any internal mutations. This can be useful if you want to separate just a move from for example flashing an update. However, we're concerned that this might be more confusion than its worth because subtle differences in mutations can cause it to trigger the other case. The existence of the property name might also make you start looking for it to solve something that it's not meant for. We already fallback to using the "update" property if it exists but layout doesn't. So if we ever decide to add this back it would backwards compatible. We've also shown in implementation that it can work. |
||
|
|
c4a3b92e09 |
Add more phases to the ReactFiberApplyGesture (#32578)
Stacked on #32585 and #32605. This adds more loops for the phases of "Apply Gesture". It doesn't implement the interesting bit yet like adding view-transition-names and measurements. I'll do that in a separate PR to keep reviewing easier. The three phases of this approach is roughly: - Clone and apply names to the "old" state. - Inside startViewTransition: Apply names to the "new" state. Measure both the "old" and "new" state to know whether to cancel some of them. Delete the clones which will include all the "old" names. - After startViewTransition: Restore "new" names back to no view-transition-name. Since we don't have any other Effects in these phases we have a bit more flexibility and we can avoid extra phases that traverse the tree. I've tried to avoid any additional passes. An interesting consequence of this approach is that we could measure both the "old" and "new" state before `startViewTransition`. This would be more efficient because we wouldn't need to take View Transition snapshots of parts of the tree that won't actually animate. However, that would require an extra pass and force layout earlier. It would also have different semantics from the fire-and-forget View Transitions because we could optimize better which can be visible. It would also not account for any late mutations. So I decided to instead let the layout be computed by painting as usual and then measure both "old" and "new" inside the startViewTransition instead. Then canceling anything that doesn't animate to keep it consistent. Unfortunately, though there's not a lot of code sharing possible in these phases because the strategy is so different with the cloning and because the animation is performed in reverse. The "finishedWork" Fiber represents the "old" state and the "current" Fiber represents the "new" state. The most complicated phase is the cloning. I actually ended up having to make a very different pattern from the other phases and CommitWork in general. Because we have to clone as we go and also do other things like apply names and finding pairs, it has more phases. I ended up with an approach that uses three different loops. The outer one for updated trees, one for inserted trees that don't need cloning (doesn't include reappearing offscreen) and one for not updated trees that still need cloning. Inside each loop it can also be in different phases which I track with the `visitPhase` enum - this pattern is kind of new. Additionally, we need to measure the cloned nodes after we've applied mutations to them and we have to wait until the whole tree is inserted. We don't have a reference to these DOM elements in the Fiber tree since that still refers to the original ones. We need to store the cloned elements somewhere. So I added a temporary field on the ViewTransitionState to keep track of any clones owned by that ViewTransition. When we deep clone an unchanged subtree we don't have DOM element instances. It wouldn't be quite safe to try to find them from the tree structure. So we need to avoid the deep clones if we might need DOM elements. Therefore we keep traversing in the case where we need to find nested ViewTransition boundaries that are either potentially affected by layout or a "pair". For the other two phases the pattern there's a lot of code duplication since it's slightly different from the commit ones but they at least follow the same pattern. For the restore phase I was actually able to reuse most of the code. I don't love how much code this is. |
||
|
|
028c8e6cf5 |
Add Transition Types (#32105)
This adds an isomorphic API to add Transition Types, which represent the
cause, to the current Transition. This is currently mainly for View
Transitions but as a concept it's broader and we might expand it to more
features and object types in the future.
```js
import { unstable_addTransitionType as addTransitionType } from 'react';
startTransition(() => {
addTransitionType('my-transition-type');
setState(...);
});
```
If multiple transitions get entangled this is additive and all
Transition Types are collected. You can also add more than one type to a
Transition (hence the `add` prefix).
Transition Types are reset after each commit. Meaning that `<Suspense>`
revealing after a `startTransition` does not get any View Transition
types associated with it.
Note that the scoping rules for this is a little "wrong" in this
implementation. Ideally it would be scoped to the nearest outer
`startTransition` and grouped with any `setState` inside of it.
Including Actions. However, since we currently don't have AsyncContext
on the client, it would be too easy to drop a Transition Type if there
were no other `setState` in the same `await` task. Multiple Transitions
are entangled together anyway right now as a result. So this just tracks
a global of all pending Transition Types for the next Transition. An
inherent tricky bit with this API is that you could update multiple
roots. In that case it should ideally be associated with each root.
Transition Tracing solves this by associating a Transition with any
updates that are later collected but this suffers from the problem
mentioned above. Therefore, I just associate Transition Types with one
root - the first one to commit. Since the View Transitions across roots
are sequential anyway it kind of makes sense that only one really is the
cause and the other one is subsequent.
Transition Types can be used to apply different animations based on what
caused the Transition. You have three different ways to choose from for
how to use them:
## CSS
It integrates with [View Transition
Types](https://www.w3.org/TR/css-view-transitions-2/#active-view-transition-pseudo-examples)
so you can match different animations based on CSS scopes:
```css
:root:active-view-transition-type(my-transition-type) {
&::view-transition-...(...) {
...
}
}
```
This is kind of a PITA to write though and if you have a CSS library
that provide View Transition Classes it's difficult to import those into
these scopes.
## Class per Type
This PR also adds an object-as-map form that can be passed to all
`className` properties:
```js
<ViewTransition className={{
'my-navigation-type': 'hello',
'default': 'world',
}}>
```
If multiple types match, then they're joined together. If no types match
then the special `"default"` entry is used instead. If any type has the
value `"none"` then that wins and the ViewTransition is disabled (not
assigned a name).
These can be combined with `enter`/`exit`/`update`/`layout`/`share`
props to match based on kind of trigger and Transition Type.
```js
<ViewTransition enter={{
'navigation-back': 'enter-right',
'navigation-forward': 'enter-left',
}}
exit={{
'navigation-back': 'exit-right',
'navigation-forward': 'exit-left',
}}>
```
## Events
In addition, you can also observe the types in the View Transition Event
callbacks as the second argument. That way you can pick different
imperative Animations based on the cause.
```js
<ViewTransition onUpdate={(inst, types) => {
if (types.includes('navigation-back')) {
...
} else if (types.includes('navigation-forward')) {
...
} else {
...
}
}}>
```
## Future
In the future we might expose types to `useEffect` for more general
purpose usage. This would also allow non-View Transition based
Animations such as existing libraries to use this same feature to
coordinate the same concept.
We might also allow richer objects to be passed along here. Only the
strings would apply to View Transitions but the imperative code and
effects could do something else with them.
|
||
|
|
cabd8a0e70 |
View Transition Class Names based on event kind (#32050)
This adds five props to `<ViewTransition>` that adds a specific
`view-transition-class` when React wants to animate it based on the
heuristic that triggers.
```js
<ViewTransition
enter="slide-from-left"
exit="slide-to-right"
layout="slide"
update="none"
share="cross-fade"
>
```
- `enter`: The <ViewTransition> or its parent Component is mounted and
there's no other <ViewTransition> with the same name being deleted.
- `exit`: The <ViewTransition> or its parent Component is unmounted and
there's no other <ViewTransition> with the same name being deleted.
- `layout`: There are no updates to the content inside this
<ViewTransition> boundary itself but the boundary has resized or moved
due to other changes to siblings.
- `share`: This <ViewTransition> is being mounted and another
<ViewTransition> instance with the same name is being unmounted
elsewhere.
- `update`: The content of <ViewTransition> has changed either due to
DOM mutations or because an inner child <ViewTransition> has resized.
The existing `className` is the baseline and the others are added to it
to combine.
This is convenient to distinguish things like `enter` / `exit` but that
can already be expressed as CSS. The other cases can't be expressed as
purely CSS.
`"none"` is a special value that deactivates the view transition name
under that condition.
The most important feature of this is that you can now limit View
Transitions to only tigger when a particular DOM node is affected, not
when just any child updates, by opt-ing out a subtree. This is safer
when added to shared parent.
```js
<ViewTransition>
<div>
<ViewTransition className="none">
{children}
</ViewTransition>
</div>
</ViewTransition>
```
This can't be fully expressed using neither just CSS nor the imperative
refs API since we need some way to have already removed the
`view-transition-name` when this happens. When you think about the
implementation details it might seem a bit strange that you specify the
`class` to `none` to remove the `name` but it's really about picking
which animation should happen for that case default (`undefined`), a
specific one (class) or none (`"none"`).
|
||
|
|
540efebcc3 |
View Transition Events (#32041)
This adds five events to `<ViewTransition>` that triggers when React
wants to animate it.
- `onEnter`: The `<ViewTransition>` or its parent Component is mounted
and there's no other `<ViewTransition>` with the same name being
deleted.
- `onExit`: The `<ViewTransition>` or its parent Component is unmounted
and there's no other `<ViewTransition>` with the same name being
deleted.
- `onLayout`: There are no updates to the content inside this
`<ViewTransition>` boundary itself but the boundary has resized or moved
due to other changes to siblings.
- `onShare`: This `<ViewTransition>` is being mounted and another
`<ViewTransition>` instance with the same name is being unmounted
elsewhere.
- `onUpdate`: The content of `<ViewTransition>` has changed either due
to DOM mutations or because an inner child `<ViewTransition>` has
resized.
Only one of these events is fired per Transition. If you want to cover
all updates you have to listen to `onLayout`, `onShare` and `onUpdate`.
We could potentially do something like fire `onUpdate` if `onLayout` or
`onShare` isn't specified but it's a little sketchy to have behavior
based on if someone is listening since it limits adding wrappers that
may or may not need it.
Each takes a `ViewTransitionInstance` as an argument so you don't need a
ref to animate it.
```js
<ViewTransition onEnter={inst => inst.new.animate(keyframes, options)}>
```
The timing of this event is after the View Transition's `ready` state
which means that's too late to do any changes to the View Transition's
snapshots but now both the new and old pseudo-elements are ready to
animate.
The order of `onExit` is parent first, where as the others are child
first. This mimics effect mount/unmount.
I implement this by adding to a queue in the commit phase and then call
it while we're finishing up the commit. This is after layout effects but
before passive effects since passive effects fire after the animation is
`finished`.
|
||
|
|
0bf1f39ec6 |
View Transition Refs (#32038)
This adds refs to View Transition that can resolve to an instance of:
```js
type ViewTransitionRef = {
name: string,
group: Animatable,
imagePair: Animatable,
old: Animatable,
new: Animatable,
}
```
Animatable is a type that has `animate(keyframes, options)` and
`getAnimations()` on it. It's the interface that exists on Element that
lets you start animations on it. These ones are like that but for the
four pseudo-elements created by the view transition.
If a name changes, then a new ref is created. That way if you hold onto
a ref during an exit animation spawned by the name change, you can keep
calling functions on it. It will keep referring to the old name rather
than the new name.
This allows imperative control over the animations instead of using CSS
for this.
```js
const viewTransition = ref.current;
const groupAnimation = viewTransition.group.animate(keyframes, options);
const imagePairAnimation = viewTransition.imagePair.animate(keyframes, options);
const oldAnimation = viewTransition.old.animate(keyframes, options);
const newAnimation = viewTransition.new.animate(keyframes, options);
```
The downside of using this API is that it doesn't work with SSR so for
SSR rendered animations they'll fallback to the CSS. You could use this
for progressive enhancement though.
Note: In this PR the ref only controls one DOM node child but there can
be more than one DOM node in the ViewTransition fragment and they are
just left to their defaults. We could try something like making the
`animate()` function apply to multiple children but that could lead to
some weird consequences and the return value would be difficult to
merge. We could try to maintain an array of Animatable that updates with
how ever many things are currently animating but that makes the API more
complicated to use for the simple case. Conceptually this should be like
a fragment so we would ideally combine the multiple children into a
single isolate if we could. Maybe one day the same name could be applied
to multiple children to create a single isolate. For now I think I'll
just leave it like this and you're really expect to just use it with one
DOM node. If you have more than one they just get the default animations
from CSS.
Using this is a little tricky due timing. In this fixture I just use a
layout effect plus rAF to get into the right timing after the
startViewTransition is ready. In the future I'll add an event that fires
when View Transitions heuristics fire with the right timing.
|
||
|
|
3a5496b3f5 |
[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.](https://github.com/facebook/react/commit/8b91b37bb8b4add5f3f8be5ef8f49bb23966b13b#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. |
||
|
|
a4d122f2d1 |
Add <ViewTransition> Component (#31975)
This will provide the opt-in for using [View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) in React. View Transitions only trigger for async updates like `startTransition`, `useDeferredValue`, Actions or `<Suspense>` revealing from fallback to content. Synchronous updates provide an opt-out but also guarantee that they commit immediately which View Transitions can't. There's no need to opt-in to View Transitions at the "cause" side like event handlers or actions. They don't know what UI will change and whether that has an animated transition described. Conceptually the `<ViewTransition>` component is like a DOM fragment that transitions its children in its own isolate/snapshot. The API works by wrapping a DOM node or inner component: ```js import {ViewTransition} from 'react'; <ViewTransition><Component /></ViewTransition> ``` The default is `name="auto"` which will automatically assign a `view-transition-name` to the inner DOM node. That way you can add a View Transition to a Component without controlling its DOM nodes styling otherwise. A difference between this and the browser's built-in `view-transition-name: auto` is that switching the DOM nodes within the `<ViewTransition>` component preserves the same name so this example cross-fades between the DOM nodes instead of causing an exit and enter: ```js <ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition> ``` This becomes especially useful with `<Suspense>` as this example cross-fades between Skeleton and Content: ```js <ViewTransition> <Suspense fallback={<Skeleton />}> <Content /> </Suspense> </ViewTransition> ``` Where as this example triggers an exit of the Skeleton and an enter of the Content: ```js <Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}> <ViewTransition><Content /></ViewTransition> </Suspense> ``` Managing instances and keys becomes extra important. You can also specify an explicit `name` property for example for animating the same conceptual item from one page onto another. However, best practices is to property namespace these since they can easily collide. It's also useful to add an `id` to it if available. ```js <ViewTransition name="my-shared-view"> ``` The model in general is the same as plain `view-transition-name` except React manages a set of heuristics for when to apply it. A problem with the naive View Transitions model is that it overly opts in every boundary that *might* transition into transitioning. This is leads to unfortunate effects like things floating around when unrelated updates happen. This leads the whole document to animate which means that nothing is clickable in the meantime. It makes it not useful for smaller and more local transitions. Best practice is to add `view-transition-name` only right before you're about to need to animate the thing. This is tricky to manage globally on complex apps and is not compositional. Instead we let React manage when a `<ViewTransition>` "activates" and add/remove the `view-transition-name`. This is also when React calls `startViewTransition` behind the scenes while it mutates the DOM. I've come up with a number of heuristics that I think will make a lot easier to coordinate this. The principle is that only if something that updates that particular boundary do we activate it. I hope that one day maybe browsers will have something like these built-in and we can remove our implementation. A `<ViewTransition>` only activates if: - If a mounted Component renders a `<ViewTransition>` within it outside the first DOM node, and it is within the viewport, then that ViewTransition activates as an "enter" animation. This avoids inner "enter" animations trigger when the parent mounts. - If an unmounted Component had a `<ViewTransition>` within it outside the first DOM node, and it was within the viewport, then that ViewTransition activates as an "exit" animation. This avoids inner "exit" animations triggering when the parent unmounts. - If an explicitly named `<ViewTransition name="...">` is deep within an unmounted tree and one with the same name appears in a mounted tree at the same time, then both are activated as a pair, but only if they're both in the viewport. This avoids these triggering "enter" or "exit" animations when going between parents that don't have a pair. - If an already mounted `<ViewTransition>` is visible and a DOM mutation, that might affect how it's painted, happens within its children but outside any nested `<ViewTransition>`. This allows it to "cross-fade" between its updates. - If an already mounted `<ViewTransition>` resizes or moves as the result of direct DOM nodes siblings changing or moving around. This allows insertion, deletion and reorders into a list to animate all children. It is only within one DOM node though, to avoid unrelated changes in the parent to trigger this. If an item is outside the viewport before and after, then it's skipped to avoid things flying across the screen. - If a `<ViewTransition>` boundary changes size, due to a DOM mutation within it, then the parent activates (or the root document if there are no more parents). This ensures that the container can cross-fade to avoid abrupt relayout. This can be avoided by using absolutely positioned children. When this can avoid bubbling to the root document, whatever is not animating is still responsive to clicks during the transition. Conceptually each DOM node has its own default that activates the parent `<ViewTransition>` or no transition if the parent is the root. That means that if you add a DOM node like `<div><ViewTransition><Component /></ViewTransition></div>` this won't trigger an "enter" animation since it was the div that was added, not the ViewTransition. Instead, it might cause a cross-fade of the parent ViewTransition or no transition if it had no parent. This ensures that only explicit boundaries perform coarse animations instead of every single node which is really the benefit of the View Transitions model. This ends up working out well for simple cases like switching between two pages immediately while transitioning one floating item that appears on both pages. Because only the floating item transitions by default. Note that it's possible to add manual `view-transition-name` with CSS or `style={{ viewTransitionName: 'auto' }}` that always transitions as long as something else has a `<ViewTransition>` that activates. For example a `<ViewTransition>` can wrap a whole page for a cross-fade but inside of it an explicit name can be added to something to ensure it animates as a move when something relates else changes its layout. Instead of just cross-fading it along with the Page which would be the default. There's more PRs coming with some optimizations, fixes and expanded APIs. This first PR explores the above core heuristic. --------- Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com> |