From d14c5a2322c555a64483dba44d99c45a534026a5 Mon Sep 17 00:00:00 2001 From: edison Date: Wed, 5 Nov 2025 11:31:13 +0800 Subject: [PATCH] feat(runtime-vapor): vapor transition work with vapor teleport (#14047) --- .../__tests__/transition.spec.ts | 49 +++++++++++++++- .../vapor-e2e-test/transition/App.vue | 15 +++++ .../compiler-vapor/src/transforms/vSlot.ts | 17 ++++-- packages/runtime-vapor/src/component.ts | 18 +++--- .../runtime-vapor/src/components/Teleport.ts | 4 ++ .../src/components/Transition.ts | 58 ++++++++++++------- 6 files changed, 123 insertions(+), 38 deletions(-) diff --git a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts index 0babdd8b8d..180b0157aa 100644 --- a/packages-private/vapor-e2e-test/__tests__/transition.spec.ts +++ b/packages-private/vapor-e2e-test/__tests__/transition.spec.ts @@ -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( + '
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 e437e07456..8b07a5ac4b 100644 --- a/packages-private/vapor-e2e-test/transition/App.vue +++ b/packages-private/vapor-e2e-test/transition/App.vue @@ -503,6 +503,21 @@ const click = () => { + +
+
+
+ + + + + + +
+ +
+ +
diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 05aac4aee3..57bd1c29eb 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 + } } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 5b18c46510..c2c2cd1f0c 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 } diff --git a/packages/runtime-vapor/src/components/Teleport.ts b/packages/runtime-vapor/src/components/Teleport.ts index ef3d4598c9..756ff5a6b5 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 a1f420deae..6b1580a922 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, @@ -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( - ' can only be used on a single element or component. ' + - 'Use 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( + ' 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))) { + } 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) -- 2.47.3