w3c / csswg-drafts

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

[css-view-transitions-2] Script event on new Document for cross-document ViewTransitions #8805

Closed khushalsagar closed 1 year ago

khushalsagar commented 1 year ago

View Transition API needs to provide an event on the new Document to enable customization of the transition based on the URL for the old Document and its state when the transition was initiated. For example, the old Document could have changed from user interaction or the transition could depend on which click initiated the navigation.

The proposed IDL is as follows:

[Exposed=Window]
interface ViewTransitionBeforeAnimationEvent : Event {
  // The URL being navigated from,.
  readonly attribute USVString url;

  // Opaque contextual information passed from the old Document.
  // This must be a serializable object : https://developer.mozilla.org/en-US/docs/Glossary/Serializable_object.
  attribute any context;

  // The transition object associated with this navigation.
  ViewTransitionOnNavigation transition;
};

The ViewTransition interface is also split into a base IDL with functionality common to same-document and cross-document transitions.

[Exposed=Window]
interface ViewTransitionBase {
  readonly attribute Promise ready;
  readonly attribute Promise finished;
  undefined skipTransition();
};

[Exposed=Window]
interface ViewTransition : ViewTransitionBase {
   readonly attribute Promise updateCallbackDone;
};

[Exposed=Window]
interface ViewTransitionOnNavigation : ViewTransitionBase {
};

The following is sample code using this event:

document.addEventListener("viewtransitionbeforeanimation", (event) => {
  // Cancel the transition (based on old URL) if needed.
  if (shouldNotTransition(event.url)) {
    event.preventDefault();
    return;
  }

  const transition = event.transition;
  const info = event.info;

  // Add render-blocking resources to delay the first paint and transition
  // start. This can be customized based on the old Document state when the
  // transition was initiated.
  markRenderBlockingResources(info);

  // The `ready` promise resolves when the pseudo-elements have been generated
  // and can be used to customize animations via script.
  transition.ready.then(() => {
    document.documentElement.animate(...,
       {
         // Specify which pseudo-element to animate
         pseudoElement: "::view-transition-new(root)",
       }
    );

    // Remove viewTransitionNames tied to this transition.
    thumbnail.style.viewTransitionName = "none";
  });

  // The `finished` promise resolves when all animations for the transition are
  // finished or cancelled and the pseudo-elements have been removed.
  transition.finished.then(() => { ... });
});
noamr commented 1 year ago

There are two occasions when this event would fire:

Let's assume for a sec that we have document.activeTransition.

For the first one, I guess the developer can already put a script at the end of the <head> to make all kind of checks and mark more resources as render-blocking etc, and use document.referrer or whatever query variable to customize?

