From 0ba9ef9f97717f37e85b40e12b3d0e9f9371a0a6 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 11:26:36 +0800 Subject: [PATCH 1/7] fix(vSlot): handle non-comment children when extracting slot key --- packages/compiler-vapor/src/transforms/vSlot.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 05aac4aee3c..57bd1c29eb5 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -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 + } } } From 645c5530714abd4df7bdfe0f353037c4fdba9e64 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 11:29:38 +0800 Subject: [PATCH 2/7] fix(component): streamline applyFallthroughProps and getRootElement functions --- packages/runtime-vapor/src/component.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5b18c465104..c2c2cd1f0c2 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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, ): 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 } From 3e03b0d8c50c5e69578459e4733e5768dedaf221 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 16:56:13 +0800 Subject: [PATCH 3/7] wip: save --- .../runtime-vapor/src/components/Teleport.ts | 4 +++ .../src/components/Transition.ts | 26 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ef3d4598c9b..756ff5a6b58 100644 --- a/packages/runtime-vapor/src/components/Teleport.ts +++ b/packages/runtime-vapor/src/components/Teleport.ts @@ -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), diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index a1f420deae5..6834fcf0df1 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -306,23 +306,21 @@ export function findTransitionBlock( // 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) { + if (c instanceof Comment) continue const item = findTransitionBlock(c) - if (item instanceof Element) { - if (__DEV__ && hasFound) { - // warn more than one non-comment child - warn( - ' can only be used on a single element or component. ' + - 'Use for lists.', - ) - break - } - child = item - hasFound = true - if (!__DEV__) break + if (__DEV__ && hasFound) { + // warn more than one non-comment child + warn( + ' can only be used on a single element or component. ' + + 'Use for lists.', + ) + break } + child = item + hasFound = true + if (!__DEV__) break } } else if ((isFrag = isFragment(block))) { if (block.insert) { @@ -344,7 +342,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) From 49ea416de655d613c66ad519a3a45d58475e1962 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 17:42:08 +0800 Subject: [PATCH 4/7] wip: save --- .../src/components/Transition.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index 6834fcf0df1..d39c460a9b0 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -10,6 +10,7 @@ import { baseResolveTransitionHooks, checkTransitionMode, currentInstance, + getComponentName, isTemplateNode, leaveCbKey, queuePostFlushCb, @@ -26,15 +27,17 @@ import { } from '../component' import { extend, isArray } from '@vue/shared' import { renderEffect } from '../renderEffect' -import { isFragment } from '../fragment' +import { type VaporFragment, isFragment } from '../fragment' import { currentHydrationNode, isHydrating, 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 @@ -209,10 +212,14 @@ export function applyTransitionHooks( fallthroughAttrs: boolean = true, ): VaporTransitionHooks { const isFrag = isFragment(block) - const child = findTransitionBlock(block) - if (!child) { + const child = findTransitionBlock( + block, // set transition hooks on fragment for reusing during it's updating - if (isFrag) setTransitionHooksOnFragment(block, hooks) + frag => setTransitionHooksOnFragment(frag, hooks), + isFrag, + ) + if (!child) { + // if (isFrag) setTransitionHooksOnFragment(block, hooks) return hooks } @@ -290,26 +297,30 @@ export function applyTransitionLeaveHooks( const transitionBlockCache = new WeakMap() export function findTransitionBlock( block: Block, + processFragment?: (frag: VaporFragment) => void, inFragment: boolean = false, ): TransitionBlock | undefined { if (transitionBlockCache.has(block)) { 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, processFragment, inFragment) // use component id as key if (child && child.$key === undefined) child.$key = block.uid } else if (isArray(block)) { let hasFound = false for (const c of block) { if (c instanceof Comment) continue - const item = findTransitionBlock(c) + // check if the child is a fragment to suppress warnings + if (isFragment(c)) inFragment = true + const item = findTransitionBlock(c, processFragment, inFragment) if (__DEV__ && hasFound) { // warn more than one non-comment child warn( @@ -322,15 +333,19 @@ export function findTransitionBlock( 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 { - child = findTransitionBlock(block.nodes, true) + processFragment && processFragment(block) + // once we encounter a fragment, we are inside a fragment + child = findTransitionBlock(block.nodes, processFragment, true) } } - if (__DEV__ && !child && !inFragment && !isFrag) { + if (__DEV__ && !child && !inFragment) { warn('Transition component has no valid child element') } From f208b4f16d5438bb2649c405ecca2b9d925ee472 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 21:12:18 +0800 Subject: [PATCH 5/7] test: add test --- .../__tests__/transition.spec.ts | 49 ++++++++++++++++++- .../vapor-e2e-test/transition/App.vue | 16 ++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 0bfc30598cc..2d6b7b8a102 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -905,7 +905,54 @@ describe('vapor transition', () => { describe.todo('transition with KeepAlive', () => {}) 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( + '
vapor compB
', + ) + 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( diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index 4855098243b..a736866da4a 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -11,6 +11,7 @@ import { const show = ref(true) const toggle = ref(true) const count = ref(0) +const hide = ref(false) const timeout = (fn, time) => setTimeout(fn, time) const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50 @@ -481,6 +482,21 @@ function changeViewInOut() { + +
+
+
+ + + + + + +
+ +
+ +
From d47909d148048b7de994c4cf39af9e8db74204d8 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 3 Nov 2025 22:03:51 +0800 Subject: [PATCH 6/7] wip: save --- .../src/components/Transition.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/runtime-vapor/src/components/Transition.ts b/packages/runtime-vapor/src/components/Transition.ts index d39c460a9b0..6b1580a9221 100644 --- a/packages/runtime-vapor/src/components/Transition.ts +++ b/packages/runtime-vapor/src/components/Transition.ts @@ -27,7 +27,7 @@ import { } from '../component' import { extend, isArray } from '@vue/shared' import { renderEffect } from '../renderEffect' -import { type VaporFragment, isFragment } from '../fragment' +import { isFragment } from '../fragment' import { currentHydrationNode, isHydrating, @@ -211,15 +211,21 @@ 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, - // set transition hooks on fragment for reusing during it's updating - frag => setTransitionHooksOnFragment(frag, hooks), - isFrag, - ) + const child = findTransitionBlock(block, isFrag) if (!child) { - // if (isFrag) setTransitionHooksOnFragment(block, hooks) + // set transition hooks on fragment for reusing during it's updating + if (isFrag) setTransitionHooksOnFragment(block, hooks) return hooks } @@ -297,7 +303,6 @@ export function applyTransitionLeaveHooks( const transitionBlockCache = new WeakMap() export function findTransitionBlock( block: Block, - processFragment?: (frag: VaporFragment) => void, inFragment: boolean = false, ): TransitionBlock | undefined { if (transitionBlockCache.has(block)) { @@ -311,7 +316,7 @@ export function findTransitionBlock( } else if (isVaporComponent(block)) { // stop searching if encountering nested Transition component if (getComponentName(block.type) === displayName) return undefined - child = findTransitionBlock(block.block, processFragment, inFragment) + child = findTransitionBlock(block.block, inFragment) // use component id as key if (child && child.$key === undefined) child.$key = block.uid } else if (isArray(block)) { @@ -320,7 +325,7 @@ export function findTransitionBlock( if (c instanceof Comment) continue // check if the child is a fragment to suppress warnings if (isFragment(c)) inFragment = true - const item = findTransitionBlock(c, processFragment, inFragment) + const item = findTransitionBlock(c, inFragment) if (__DEV__ && hasFound) { // warn more than one non-comment child warn( @@ -339,9 +344,7 @@ export function findTransitionBlock( if (block.insert) { child = block } else { - processFragment && processFragment(block) - // once we encounter a fragment, we are inside a fragment - child = findTransitionBlock(block.nodes, processFragment, true) + child = findTransitionBlock(block.nodes, true) } } From d0286f044ac48f9af8fc800100c5a0b5ebc91d7e Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 5 Nov 2025 11:16:03 +0800 Subject: [PATCH 7/7] chore: tweaks --- packages-private/vapor-e2e-test/transition/App.vue | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages-private/vapor-e2e-test/transition/App.vue b/packages-private/vapor-e2e-test/transition/App.vue index a4d65551a3b..8b07a5ac4be 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -12,7 +12,6 @@ import { const show = ref(true) const toggle = ref(true) const count = ref(0) -const hide = ref(false) const timeout = (fn, time) => setTimeout(fn, time) const duration = typeof process !== 'undefined' && process.env.CI ? 200 : 50 @@ -511,11 +510,11 @@ const click = () => { - +
- +