Browse Source

fix(transition): handle rapid in-out transition toggles (#14540)

close #14539
edison 1 month ago
parent
commit
452290ddaa

+ 47 - 0
packages-private/vapor-e2e-test/__tests__/transition.spec.ts

@@ -1823,6 +1823,53 @@ describe('vapor transition', () => {
       E2E_TIMEOUT,
     )
 
+    // #14539
+    test(
+      'in-out mode with if/else rapid toggles',
+      async () => {
+        const btnSelector = '.in-out-if-else-rapid > button'
+        const containerSelector = '.in-out-if-else-rapid > div'
+
+        expect(await html(containerSelector)).toBe(`<div>2</div>`)
+
+        expect(
+          (await transitionStart(btnSelector, containerSelector)).innerHTML,
+        ).toBe(`<div>2</div><div class="v-enter-from v-enter-active">1</div>`)
+
+        await nextTick()
+        await click(btnSelector)
+        await waitForInnerHTML(
+          containerSelector,
+          `<div class="v-enter-active v-enter-to">1</div><div class="v-enter-from v-enter-active">2</div>`,
+        )
+
+        await click(btnSelector)
+        await waitForInnerHTML(
+          containerSelector,
+          `<div class="v-enter-from v-enter-active">2</div><div class="v-enter-from v-enter-active">1</div>`,
+        )
+
+        await waitForInnerHTML(
+          containerSelector,
+          `<div class="v-enter-active v-enter-to">2</div><div class="v-enter-active v-enter-to">1</div>`,
+        )
+        await waitForInnerHTML(containerSelector, `<div class="">1</div>`)
+
+        await click(btnSelector)
+        await waitForInnerHTML(
+          containerSelector,
+          `<div class="">1</div><div class="v-enter-from v-enter-active">2</div>`,
+        )
+        await waitForInnerHTML(
+          containerSelector,
+          `<div class="">1</div><div class="v-enter-active v-enter-to">2</div>`,
+        )
+
+        await waitForInnerHTML(containerSelector, `<div class="">2</div>`)
+      },
+      E2E_TIMEOUT,
+    )
+
     test(
       'out-in mode with if/else-if',
       async () => {

+ 21 - 0
packages-private/vapor-e2e-test/transition/cases/mode/in-out-mode-with-if-else-rapid-toggles.vue

@@ -0,0 +1,21 @@
+<script setup vapor>
+import { ref } from 'vue'
+
+const view = ref(0)
+
+function changeView() {
+  view.value = view.value === 1 ? 0 : 1
+}
+</script>
+
+<template>
+  <div class="in-out-if-else-rapid">
+    <button @click="changeView">changeView</button>
+    <div>
+      <Transition mode="in-out">
+        <div v-if="view">1</div>
+        <div v-else>2</div>
+      </Transition>
+    </div>
+  </div>
+</template>

+ 20 - 7
packages/runtime-vapor/src/components/Transition.ts

@@ -265,19 +265,32 @@ function applyTransitionLeaveHooksImpl(
       earlyRemove,
       delayedLeave,
     ) => {
-      state.leavingNodes.set(String(leavingBlock.$key), leavingBlock)
+      const leavingKey = String(leavingBlock.$key)
+      state.leavingNodes.set(leavingKey, leavingBlock)
+      // Bind cleanup to this specific handoff so an older leave callback
+      // cannot clear a newer delayedLeave during rapid toggles.
+      const delayedLeaveCb = () => {
+        delayedLeave()
+        leavingBlock.$transition = undefined
+        if (enterHooks.delayedLeave === delayedLeaveCb) {
+          delete enterHooks.delayedLeave
+        }
+      }
       // early removal callback
       block[leaveCbKey] = () => {
         earlyRemove()
         block[leaveCbKey] = undefined
         leavingBlock.$transition = undefined
-        delete enterHooks.delayedLeave
-      }
-      enterHooks.delayedLeave = () => {
-        delayedLeave()
-        leavingBlock.$transition = undefined
-        delete enterHooks.delayedLeave
+        // Same-key in-out switches early-remove the previous leaving block.
+        // Clear the cache entry so the next enter isn't skipped as "still leaving".
+        if (state.leavingNodes.get(leavingKey) === leavingBlock) {
+          state.leavingNodes.delete(leavingKey)
+        }
+        if (enterHooks.delayedLeave === delayedLeaveCb) {
+          delete enterHooks.delayedLeave
+        }
       }
+      enterHooks.delayedLeave = delayedLeaveCb
     }
   }
 }