domchristie / turn

📖 Animate page transitions in Turbo Drive apps
https://domchristie.github.io/turn/
MIT License
155 stars 7 forks source link

Possible improvements with the view transition API #15

Open YummYume opened 10 months ago

YummYume commented 10 months ago

Hello :slightly_smiling_face:

I started using Turn with the view transition API and I noticed a few things that could maybe be integrated into the library, let me know what you think (I am still learning how the API works and its best practices, but I think I have a pretty good understanding of it already).

1. Add an attribute helper to add view-transition-name on elements during transitions

I think one of the best features of the view transition API is the ability to let the browser automatically translate elements from one place to another using view-transition-name. From what I understand (and what I've tested so far), it's important to have this attribute added only during the transition and then removed if the DOM elements are not the same (which will be the case 99% of the time during visits). This is explained here.

With that, I think it's in the scope of this library to add an attribute like data-turn-transition-name that would only get added to its respective element during a view transition, and then automatically removed (by setting the view-transition-name property to an empty string or none).

It would also maybe be interesting to have it "scoped" to its current element, so it does not add the view-transition-name property on every element on the page? I don't know what the best practises for names are. e.g. I have a list of products, do I just add view-transition-name: product-title; to the product I clicked on during the transition, or add view-transition-name: product-title-1;, etc... On each product currently visible on my page?

I did a quick implementation with the current events dispatched by Turn, I have no idea if this is the best way to do it and it could probably get some improvement, but it worked pretty well during my testing.

let pendingTransitions: HTMLElement[] = [];

// Add transition name to elements with data-turn-transition-name attribute (only those inside the initiator)
window.addEventListener('turn:before-transition', (({ detail }: CustomEvent<{ initiator: HTMLElement }>) => {
  if (getPrefersReducedMotion()) {
    return;
  }

  const { initiator } = detail;
  const transitions = initiator.querySelectorAll('[data-turn-transition-name]');

  transitions.forEach((transition) => {
    if (!(transition instanceof HTMLElement)) {
      return;
    }

    // @ts-expect-error Experimental API
    transition.style.viewTransitionName = transition.dataset.turnTransitionName;

    pendingTransitions.push(transition);
  });
}) as EventListener);

// Remove the transition names we added before
window.addEventListener('turn:before-enter', (() => {
  pendingTransitions.forEach((transition) => {
    // @ts-expect-error Experimental API
    transition.style.viewTransitionName = 'none';
  });

  pendingTransitions = [];
}) as EventListener);

With that, my details page would have the transition names set

<h1 style="view-transition-name: news-title;">Some news</h1>

<p style="view-transition-name: news-desc;">News description<p>

And my results on my search page would have the data- attribute with the same name

<a href="news/some-news" class="news__result">
  <p data-turn-transition-name="news-title">Some news</p>
  <p data-turn-transition-name="news-desc">News desc...</p>
</a>

The title and description now nicely translate when clicking on the link or going back to the search page.

2. Allow hybrid configuration for Firefox/Safari

I found myself in a situation where I had to make a choice : either support only view transition browsers and hope Firefox/Safari implement it soon enough, or give up on it and use data-turn-enter & data-turn-exit only.

This happened because on a page with a navbar header, a footer and a sidebar (lots of navigation), I wanted these elements to not animate during a visit, so I would add data-turn-enter & data-turn-exit to the container with the current view. It works well but breaks the view transition API completely. It seems that as soon as I add these attributes, the view transitions are not taken into account at all (most likely because they conflict with each other as Turn is animating the whole view with a fade). It would be interesting to be able to add these attributes, but only use them if the view transition API is not available.

I know what I want to achieve would most likely be achievable by using CSS only rather than the data- attributes, but it would require more work than simply adding my Tailwind animation to the attribute.

domchristie commented 10 months ago
  1. Add an attribute helper to add view-transition-name on elements during transitions

This has been a consideration during the design of the events system. Here's how I have implemented this in a proof-of-concept:

;(function () {
  window.addEventListener('turn:before-transition', function ({ detail }) {
    let { referrer, action, initiator, newBody } = detail

    if (action === 'restore') {
      const selector = 'a[data-transition]'
      initiator = [...newBody.querySelectorAll(selector)].find(
        a => a.href === referrer
      ) || document.documentElement
    } else {
      reset()
    }

    applyNames(initiator)
    applyNames(initiator, newBody)
  })

  function applyNames (initiator, body = document.body) {
    if (!initiator.dataset.transition) return

    // <a href="…" data-transition="video_0">…</a>
    const viewTransitionIds = initiator.dataset.transition.split(' ')

    viewTransitionIds.forEach(function (id) {
      const element = body.querySelector(`[id='${id}']`)
      if (element) {
        // <video id="video_0" data-view-transition-name="video" …>
        element.style.viewTransitionName = element.dataset.viewTransitionName
      }
    })
  }

  function reset () {
    document.querySelectorAll('[data-view-transition-name]').forEach(function (e) {
      e.style.viewTransitionName = ''
    })
  }
})()

To use, give each transitioning element an id and data-view-transition-name attributes:

<video id="video_0" data-view-transition-name="video" …>

Then give links that might trigger a transition, a data-transition attribute that references any IDs of elements that will transition (space separated):

<a href="…" data-transition="video_0">View video 0</a>

On the turn:before-transition event, query the elements that match the initiator's data-transition, and apply their data-view-transition-name.

There is a few parts to this, and the initiator is only a best guess, so until it's a bit more reliable, I'm probably not going to make it a feature.

  1. Allow hybrid configuration for Firefox/Safari

Checkout the hybrid demos: https://domchristie.github.io/turn/examples/hybrid-basic/, https://domchristie.github.io/turn/examples/hybrid-advanced/

I'd recommend creating custom variants to scope the transitions by the html.turn-view-transitions and html.turn-no-view-transitions classes.

YummYume commented 10 months ago

@domchristie Thank you for the detailed process. By saying that initiator is not reliable enough, you're talking about an issue with Turbo?

As for the hybrid transitions, I think my situation is a bit more complicated because what I want to achieve is : use view transition only (if available), else use enter/leave animations. As an example of what I am trying to achieve :

<body>
  <header style="view-transition-name: header;">Header</header>
  <main>
    <aside style="view-transition-name: side-nav;">
      Some long navigation...
    </aside>
    <section data-turn-exit data-turn-enter>
      <a href="...">
        <p style="view-transition-name: news-title;">Title</p>
        <p style="view-transition-name: news-desc;">Desc</p>
      </a>
    </section>
  </main>
  <footer style="view-transition-name: footer;">Footer</footer>
</body>

Here, I would like to let the view transition translate the different elements of the page using view-transition-name, I can then edit the animations very easily with CSS. But if the view transition API is not available, then I would like Turn to animate enter/leave on the section (give it a fade out/in). It works well in theory, but in practise, the view transition API stops working because of the CSS to prevent the "flash of content" issue. The CSS is as simple as :

html.turn-no-view-transitions.turn-advance.turn-exit [data-turn-exit] {
    @apply animate-fade-out;
}

html.turn-no-view-transitions.turn-advance.turn-enter [data-turn-enter] {
    @apply animate-fade-in;
}

html.turn-before-exit [data-turn-exit],
html.turn-exit [data-turn-exit] {
    will-change: transform, opacity;
}

.turn-view-transitions.turn-transition [data-turn-enter],
.turn-view-transitions.turn-before-transition [data-turn-enter] {
    opacity: 0;
}

If I remove the last lines, then this specific scenario will work as intended, but other pages will suffer from the "flash of content" issue. I feel like I'm missing something simple, but I can't get my hand on it.

domchristie commented 10 months ago

By saying that initiator is not reliable enough, you're talking about an issue with Turbo?

Kind of! Turbo doesn't provide any details as to which element triggered the visit, so Turn makes a best guess.

If I remove the last lines, then this specific scenario will work as intended, but other pages will suffer from the "flash of content" issue. I feel like I'm missing something simple, but I can't get my hand on it.

Would you be able to provide a live demonstration of this?

YummYume commented 10 months ago

I do! I tried my best to reproduce the current situation I'm having here.

If you comment

.turn-view-transitions.turn-transition [data-turn-enter],
.turn-view-transitions.turn-before-transition [data-turn-enter] {
    opacity: 0;
}

Then the "flash of new content" issue does not appear on other pages any more, but the hybrid configuration gets broken (the view transition API is basically hidden?).

I also tried your implementation for the transition names, it seems to work just fine for now (although the text zooms a bit too much lol).

Note : Your browser will hang for some time after the first visit, I have no idea why, sorry (if you wait it out then it's fine)

EDIT : I added the "flash of new content" issue on the about page.