w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.48k stars 662 forks source link

[css-view-transitions-2] Handling DOM updates when a transition's animation is reverse back to start/canceled #7957

Open ydaniv opened 2 years ago

ydaniv commented 2 years ago

Consider a case where the user has interacted with a page in a way which caused the animation playing in reverse back to the start, or was seeked back to start, like mentioned here for example. In such case the animation would either finish back on the "old" state or get cancel()'ed by the author, and should result in showing the "old" DOM again.

Such a case will require additional DOM update to render back previous state, prior to transition start. This may be both tricky for the author to handle, and slow since the transition finishes but the DOM's state isn't ready beforehand.

I see several options here:

  1. User needs to create a new ViewTransition back to previous state - same as a "back" navigation - which will trigger the flow of a new transition starting and aborting the current one.
  2. The browser can somehow cache the old DOM (like in a DocumentFragments or something?) - either declaratively or magically - for a quick reinsertion (though a framework would still need to "rehydrate" that).
  3. Provide other endpoints on the current API for such a case.

Not sure which, I guess 1 could be the easiest default perhaps.

cc @jakearchibald @khushalsagar @vmpstr

jakearchibald commented 2 years ago

I think 1 is fine. 2 seems leaky, and unlikely to do what the developer wants.

For example, say a counter is updated via a websocket. Meanwhile, a transition is 'reversed'. The developer may want to go back to the previous view, but it's unlikely that they'd want the counter to decrement too. DOM caching wouldn't be able to differentiate between the two.

khushalsagar commented 2 years ago

Expanding on the flow to make sure I got the details right:

  1. Developer starts the transition.
  2. While the animation is progressing, the user takes an action which should restore the page back to the old state. For example, they press the back button.
  3. The developer would like the animation to play in reverse (from the current state) and end with the DOM back to the old state.

First option would be easy but creating a new ViewTransition would cause us to first jump to the DOM state that the current transition will end at.

I'm trying to think whether this could be doable with the current API. Something like:

function goBack(transition) {
  // Reverse animations on all pseudo-elements.
  // Would UA CSS animations show up in this list? And would reverse() have them go back from current state.
  for (animation: document.getAnimations())
    animation.reverse();

  // When all the reverse animations are done, update DOM back to the old state.
  transition.finished.then(updateDomToOldState);
}

The other option would be that we add an API to do the animation reverse as a convenience:

function goBack(transition) {
  transition.reverse();
  // When all the reverse animations are done, update DOM back to the old state.
  transition.finished.then(updateDomToOldState);
}

Or, include the DOM update back to old state in the API since I'm assuming it'll need to be done for every use of the API:

function goBack(transition) {
  // The reverse API takes a callback which is invoked when all animations are done but before pseudo-elements
  // are removed.
  transition.reverse(updateDomToOldState);
}
ydaniv commented 1 year ago

@khushalsagar yes. After I opened this issue I realized creating a new transition won't give us the result we need, because, as you say:

First option would be easy but creating a new ViewTransition would cause us to first jump to the DOM state that the current transition will end at.

So what we really need is reverse the animations so that the effect is running backwards from current position, not have it jump to end result and play from there. That means we're animating back from the "new" images to the "old" ones. And as the effect finishes we need to update the DOM again back to the old state and remove the "old" image that should be identical to the current state (or perhaps fade it out?). And for that I guess we need to change the API to support this.

We don't need to take another snapshot of the UI, since we already have one, we probably want to simply reverse all running animations that are playing on the pseudo-tree of the transition, not really document.getAnimations() since that will return all CSS Animations, Web Animations, and CSS Transitions on the document and its descendants. Maybe have something like transition.getAnimations()? If we have paused animations, like mentioned in #7785, they will probably be scrubbed all the way back, and then signal to the transition that we want to update DOM back to old state and finish it, could be using same method as transition.reverse().

So, I suppose it could use a method like:

transition.reverse();

As a handy way to reverse the animations, and then when it's actually done we want to update the DOM back to old state.

What I'm still not sure of is how to handle the finished promise. Should it reject?

khushalsagar commented 1 year ago

Ok. I'd be supportive of an API to reverse the transition to the old state. The assumption being that we don't need to re-snapshot anything since the old state should be identical to the snapshots we cached. Just need to ensure that the developer doesn't switch the DOM back to the old state until the reverse animations are done, since until then we're using the DOM for live snapshots of the current state.

The pattern for when it's ok to update the DOM back to the old state is:

I think the finished promise should still resolve. The reverse API simply changes the notion of what the end state is. And if that's the case then option 1 is simpler. If the developer keeps calling reverse multiple times then we'd keep flipping between going forwards/backwards? :)

jakearchibald commented 1 year ago

I handled this when creating the gesture demo and it worked pretty well.

I reversed the animation, then held the final state until I'd reset the DOM back to its original state.

The only really hacky bit, is I had to create a long dummy animation to prevent the view transition ending before the old DOM was back in place. https://github.com/w3c/csswg-drafts/issues/8132 would solve that.

I think we can close this until we have more concrete info on the patterns and what could be done to make them easier.

khushalsagar commented 1 year ago

@ydaniv wdyt? Does reversing the animation (as-in Jake's demo) + #8132 make this easy enough or do we need a better API for this pattern?

ydaniv commented 1 year ago

Yeah, what Jake showed is what I was referring to in #7785. As I understood from that resolution is that an author can pause the animation and control it via rAF to either direction, and it should keep the pseudo tree alive while the animations are paused. So I'm a little confused why we still need #8132 to solve that.

Regardless, to get the reversed transition working you need to "hold" the progress at ~0.001%, update the DOM back to previous state "manually", and then reverse all of them manually, and then the hacky bit with cancelling the "holding animation". And all of this for a rAF controlled animation.

So I think there should be a more dev-friendly way to reverse all underlaying animations and/or cancel the transition in case of:

And then we can still reuse current pseudo tree, and have a way of holding 0% progress, update DOM back, and then cancel and remove the pseudo tree.