diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index a15d18d56bf..345bf2ac10e 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -27,7 +27,7 @@ import { warnDeprecation, } from './compat/compatConfig' import { shallowReadonly } from '@vue/reactivity' -import { setTransitionHooks } from './components/BaseTransition' +import { getInnerChild, setTransitionHooks } from './components/BaseTransition' /** * dev only flag to track whether $attrs was used during render. @@ -248,13 +248,14 @@ export function renderComponentRoot( } // inherit transition data if (vnode.transition) { - if (__DEV__ && !isElementRoot(root)) { + const child = getInnerChild(root) || root + if (__DEV__ && !isElementRoot(child)) { warn( `Component inside renders non-element root node ` + `that cannot be animated.`, ) } - setTransitionHooks(root, vnode.transition) + setTransitionHooks(child, vnode.transition) } if (__DEV__ && setRoot) { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 6ce06d28239..511454be9c5 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -484,7 +484,7 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined { } } -function getInnerChild(vnode: VNode): VNode | undefined { +export function getInnerChild(vnode: VNode): VNode | undefined { if (!isKeepAlive(vnode)) { if (isTeleport(vnode.type) && vnode.children) { return findNonCommentChild(vnode.children as VNode[]) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index c0863a75991..ad5ca985e46 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -2311,6 +2311,87 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #11910 + test( + 'apply transition to teleport component child', + async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + template: ` +
+
+ + + +
+ + `, + components: { + Comp: { + template: ` + +
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', () => {