vueuse / motion

๐Ÿคน Vue Composables putting your components in motion
https://motion.vueuse.org
MIT License
2.13k stars 75 forks source link

[Bug Report] Since v2.1.0, animations no longer work when navigating back between routes from the bottom of a page #177

Closed rylanharper closed 4 months ago

rylanharper commented 4 months ago

First of all, let me say thank you for the work/up-keep on this project, its one of my favorite Vue-related libraries and makes working with animations a breeze!

System info

Reproduction

https://stackblitz.com/edit/nuxt-starter-ydptdz?file=README.md

Describe the bug

Since the most recent update, I noticed when working on my portfolio that when I navigate to the bottom of a page and click the Next Project button, the following page's initial visibleOnce animation does not occur, nor does it occur when navigating manually between project routes from the bottom of a page and back. It only occurs when I navigate to a given page (or in my case project) from the top of the router view. This could be a router issue, although I am not sure.

To reliably demonstrate this, I created a the following Slackblitz.

Additional context

This is a pretty common use-case since users most always navigate to the bottom of a given page and then click new link that takes them to the next page.

Not really sure what a quick fix for this is.. Even when using a useMotion composable within an onMounted hook for my project items it does not appear to work when navigating to a new project page from the bottom.

// project DOM refs
const projectItems = ref(null)
const animationReady = ref(false)

// Animation
const animate = (i) => ({
  initial: {
    y: 35,
    opacity: 0
  },
  visibleOnce: {
    y: 0,
    opacity: 1,
    transition: {
      type: 'spring',
      delay: 75 * i
    }
  }
})

// Init animation w/ useMotion
onMounted(() => {
  const items = projectItems.value

  if (items) {
    items.forEach((item, i) => {
      useMotion(item, animate(i))
    })
  }

  animationReady.value = true
})
</script>

Logs

No response

rylanharper commented 4 months ago

Further context on this:

Oddly enough, even when downgrading to v2.0.0 the issue still occurs. I have to set my page/layout transitions to false in my nuxt.config for everything to back to working correctly (when using v2.0.0:

app: {
  pageTransition: false,
  layoutTransition: false
}

With v2.1.0, even when page/layout transitions are false, the issue persists.

Haha I feel like this may be an odd nextTick situation..

BobbieGoede commented 4 months ago

Thanks for the complete bug report!

It looks like the issue here is that the element is moved outside of the viewport (initial y is bigger than the element itself, at the bottom of the page) which makes the page taller, but when it's visible the page is shorter as the element is moved back to y: 0. When you navigate back to the page the scroll position is at the edge of the shorter page.

Basically the scroll position after navigation is set to what would have been the bottom when the element is in the visible variant state.

I think this is kind of an edge case but it would be nice if there was a good way to make it work as expected, open to ideas ๐Ÿ˜…

rylanharper commented 4 months ago

@BobbieGoede Ah, got it! Really appreciate the feedback!

Hmm would somehow incorporating a nextTick somewhere work? I had the idea of trying to do something like this with the useMotion composable and now fixes the issue with v2.1.0, but it isn't too pretty:

// Project DOM refs
const projectItems = ref(null)
const animationReady = ref(false)

// Animation
const animate = (i) => ({
  initial: {
    y: 35,
    opacity: 0
  },
  enter: {
    y: 0,
    opacity: 1,
    transition: {
      type: 'spring',
      delay: 75 * i
    }
  }
})

// Init animation w/ useMotion
onMounted(() => {
  const items = projectItems.value

  nextTick(() => {
    if (items) {
      items.forEach((item, i) => {
        useMotion(item, animate(i))
      })
    }
  })

  animationReady.value = true
})

However, if a Nuxt app has a page/layout transition, this solution will not work.. (not sure why this is yet)

BobbieGoede commented 4 months ago

Now that I look at the issue in your portfolio I'm not sure if it's the same as your reproduction. I changed your reproduction to demonstrate here, I added some padding so that the invisible element will be inside the viewport after navigation.

In your portfolio's case the element should be in the viewport already, so I think there is something else going on ๐Ÿค”

rylanharper commented 4 months ago

@BobbieGoede Hmm you're right! It seems to be another issue..

The solution I just added with the nextTick seems to fix the issue when I navigate between pages from the bottom of the page. I just pushed the changes to live, although now I cant use any page/layout fade transitions haha (which is somewhat unfortunate).

Although whats interesting is that if I am in the middle of a project page (somewhere halfway down) and navigate back to the home page, then navigate back to the project page manually (with back button), the animations will also not appear in. Its also as if the IntersectionObserver gets lost within the router to-from and doesn't fire off the initial animations. This also why I have to page/layout transitions set to false. I say IntersectionObserver because I have to scroll until the elements are in the viewport to get them to animate.

There are also no issues/errors with my elements rendering in the dom. All the elements are there, they just don't shoot off their initial vue-motion animation with navigating back between routes here for some reason.

This does seem to be a pretty edge case though.

rylanharper commented 4 months ago

So after spending a few hours debugging tonight, this is indeed an IntersectionObserver related issue within the context of the module. If add an IntersectionObserver to the onMounted hook with the useMotion composable I linked above, all animation issues are fixed when navigating between routes (animations start no matter where the router router enters/leaves from) and I can use page/layout transitions normally. However the animations themselves are slightly janky now because there are two observers going on at once, so this is not a fix, but rather pointing to a likely culprit.

Note that non-composable animations still do not work in certain router to-from scenarios such as inline v-motion-fade-visible-once, etc.

BobbieGoede commented 4 months ago

Hmm, if you can make a reproduction of this issue I'll give it a closer look.

Also, is the issue still present if you use the directive approach (<div v-motion :initial="..." :visible-once="...">)? I suppose this may be the same as v-motion-fade-visible-once but I know that this registers the intersection observer during mount (see https://github.com/BobbieGoede/motion/blob/900069f0ae40eb445fa2bc919d0cde0efb9ba5bb/src/directive/index.ts#L42-L53).

rylanharper commented 4 months ago

Yeah for sure! I'll see if I can and see if I can reproduce it in a StackBlitz. Hopefully either tonight or early next week.

rylanharper commented 4 months ago

Hey @BobbieGoede! I took time this past weekend to try and reproduce this issue..

I think you're right that this is an edge case for the most part. I am like 95% certain this animation issue stems from using a component :is template structure within my project pages. I currently use this to manage my various page sections which are controlled by the backend CMS I am using.

I found that if I mount a page section component after initial page load with nextTick all issues are fixed that I described above (animations not starting between routes, etc.). Its something simple like this:

// JS
const animate = (i) => ({
  initial: {
    y: 32,
    opacity: 0
  },
  visibleOnce: {
    y: 0,
    opacity: 1,
    transition: {
      type: 'spring',
      delay: 75 * i
    }
  }
})

const readyToAnimate = ref(false)

onMounted(() => {
  nextTick(() => {
    readyToAnimate.value = true
  })
})

// HTML
<section class="project-media-grid" v-if="readyToAnimate">
  // ...
  <div
    v-for="(media, i) in section.assets"
    :key="media._key"
    v-motion="animate(i)"
  >
    // ...
  </div>
</section>

I pushed the changes to my portfolio and everything works! Sorry to cause some wasted time looking into this issue, but I do really appreciate the help ๐Ÿ™