]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): vapor transition work with vapor teleport (#14047)
authoredison <daiwei521@126.com>
Wed, 5 Nov 2025 03:31:13 +0000 (11:31 +0800)
committerGitHub <noreply@github.com>
Wed, 5 Nov 2025 03:31:13 +0000 (11:31 +0800)
packages-private/vapor-e2e-test/__tests__/transition.spec.ts
packages-private/vapor-e2e-test/transition/App.vue
packages/compiler-vapor/src/transforms/vSlot.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/Teleport.ts
packages/runtime-vapor/src/components/Transition.ts

index 0babdd8b8dc0da939c5359348319ed5d4d4ba546..180b0157aaa0aed6a2d349d9133cb3b8ef961b6a 100644 (file)
@@ -923,7 +923,54 @@ describe('vapor transition', () => {
   })
 
   describe.todo('transition with Suspense', () => {})
-  describe.todo('transition with Teleport', () => {})
+
+  describe('transition with Teleport', () => {
+    test(
+      'apply transition to teleport child',
+      async () => {
+        const btnSelector = '.with-teleport > button'
+        const containerSelector = '.with-teleport > .container'
+        const targetSelector = `.with-teleport > .target`
+
+        await transitionFinish()
+        expect(await html(containerSelector)).toBe('')
+        expect(await html(targetSelector)).toBe('')
+
+        // enter
+        expect(
+          (await transitionStart(btnSelector, `${targetSelector} div`))
+            .classNames,
+        ).toStrictEqual(['test', 'v-enter-from', 'v-enter-active'])
+        await nextFrame()
+        expect(await classList(`${targetSelector} div`)).toStrictEqual([
+          'test',
+          'v-enter-active',
+          'v-enter-to',
+        ])
+        await transitionFinish()
+        expect(await html(targetSelector)).toBe(
+          '<div class="test">vapor compB</div>',
+        )
+        expect(await html(containerSelector)).toBe('')
+
+        // leave
+        expect(
+          (await transitionStart(btnSelector, `${targetSelector} div`))
+            .classNames,
+        ).toStrictEqual(['test', 'v-leave-from', 'v-leave-active'])
+        await nextFrame()
+        expect(await classList(`${targetSelector} div`)).toStrictEqual([
+          'test',
+          'v-leave-active',
+          'v-leave-to',
+        ])
+        await transitionFinish()
+        expect(await html(targetSelector)).toBe('')
+        expect(await html(containerSelector)).toBe('')
+      },
+      E2E_TIMEOUT,
+    )
+  })
 
   describe('transition with v-show', () => {
     test(
index e437e07456d3fe5be05c710bf5cc1bade029ee4b..8b07a5ac4be4da909138060cb30adf310ebfec5b 100644 (file)
@@ -503,6 +503,21 @@ const click = () => {
     </div>
     <!-- mode end -->
 
+    <!-- with teleport -->
+    <div class="with-teleport">
+      <div class="target"></div>
+      <div class="container">
+        <Transition>
+          <Teleport to=".target" defer>
+            <!-- comment -->
+            <VaporCompB v-if="!toggle" class="test"></VaporCompB>
+          </Teleport>
+        </Transition>
+      </div>
+      <button @click="toggle = !toggle">button</button>
+    </div>
+    <!-- with teleport end -->
+
     <!-- with keep-alive -->
     <div class="keep-alive">
       <div>
index 05aac4aee3c50cdfb86e693356d81d9f9bfa56eb..57bd1c29eb524c730be73722dcd249c14df1607b 100644 (file)
@@ -90,12 +90,17 @@ function transformComponentSlot(
 
   let slotKey
   if (isTransitionNode(node) && nonSlotTemplateChildren.length) {
-    const keyProp = findProp(
-      nonSlotTemplateChildren[0] as ElementNode,
-      'key',
-    ) as VaporDirectiveNode
-    if (keyProp) {
-      slotKey = keyProp.exp
+    const nonCommentChild = nonSlotTemplateChildren.find(
+      n => n.type !== NodeTypes.COMMENT,
+    )
+    if (nonCommentChild) {
+      const keyProp = findProp(
+        nonCommentChild as ElementNode,
+        'key',
+      ) as VaporDirectiveNode
+      if (keyProp) {
+        slotKey = keyProp.exp
+      }
     }
   }
 
index 5b18c46510435970ddc77d6c477c7dacee0e4147..c2c2cd1f0c2f59f3fe4630a2319c064d448a35e0 100644 (file)
@@ -381,10 +381,7 @@ export function setupComponent(
     component.inheritAttrs !== false &&
     Object.keys(instance.attrs).length
   ) {
-    const el = getRootElement(instance)
-    if (el) {
-      renderEffect(() => applyFallthroughProps(el, instance.attrs))
-    }
+    renderEffect(() => applyFallthroughProps(instance.block, instance.attrs))
   }
 
   setActiveSub(prevSub)
@@ -402,9 +399,12 @@ export function applyFallthroughProps(
   block: Block,
   attrs: Record<string, any>,
 ): void {
-  isApplyingFallthroughProps = true
-  setDynamicProps(block as Element, [attrs])
-  isApplyingFallthroughProps = false
+  const el = getRootElement(block)
+  if (el) {
+    isApplyingFallthroughProps = true
+    setDynamicProps(el, [attrs])
+    isApplyingFallthroughProps = false
+  }
 }
 
 /**
@@ -761,9 +761,7 @@ export function getExposed(
   }
 }
 
-function getRootElement({
-  block,
-}: VaporComponentInstance): Element | undefined {
+function getRootElement(block: Block): Element | undefined {
   if (block instanceof Element) {
     return block
   }
index ef3d4598c9be7f956b11fc3094fbb716f14e11c6..756ff5a6b58cd0b4c56509cb3c3b09cbbe1ab9d7 100644 (file)
@@ -29,6 +29,7 @@ import {
   runWithoutHydration,
   setCurrentHydrationNode,
 } from '../dom/hydration'
+import { applyTransitionHooks } from './Transition'
 
 export const VaporTeleportImpl = {
   name: 'VaporTeleport',
@@ -122,6 +123,9 @@ export class TeleportFragment extends VaporFragment {
     if (!this.parent || isHydrating) return
 
     const mount = (parent: ParentNode, anchor: Node | null) => {
+      if (this.$transition) {
+        applyTransitionHooks(this.nodes, this.$transition)
+      }
       insert(
         this.nodes,
         (this.mountContainer = parent),
index a1f420deae58ed828c5f5e31188a544e337bb250..6b1580a9221b81a8b46cb2533df25b4de41b5dfe 100644 (file)
@@ -10,6 +10,7 @@ import {
   baseResolveTransitionHooks,
   checkTransitionMode,
   currentInstance,
+  getComponentName,
   isTemplateNode,
   leaveCbKey,
   queuePostFlushCb,
@@ -33,8 +34,10 @@ import {
   setCurrentHydrationNode,
 } from '../dom/hydration'
 
+const displayName = 'VaporTransition'
+
 const decorate = (t: typeof VaporTransition) => {
-  t.displayName = 'VaporTransition'
+  t.displayName = displayName
   t.props = TransitionPropsValidators
   t.__vapor = true
   return t
@@ -208,8 +211,18 @@ export function applyTransitionHooks(
   hooks: VaporTransitionHooks,
   fallthroughAttrs: boolean = true,
 ): VaporTransitionHooks {
+  // filter out comment nodes
+  if (isArray(block)) {
+    block = block.filter(b => !(b instanceof Comment))
+    if (block.length === 1) {
+      block = block[0]
+    } else if (block.length === 0) {
+      return hooks
+    }
+  }
+
   const isFrag = isFragment(block)
-  const child = findTransitionBlock(block)
+  const child = findTransitionBlock(block, isFrag)
   if (!child) {
     // set transition hooks on fragment for reusing during it's updating
     if (isFrag) setTransitionHooksOnFragment(block, hooks)
@@ -296,35 +309,38 @@ export function findTransitionBlock(
     return transitionBlockCache.get(block)
   }
 
-  let isFrag = false
   let child: TransitionBlock | undefined
   if (block instanceof Node) {
     // transition can only be applied on Element child
     if (block instanceof Element) child = block
   } else if (isVaporComponent(block)) {
-    child = findTransitionBlock(block.block)
+    // stop searching if encountering nested Transition component
+    if (getComponentName(block.type) === displayName) return undefined
+    child = findTransitionBlock(block.block, inFragment)
     // use component id as key
     if (child && child.$key === undefined) child.$key = block.uid
   } else if (isArray(block)) {
-    child = block[0] as TransitionBlock
     let hasFound = false
     for (const c of block) {
-      const item = findTransitionBlock(c)
-      if (item instanceof Element) {
-        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 = item
-        hasFound = true
-        if (!__DEV__) break
+      if (c instanceof Comment) continue
+      // check if the child is a fragment to suppress warnings
+      if (isFragment(c)) inFragment = true
+      const item = findTransitionBlock(c, inFragment)
+      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 = item
+      hasFound = true
+      if (!__DEV__) break
     }
-  } else if ((isFrag = isFragment(block))) {
+  } else if (isFragment(block)) {
+    // mark as in fragment to suppress warnings
+    inFragment = true
     if (block.insert) {
       child = block
     } else {
@@ -332,7 +348,7 @@ export function findTransitionBlock(
     }
   }
 
-  if (__DEV__ && !child && !inFragment && !isFrag) {
+  if (__DEV__ && !child && !inFragment) {
     warn('Transition component has no valid child element')
   }
 
@@ -344,7 +360,7 @@ export function setTransitionHooksOnFragment(
   hooks: VaporTransitionHooks,
 ): void {
   if (isFragment(block)) {
-    setTransitionHooks(block, hooks)
+    block.$transition = hooks
   } else if (isArray(block)) {
     for (let i = 0; i < block.length; i++) {
       setTransitionHooksOnFragment(block[i], hooks)