]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(transition): support directly nesting Teleport inside Transition (#6548)
authoredison <daiwei521@126.com>
Thu, 25 Apr 2024 07:19:53 +0000 (15:19 +0800)
committerGitHub <noreply@github.com>
Thu, 25 Apr 2024 07:19:53 +0000 (15:19 +0800)
close #5836

packages/runtime-core/src/components/BaseTransition.ts
packages/vue/__tests__/e2e/Transition.spec.ts

index f54a657025d5c7bca69998909c0b68287c602e38..38fa44551bd3d9f611240c9ccccacdddaf0ac15a 100644 (file)
@@ -18,6 +18,7 @@ import { toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
 import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared'
 import { onBeforeUnmount, onMounted } from '../apiLifecycle'
+import { isTeleport } from './Teleport'
 import type { RendererElement } from '../renderer'
 import { SchedulerJobFlags } from '../scheduler'
 
@@ -152,27 +153,7 @@ const BaseTransitionImpl: ComponentOptions = {
         return
       }
 
-      let child: VNode = children[0]
-      if (children.length > 1) {
-        let hasFound = false
-        // locate first non-comment child
-        for (const c of children) {
-          if (c.type !== Comment) {
-            if (__DEV__ && hasFound) {
-              // warn more than one non-comment child
-              warn(
-                '<transition> can only be used on a single element or component. ' +
-                  'Use <transition-group> for lists.',
-              )
-              break
-            }
-            child = c
-            hasFound = true
-            if (!__DEV__) break
-          }
-        }
-      }
-
+      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)
@@ -194,7 +175,7 @@ const BaseTransitionImpl: ComponentOptions = {
 
       // in the case of <transition><keep-alive/></transition>, we need to
       // compare the type of the kept-alive children.
-      const innerChild = getKeepAliveChild(child)
+      const innerChild = getInnerChild(child)
       if (!innerChild) {
         return emptyPlaceholder(child)
       }
@@ -208,7 +189,7 @@ const BaseTransitionImpl: ComponentOptions = {
       setTransitionHooks(innerChild, enterHooks)
 
       const oldChild = instance.subTree
-      const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
+      const oldInnerChild = oldChild && getInnerChild(oldChild)
 
       // handle mode
       if (
@@ -268,6 +249,30 @@ if (__COMPAT__) {
   BaseTransitionImpl.__isBuiltIn = true
 }
 
+function findNonCommentChild(children: VNode[]): VNode {
+  let child: VNode = children[0]
+  if (children.length > 1) {
+    let hasFound = false
+    // locate first non-comment child
+    for (const c of children) {
+      if (c.type !== Comment) {
+        if (__DEV__ && hasFound) {
+          // warn more than one non-comment child
+          warn(
+            '<transition> can only be used on a single element or component. ' +
+              'Use <transition-group> for lists.',
+          )
+          break
+        }
+        child = c
+        hasFound = true
+        if (!__DEV__) break
+      }
+    }
+  }
+  return child
+}
+
 // export the public type for h/tsx inference
 // also to avoid inline import() in generated d.ts files
 export const BaseTransition = BaseTransitionImpl as unknown as {
@@ -458,8 +463,12 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
   }
 }
 
-function getKeepAliveChild(vnode: VNode): VNode | undefined {
+function getInnerChild(vnode: VNode): VNode | undefined {
   if (!isKeepAlive(vnode)) {
+    if (isTeleport(vnode.type) && vnode.children) {
+      return findNonCommentChild(vnode.children as VNode[])
+    }
+
     return vnode
   }
   // #7121 ensure get the child component subtree in case
index e8d6d1e049ec1def817fa4a8e908daba4e6fcb41..d52af21ce6adfde17da6958debb7facb430d8e2d 100644 (file)
@@ -1725,6 +1725,95 @@ describe('e2e: Transition', () => {
     )
   })
 
+  describe('transition with Teleport', () => {
+    test(
+      'apply transition to teleport child',
+      async () => {
+        await page().evaluate(() => {
+          const { createApp, ref, h } = (window as any).Vue
+          createApp({
+            template: `
+            <div id="target"></div>
+            <div id="container">
+              <transition>
+                <Teleport to="#target">
+                  <!-- comment -->
+                  <Comp v-if="toggle" class="test">content</Comp>
+                </Teleport>
+              </transition>
+            </div>
+            <button id="toggleBtn" @click="click">button</button>
+          `,
+            components: {
+              Comp: {
+                setup() {
+                  return () => h('div', { class: 'test' }, 'content')
+                },
+              },
+            },
+            setup: () => {
+              const toggle = ref(false)
+              const click = () => (toggle.value = !toggle.value)
+              return { toggle, click }
+            },
+          }).mount('#app')
+        })
+
+        expect(await html('#target')).toBe('<!-- comment --><!--v-if-->')
+        expect(await html('#container')).toBe(
+          '<!--teleport start--><!--teleport end-->',
+        )
+
+        const classWhenTransitionStart = () =>
+          page().evaluate(() => {
+            ;(document.querySelector('#toggleBtn') as any)!.click()
+            return Promise.resolve().then(() => {
+              // find the class of teleported node
+              return document
+                .querySelector('#target div')!
+                .className.split(/\s+/g)
+            })
+          })
+
+        // enter
+        expect(await classWhenTransitionStart()).toStrictEqual([
+          'test',
+          'v-enter-from',
+          'v-enter-active',
+        ])
+        await nextFrame()
+        expect(await classList('.test')).toStrictEqual([
+          'test',
+          'v-enter-active',
+          'v-enter-to',
+        ])
+        await transitionFinish()
+        expect(await html('#target')).toBe(
+          '<!-- comment --><div class="test">content</div>',
+        )
+
+        // leave
+        expect(await classWhenTransitionStart()).toStrictEqual([
+          'test',
+          'v-leave-from',
+          'v-leave-active',
+        ])
+        await nextFrame()
+        expect(await classList('.test')).toStrictEqual([
+          'test',
+          'v-leave-active',
+          'v-leave-to',
+        ])
+        await transitionFinish()
+        expect(await html('#target')).toBe('<!-- comment --><!--v-if-->')
+        expect(await html('#container')).toBe(
+          '<!--teleport start--><!--teleport end-->',
+        )
+      },
+      E2E_TIMEOUT,
+    )
+  })
+
   describe('transition with v-show', () => {
     test(
       'named transition with v-show',