diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index f54a657025d..38fa44551bd 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -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( - ' can only be used on a single element or component. ' + - 'Use 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 , 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( + ' can only be used on a single element or component. ' + + 'Use 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 diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index e8d6d1e049e..d52af21ce6a 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -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: ` +
+
+ + + + content + + +
+ + `, + 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('') + expect(await html('#container')).toBe( + '', + ) + + 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( + '
content
', + ) + + // 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('') + expect(await html('#container')).toBe( + '', + ) + }, + E2E_TIMEOUT, + ) + }) + describe('transition with v-show', () => { test( 'named transition with v-show',