Jelajahi Sumber

fix(transition): preserve placeholder for conditional explicit default slots (#14748)

close #14727
edison 1 hari lalu
induk
melakukan
45990cecf4

+ 10 - 2
packages/runtime-core/src/components/BaseTransition.ts

@@ -10,6 +10,7 @@ import {
   type VNode,
   type VNodeArrayChildren,
   cloneVNode,
+  createCommentVNode,
   isSameVNodeType,
 } from '../vnode'
 import { warn } from '../warning'
@@ -155,11 +156,18 @@ const BaseTransitionImpl: ComponentOptions = {
     return () => {
       const children =
         slots.default && getTransitionRawChildren(slots.default(), true)
-      if (!children || !children.length) {
+      const child =
+        children && children.length
+          ? findNonCommentChild(children)
+          : // Keep explicit default-slot conditionals on the same transition path
+            // as regular v-if branches, which render a comment placeholder.
+            instance.subTree
+            ? createCommentVNode()
+            : undefined
+      if (!child) {
         return
       }
 
-      const child: VNode = findNonCommentChild(children)
       // there's no need to track reactivity for these props so use the raw
       // props for a bit better perf
       const rawProps = toRaw(props)

+ 51 - 0
packages/vue/__tests__/e2e/Transition.spec.ts

@@ -1428,6 +1428,57 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+
+    // #14727
+    test(
+      'explicit default slot template can toggle again before leave finishes',
+      async () => {
+        const spy = vi.fn()
+        const currentPage = page()
+        currentPage.on('pageerror', spy)
+
+        await page().evaluate(() => {
+          const { createApp, ref } = (window as any).Vue
+          createApp({
+            template: `
+              <div id="container">
+                <transition name="test">
+                  <template v-if="show" #>
+                    <div class="test">text</div>
+                  </template>
+                </transition>
+              </div>
+              <button id="toggleBtn" @click="show = !show">button</button>
+            `,
+            setup: () => {
+              const show = ref(true)
+              return { show }
+            },
+          }).mount('#app')
+        })
+
+        expect(await html('#container')).toBe('<div class="test">text</div>')
+
+        await click('#toggleBtn')
+        await nextTick()
+        await click('#toggleBtn')
+
+        expect(
+          await page().$$eval('#container .test', nodes =>
+            nodes.map(node => node.className),
+          ),
+        ).toStrictEqual(['test test-enter-from test-enter-active'])
+
+        await nextFrame()
+        await transitionFinish()
+        await nextFrame()
+
+        expect(spy).not.toHaveBeenCalled()
+        currentPage.off('pageerror', spy)
+        expect(await html('#container')).toBe('<div class="test">text</div>')
+      },
+      E2E_TIMEOUT,
+    )
   })
 
   describe('transition with KeepAlive', () => {