For the second one, pageshow should be sufficient. We can consider adding the reveal event to HTML (see whatwg/html#9315) but I want to have the use cases spelled out to make the case for it.

If I look at the above example, the steps that should be done before first render are different from the steps to be done at reactivation. Perhaps we should keep that explicit rather than merge those into one event?

This is how the above example would look when only exposing document.activeTransition:

<head>
<script>
const info = sessionStorage.getItem("pre-transition-info");
if (document.activeTransition)
  markRenderBlockingResources(info);
function setupCustomAnimation() {
    const transition = document.activeTransition;
    if (!transition)
      return;
    transition.ready.then(() => {
    document.documentElement.animate(...,
       {
         // Specify which pseudo-element to animate
         pseudoElement: "::view-transition-new(root)",
       }
    );

    // Remove viewTransitionNames tied to this transition.
    thumbnail.style.viewTransitionName = "none";
  });

  // The `finished` promise resolves when all animations for the transition are
  // finished or cancelled and the pseudo-elements have been removed.
  transition.finished.then(() => { ... });
}

setupCustomAnimation();
window.addEventListener("pageshow", () => setupCustomAnimation());
</script>
</head>
noamr commented 1 year ago

For the purpose of the F2F, summarizing current thinking about this. The use-cases this comes to address are:

A design principle here is to keep consistency, both with view-transitions-1 and with other web features.

There are several alternative on how to achieve this:

  1. As originally proposed in this issue, have an event that's fired only when there is an inbound view transition, e.g. inboundviewtransition or crossdocumentviewtransition.
  2. Always fire an event when a document is about to be presented (reveal), and include an optional ViewTransition object as a property of that event. This has the advantage of being compatible with css-view-transitions-1, and also the reveal event might be useful for things other than view-transitions so why not.
  3. Send separate events for start, ready and finished, without a ViewTransition object. This is compatible with e.g. how mouse/touch events work, and avoids the need to add a listener only in order to register a promise, but it's not compatible with css-view-transitions-1 where the ViewTransition object is promise-based.

Note that as the OP states, updateCallbackDone is irrelevant for cross-document view transitions. The current spec proposal is to simply have it as a resolved promise rather than have different IDL types and inheritance.

The current spec draft goes with option (2), as it seems to be the most compatible with VT-1.

See example in the spec draft.

css-meeting-bot commented 1 year ago

The CSS Working Group just discussed [css-view-transitions-2] Script event on new Document for cross-document ViewTransitions, and agreed to the following:

The full IRC log of that discussion <noamr> https://github.com/w3c/csswg-drafts/issues/8805#issuecomment-1637790887
<fantasai> noamr: This comment summarizes the issue
<fantasai> noamr: Let's say both documents opted into a transition
<fantasai> noamr: We are at the state where the new document is ready to present and activate the transition
<fantasai> noamr: Some transitions need to be in JS
<fantasai> noamr: e.g. animating using WebAnimations API
<fantasai> noamr: Another use case is JS wanting to have access to the ViewTransition object
<fantasai> noamr: to know when it ends, or to be able to call skipTransition()
<fantasai> noamr: This is a problem we're trying to solve
<fantasai> noamr: Currently in the draft spec, we invented a new event "reveal"
<fantasai> noamr: right before the first frame
<fantasai> noamr: Right when the document is unblocked for render
<fantasai> noamr: That event will have an optional ViewTransition object
<fantasai> noamr: You can sign up to its promises, or skip it
<fantasai> noamr: The "reveal" event could be useful to HTML in any case
<fantasai> noamr: page visibility events only fire when the document is fully loaded
<fantasai> noamr: so this is how it's currently presented in the draft spec
<fantasai> noamr: We like it for this purpose, and also because it lets us keep the existing ViewTransition object
<fantasai> noamr: One of the design principles is that same-page and cross-page transitions should be as similar as possible wrt API a
<fantasai> noamr: Another idea was to only send an event for actual view transitions, not for every reveal
<fantasai> noamr: That event would require the ViewTransition object, wouldn't be optional
<fantasai> noamr: Another idea was to fire events for when the VT starts, is ready, and finished
<fantasai> noamr: This is a little more consistent with touch events etc.
<fantasai> noamr: where they have discrete events, rather than register to one event
<fantasai> noamr: In all cases, updateCallbackDone promise would be resolved from the beginning, as it's not relevant
<fantasai> astearns: My question about reveal event, say it might be useful for other than VT. Did you have examples in mind?
<fantasai> noamr: It's a place where you can register for events, e.g. when the first content paints etc.
<fantasai> noamr: and also place where you can do last-minute things that affect presentation
<fantasai> noamr: e.g. if you think not enough of the document is parsed, so you want to hide some elements
<khush> q+
<fantasai> noamr: other use cases have not been explored much, but it felt like this is a new lifecycle point that we should expose rather than only expose the one use case for VT
<astearns> ack khush
<fantasai> khush: Concrete use case from WebPerf, if you're adding new resources that you want to block rendering, this event lets you measure that
<fantasai> khush: Because it fires right before firstRender, it allows customizing your transition
<fantasai> khush: E.g. if you're going from one page to another, but the image hasn't been fetched yet, you might choose to do a different transition
<fantasai> astearns: In addition to testing blocking things, you could use it to test blocking in general. Since if blocking, would affect when this event fires
<fantasai> astearns: Any more comments about the event?
<astearns> ack fantasai
<TabAtkins> fantasai: I don't remember from the spec (even tho i recently read it) - are there events for same-page tranitions?
<TabAtkins> fantasai: or was it just promises?
<TabAtkins> noamr: promises only
<TabAtkins> fantasai: in that case i don't htink we should do the third option (separate events for stages). if we want that we should do it for both
<SebastianZ> q+
<TabAtkins> fantasai: and if we want it for same-document we should also have it for cross-document
<fantasai> noamr: Reason I brought up option 3, if you just want to respond when transition is finished
<fantasai> noamr: you have to add an event listener onto the promise. Jump through hoops
<fantasai> noamr: but it's a trade-off between that and same-document
<fantasai> khush: If you have just one event with the VT object, you have access to everything you need
<fantasai> khush: In same-document, you already have the VT
<fantasai> khush: [something about customizing]
<fantasai> khush: This aspect of reveal event conceptually maps to one of the promises in smae-document case
<fantasai> khush: in that case you have your own callback, which sets up the DOM, and then the first promise resolves
<fantasai> khush: since no callback setting up the DOM in cross-document case, we need some event to do conceptually what's happening
<fantasai> khush: to let you know when that's done
<astearns> ack SebastianZ
<fantasai> SebastianZ: I think we should still do it for both same-document and cross-document transitions
<fantasai> SebastianZ: for consistency
<fantasai> SebastianZ: I would have one event for them and then you could have an attribute on those events to distinguish which kind of transition it actually is
<fantasai> noamr: What's the proposal exactly?
<fantasai> SebastianZ: Maybe start/ready/finished and distinguish by an attribute on the event whether it's cross-origin or same-document
<fantasai> [some clarifications on naming]
<fantasai> SebastianZ: My point was to have one event that is covering all those cases, and distinguish by the attributes of those document
<fantasai> noamr: so also when you have a transition you started yourself?
<fantasai> SebastianZ: for consistency
<fantasai> astearns: If we have this reveal event, and it fires always before first render
<fantasai> astearns: it will fire for every document
<fantasai> astearns: when you have a same-origin transition
<fantasai> astearns: and you have this event firing, does it contain the VT object even though it's not a cross-origin VT
<bramus> fantasai: i think we are getting confused here. There are cross doc and same doc transitions
<bramus> … for same docs we have an existing api and proposal
<bramus> … in same doc case you create a VT by calling a method
<bramus> … you know you created that, because you said so
<bramus> … when doing cross-doc, you need to get the VT object which the UA creates automatically
<bramus> … for that we need an event, or to add it to the doc somehow
<khush> thanks for the excellent summary fantasai!
<bramus> … we are alking about 3 options
<bramus> … events for phases? yes, we should have them for same-doc as cross doc
<bramus> … [missed]
<khush> q+
<astearns> ack fantasai
<astearns> aack khush
<astearns> ack khush
<fantasai> khush: thanks for the summary
<fantasai> khush: My vote is for one event that gives you access to the VT object
<fantasai> khush: if we have multiple events, having it for the same document transitions is awkward
<fantasai> khush: because we will soon have ability to trigger multiple same-document transitions
<fantasai> khush: so if this event is limited to having a cross-document transition
<fantasai> khush: [missed]
<fantasai> khush: seems much easier to wrap your head around
<TabAtkins> fantasai: so i fthe start/ready/finsihed events for samedocumetn doesn't make sense
<TabAtkins> fantasai: then we should def go for one of th eother two options
<TabAtkins> fantasai: either special event that only fires for a VT or the reveal event
<TabAtkins> fantasai: interested in hearing about the perf impls
<TabAtkins> fantasai: and wondering hwo html folks feel about reveal more generally
<fantasai> astearns: Any thoughts on perf?
<fantasai> [silence]
<emilio> q+
<fantasai> astearns: Have you discussed with HTML folks about new event?
<fantasai> noamr: Was discussed in HTML ??
<fantasai> noamr: don't think there's any particular perf implications
<fantasai> noamr: wanted to understand use case better
<fantasai> emilio: isn't the reveal effectively the first request Animation Frame callback?
<fantasai> noamr: it's before that
<fantasai> noamr: rAF happens [missed]
<fantasai> noamr: Reveal happens right when last render-blocking element is unblocked, so when you're about to have your first rAF callback
<fantasai> noamr: but before actual rendering
<fantasai> noamr: if you write polyfill code today, you would check all your render-blocking styles and scripts, and wait until all loaded, and then fire an event
<fantasai> emilio: so point is you cannot insert new render-blocking stuff then, because those are only parser-inserted?
<fantasai> noamr: you have to also listen for HEAD being finished.You can't add more render-blocking at that point
<fantasai> emilio: is there use case for exposing this timing, rather than firing right before first rAF callback?
<fantasai> emilio: presumably point of reveal event is you might want DOM set up in a particular way
<fantasai> emilio: and that's what rAF does
<fantasai> emilio: so it would avoid exposing a new timepoint
<fantasai> emilio: but I don't feel strongly either way
<fantasai> ???: Several reasons to make this appealing
<fantasai> ???: First is providing the VT object -- rAF can't provide that
<astearns> s/???/vmpstr
<emilio> s/???/vmpstr
<fantasai> vmpstr: second is back/forward
<fantasai> vmpstr: ...
<fantasai> [missed something]
<fantasai> khush: can discuss at HTML meeting, if there's more thinking about timing can bring with HTML group
<fantasai> khush: this is useful for perf measurement
<astearns> ack emilio
<fantasai> emilio: yes, it seems like something that HTML folks would have an opinion on
<fantasai> emilio: but seems like using first request animation frame isn't quite doable ecause of the ?? cache
<fantasai> emilio: if we need a new event, as long as it's well-defined when it should fire, seems OK
<fantasai> emilio: just feels a bit weird to expose this extra last render-blocking thing
<fantasai> emilio: especially because a bunch of stylesheets can be loaded async
<fantasai> emilio: authors would observe
<fantasai> emilio: so if you have a stylesheet in the memory cache, right now that effectively doesn't block rendering
<fantasai> emilio: but from PoV of author, it probably should behave like that
<fantasai> emilio: and I think right now what we do is fire load event
<fantasai> emilio: so timing details is worth raising with HTML folks
<fantasai> emilio: but need it to happen before page is presented
<fantasai> emilio: so can't do rAF or synchornously when last stylesheet loads etc.
<astearns> ack fantasai
<emilio> fantasai: it sounds we could resolve we want a single event with the vt object attached
<emilio> ... not sure we want it to be fired only for vt or something more generic
<emilio> ... is that what we're landing
<emilio> khush: also emilio's question about when this is fired
<emilio> fantasai: which might depend on which of those two we land on
<emilio> fantasai: in that case we should resolve on that and then discuss further with the html folks?
<fantasai> astearns: So let's resolve to have a single event to carry the VT object
<fantasai> astearns: and work with HTML folks to define exact timing and whether it's a vT-specific event or generic
<fantasai> astearns: any objections?
<fantasai> RESOLVED: Add a single event to carry the cross-document VT object. Work with HTML folks to define exact timing, and whether it's a VT-specific event or generic (always fired)
noamr commented 1 year ago

I'm pretty convinced that we should fire the reveal event even when there is no view transition. The use case is that authors that do use view transitions, might want to do something specifically if the view transition is not present (e.g. the previous page hasn't opted in. e.g.:

document.addEventListener("reveal", event => {
   if (event.viewTransition) {
     event.viewTransition.ready.then(() => { animate... });
     event.viewTransition.finished.then(() => { postTranisitonChanges(); });
   } else {
     postTranisitonChanges();
   }
});
jakearchibald commented 1 year ago

Is reveal just pageshow, but without the stupid thing where pageshow initially fires after window onload?

noamr commented 1 year ago

Is reveal just pageshow, but without the stupid thing where pageshow initially fires after window onload?

Yes exactly, plus includes a property that refs the ViewTransition if it's there. That thing with pageshow is really strange IMO.

khushalsagar commented 1 year ago

Is reveal just pageshow, but without the stupid thing where pageshow initially fires after window onload?

FWIW, based on the comments I've seen it looks like pageshow/pagehide were designed to be replacements for load/unload if some logic needed to be done for BFCache also. That's why the timing for pageshow closely aligns with load. Unfortunate naming but it was never meant to be about timing display of page content.

noamr commented 1 year ago

Closing this, as pagereveal's monkey patch version is in the spec.