TeemuK
TeemuK

Reputation: 2559

Svelte list crossfade randomly misplaces elements

I have a list which I want to animate so that when an item is selected, it floats to top and the rest fly out. On reselecting the element it floats back to its place and others fly back in.

Using this crossfade demo I was able to produce a pretty solid version:

REPL

However, I noticed that the elements sometimes aren't returned to their original position and only a subsequent re-animating might fix it. I am pretty sure this is because my crossfade function needs more sophisticated handling (or that I should use just regular transition instead?) but I am unsure how exactly.

<script>
  import { flip } from 'svelte/animate'
  import { writable } from 'svelte/store'
  import { crossfade, fly } from 'svelte/transition'

  const ACTIONS = [
    { key: 'blue' },
    { key: 'green' },
    { key: 'yellow' },
    { key: 'purple' },
    { key: 'orange' },
    { key: 'red' },
  ]
  const selectedAction = writable(null)
  let shownActions = ACTIONS.map(a => a)

  const [send, receive] = crossfade({
    fallback(node, _params) {
      return fly(node, { x: -800, duration: 600 })
    }
  })

  function selectAction(action) {
    if ($selectedAction === action) {
      shownActions = ACTIONS.map(a => a)
      selectedAction.update(v => {
        if (v === action || action === undefined) {
          return null
        } else {
          return action
        }
      })
    } else {
      shownActions = ACTIONS.filter(a => a.key === action)
      selectedAction.update(v => {
        if (v === action || action === undefined) {
          return null
        } else {
          return action
        }
      })
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      })
    }
  }
</script>

<ul>
{#each shownActions as action (action)}
  <li
    class={`${$$props.class || ''}`}
    animate:flip={{ duration: 600 }}
    in:receive={{ key: action.key }}
    out:send={{ key: action.key }}
  >
    <button on:click={e => selectAction(action.key)}>{action.key}</button>
  </li>
{/each}
</ul>

<style lang="scss">
    ul {
        list-style: none;
    }
    li {
        display: flex;
        margin-bottom: 1rem;
    }
    button {
        margin-right: 1rem;
        padding: 0.5rem 1rem;
    }
</style>

EDIT: So in the end I discarded the list out animation, leaving just the sliding effect for the individual item. It just feels less noisy that way. Now that I upgraded my svelte version, it seems the animations themselves feel less buggier — there were some problems with the animated elements not being reset properly, making them appear squished.

Animations for both in/out for all items:

{#each shownActions as action (action)}
  <li
    class={`${$$props.class || ''}`}
    animate:flip={{ duration: 500 }}
    in:fly={{ delay: 0, duration: 500 }}
  >
    <div
      class={`${$$props.class || ''}`}
      in:receive={{ key: action.key }}
      out:send={{ key: action.key }}
    >
      <button on:click={e => selectAction(action.key)}>{action.key}</button>
    </div>
  </li>
{/each}

No fly-out animation for the other list items, my preferred choice:

{#each shownActions as action (action)}
  <li
    class={`${$$props.class || ''}`}
    animate:flip={{ duration: 500 }}
    in:receive={{ key: action.key }}
  >
    <button on:click={e => selectAction(action.key)}>{action.key}</button>
  </li>
{/each}

Upvotes: 1

Views: 77

Answers (2)

Corrl
Corrl

Reputation: 7741

Looks like this can be solved by

  • seperating the animate:flip and the transitions on seperate elements. (Since the elements just vanish and don't move to a different 'location/container' the crossfade isn't needed)
  • Making the <ul> a flex element to prevent 'collapsing' of the stack (visible with a longer duration and clicking on a lower element / related question)
  • blocking action while transition is playing

REPL

<script>
    import { flip } from 'svelte/animate'
    import { writable } from 'svelte/store'
    import { crossfade, fly } from 'svelte/transition'

    const ACTIONS = [
        { key: 'blue' },
        { key: 'green' },
        { key: 'yellow' },
        { key: 'purple' },
        { key: 'orange' },
        { key: 'red' },
    ]

    let animating = false

    const selectedAction = writable(null)
    let shownActions = ACTIONS.map(a => a)

    function selectAction(action) {
        if(animating === true) return

        if ($selectedAction === action) {
            shownActions = ACTIONS.map(a => a)
            selectedAction.update(v => {
                if (v === action || action === undefined) {
                    return null
                } else {
                    return action
                }
            })
        } else {
            shownActions = ACTIONS.filter(a => a.key === action)
            selectedAction.update(v => {
                if (v === action || action === undefined) {
                    return null
                } else {
                    return action
                }
            })
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            })
        }
    }
</script>

<ul>
    {#each shownActions as action (action)}
        <li
            class={`${$$props.class || ''}`}
            animate:flip={{ duration: 600 }}
            >
            <div
                in:fly={{ x: -800, duration: 600 }}
                out:fly={{ x: -800, duration: 600 }}
                on:introstart={() => animating = true}
                on:outrostart={() => animating = true}
                on:introend={() => animating = false}
                on:outroend={() => animating = false}
                >
                <button on:click={e => selectAction(action.key)}>{action.key}</button>
            </div>
        </li>
    {/each}
</ul>

<style lang="scss">
    ul {
        display: flex;
        flex-direction: column;
        list-style: none;
    }
    li {
        display: flex;
        margin-bottom: 1rem;
    }
    button {
        margin-right: 1rem;
        padding: 0.5rem 1rem;
    }
</style>

Upvotes: 1

Brewal
Brewal

Reputation: 8199

It looks like the issue pops when you are clicking before the animation is over. You could wait for the animation to end using transition events. But there's also an issue with the position of the elements during the transition. I had better results when using a child div inside li elements and a slide animation on li :

{#each shownActions as action (action)}
  <li
        in:slide={({deylay: 0, duration: 300})}
        out:slide={({deylay: 600, duration: 300})}
  >
        <div
            class={`${$$props.class || ''}`}
            in:receive={{ key: action.key }}
            out:send={{ key: action.key }}
        >
            <button on:click={e => selectAction(action.key)}>{action.key}</button>
        </div>
  </li>
{/each}

demo

Upvotes: 1

Related Questions