From 000c04f5287c5c24d3bb62402e3a588b456ae46e Mon Sep 17 00:00:00 2001 From: yangchangtao Date: Wed, 18 Sep 2024 16:00:29 +0800 Subject: [PATCH 1/5] feat(transition): support transition to teleport component child --- .../runtime-core/src/componentRenderUtils.ts | 7 +- .../src/components/BaseTransition.ts | 2 +- packages/vue/__tests__/e2e/Transition.spec.ts | 85 ++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index a15d18d56bf..c045252551a 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..1d72a894ccb 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1,6 +1,6 @@ import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' import path from 'node:path' -import { Transition, createApp, h, nextTick, ref } from 'vue' +import { Teleport, Transition, createApp, h, nextTick, ref } from 'vue' describe('e2e: Transition', () => { const { page, html, classList, isVisible, timeout, nextFrame, click } = @@ -2310,6 +2310,89 @@ describe('e2e: Transition', () => { ) }, E2E_TIMEOUT, + ), + test( + 'apply transition to teleport component child', + async () => { + await page().evaluate(() => { + const { createApp, ref, h } = (window as any).Vue + createApp({ + template: ` +
+
+ + content + +
+ + `, + components: { + Comp: { + setup() { + return () => h(Teleport, { to: '#target' }, [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, ) }) From cf6511305b4f32dcaa763ee8ec811d3f66ef25c0 Mon Sep 17 00:00:00 2001 From: yangchangtao Date: Wed, 18 Sep 2024 16:19:20 +0800 Subject: [PATCH 2/5] chore: update --- packages/vue/__tests__/e2e/Transition.spec.ts | 141 +++++++++--------- 1 file changed, 70 insertions(+), 71 deletions(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 1d72a894ccb..a05273a7f4f 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -2311,89 +2311,88 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ), - test( - 'apply transition to teleport component child', - async () => { - await page().evaluate(() => { - const { createApp, ref, h } = (window as any).Vue - createApp({ - template: ` + test( + 'apply transition to teleport component child', + async () => { + await page().evaluate(() => { + const { createApp, ref, h } = (window as any).Vue + createApp({ + template: `
- content +
`, - components: { - Comp: { - setup() { - return () => h(Teleport, { to: '#target' }, [h('div', { class: 'test' }, 'content')]) + components: { + Comp: { + setup() { + return () => + h( + Teleport, + { to: '#target' }, + 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) - }) + setup: () => { + const toggle = ref(false) + const click = () => (toggle.value = !toggle.value) + return { toggle, click } + }, + }).mount('#app') }) - // 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
', - ) + 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) + }) + }) - // 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, - ) + // 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', () => { From e5432782c1eac9f3d3f0bf56123cbc9b7a8e7a11 Mon Sep 17 00:00:00 2001 From: yangchangtao Date: Wed, 18 Sep 2024 16:55:32 +0800 Subject: [PATCH 3/5] chore: test --- packages/vue/__tests__/e2e/Transition.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index a05273a7f4f..db0910da3e8 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -2354,6 +2354,7 @@ describe('e2e: Transition', () => { ;(document.querySelector('#toggleBtn') as any)!.click() return Promise.resolve().then(() => { // find the class of teleported node + console.log(document.querySelector('#target')?.innerHTML) return document .querySelector('#target div')! .className.split(/\s+/g) From 854506192f619626ddb6abd3fa6db77a1784d702 Mon Sep 17 00:00:00 2001 From: yangchangtao Date: Wed, 18 Sep 2024 17:45:02 +0800 Subject: [PATCH 4/5] chore: update --- packages/vue/__tests__/e2e/Transition.spec.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index db0910da3e8..4afa92550c7 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1,6 +1,6 @@ import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils' import path from 'node:path' -import { Teleport, Transition, createApp, h, nextTick, ref } from 'vue' +import { Transition, createApp, h, nextTick, ref } from 'vue' describe('e2e: Transition', () => { const { page, html, classList, isVisible, timeout, nextFrame, click } = @@ -2315,7 +2315,7 @@ describe('e2e: Transition', () => { 'apply transition to teleport component child', async () => { await page().evaluate(() => { - const { createApp, ref, h } = (window as any).Vue + const { createApp, ref } = (window as any).Vue createApp({ template: `
@@ -2328,14 +2328,11 @@ describe('e2e: Transition', () => { `, components: { Comp: { - setup() { - return () => - h( - Teleport, - { to: '#target' }, - h('div', { class: 'test' }, 'content'), - ) - }, + template: ` + +
content
+
+ `, }, }, setup: () => { @@ -2354,7 +2351,6 @@ describe('e2e: Transition', () => { ;(document.querySelector('#toggleBtn') as any)!.click() return Promise.resolve().then(() => { // find the class of teleported node - console.log(document.querySelector('#target')?.innerHTML) return document .querySelector('#target div')! .className.split(/\s+/g) From ba6f7489c1ef58628d0d434d75c363f9c9a24066 Mon Sep 17 00:00:00 2001 From: yangchangtao Date: Thu, 19 Sep 2024 10:38:00 +0800 Subject: [PATCH 5/5] chore: update --- .../runtime-core/src/componentRenderUtils.ts | 2 +- packages/vue/__tests__/e2e/Transition.spec.ts | 130 +++++++++--------- 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index c045252551a..345bf2ac10e 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -248,7 +248,7 @@ export function renderComponentRoot( } // inherit transition data if (vnode.transition) { - const child = getInnerChild(root) ?? root + const child = getInnerChild(root) || root if (__DEV__ && !isElementRoot(child)) { warn( `Component inside renders non-element root node ` + diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 4afa92550c7..ad5ca985e46 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -2310,14 +2310,16 @@ describe('e2e: Transition', () => { ) }, E2E_TIMEOUT, - ), - test( - 'apply transition to teleport component child', - async () => { - await page().evaluate(() => { - const { createApp, ref } = (window as any).Vue - createApp({ - template: ` + ) + + // #11910 + test( + 'apply transition to teleport component child', + async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + template: `
@@ -2326,70 +2328,70 @@ describe('e2e: Transition', () => {
`, - components: { - Comp: { - template: ` + components: { + Comp: { + template: `
content
`, - }, - }, - setup: () => { - const toggle = ref(false) - const click = () => (toggle.value = !toggle.value) - return { toggle, click } }, - }).mount('#app') - }) + }, + 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) - }) + 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, - ) + // 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', () => {