diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 3044884669a..eb9b896c7f9 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -166,7 +166,7 @@ export function renderComponentRoot( propsOptions, ) } - root = cloneVNode(root, fallthroughAttrs) + root = cloneVNode(root, fallthroughAttrs, false, true) } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { const allAttrs = Object.keys(attrs) const eventAttrs: string[] = [] @@ -221,10 +221,15 @@ export function renderComponentRoot( getComponentName(instance.type), ) } - root = cloneVNode(root, { - class: cls, - style: style, - }) + root = cloneVNode( + root, + { + class: cls, + style: style, + }, + false, + true, + ) } } @@ -237,7 +242,7 @@ export function renderComponentRoot( ) } // clone before mutating since the root may be a hoisted vnode - root = cloneVNode(root) + root = cloneVNode(root, null, false, true) root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs } // inherit transition data diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a1a6a908d2a..0e2a4bafcc5 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -624,10 +624,11 @@ export function cloneVNode( vnode: VNode, extraProps?: (Data & VNodeProps) | null, mergeRef = false, + cloneTransition = false, ): VNode { // This is intentionally NOT using spread or extend to avoid the runtime // key enumeration cost. - const { props, ref, patchFlag, children } = vnode + const { props, ref, patchFlag, children, transition } = vnode const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props const cloned: VNode = { __v_isVNode: true, @@ -670,7 +671,7 @@ export function cloneVNode( dynamicChildren: vnode.dynamicChildren, appContext: vnode.appContext, dirs: vnode.dirs, - transition: vnode.transition, + transition, // These should technically only be non-null on mounted VNodes. However, // they *should* be copied for kept-alive vnodes. So we just always copy @@ -685,9 +686,18 @@ export function cloneVNode( ctx: vnode.ctx, ce: vnode.ce, } + + // if the vnode will be replaced by the cloned one, it is necessary + // to clone the transition to ensure that the vnode referenced within + // the transition hooks is fresh. + if (transition && cloneTransition) { + cloned.transition = transition.clone(cloned as VNode) + } + if (__COMPAT__) { defineLegacyVNodeProperties(cloned as VNode) } + return cloned } diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index b2c1ba572dc..4fe78ae8ab0 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1215,6 +1215,54 @@ describe('e2e: Transition', () => { E2E_TIMEOUT, ) + // #3716 + test( + 'wrapping transition + fallthrough attrs', + async () => { + await page().goto(baseUrl) + await page().waitForSelector('#app') + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + 'my-transition': { + template: ` + + + + `, + }, + }, + template: ` +
+ +
content
+
+
+ + `, + setup: () => { + const toggle = ref(true) + const click = () => (toggle.value = !toggle.value) + return { toggle, click } + }, + }).mount('#app') + }) + expect(await html('#container')).toBe('
content
') + + await click('#toggleBtn') + // toggle again before leave finishes + await nextTick() + await click('#toggleBtn') + + await transitionFinish() + expect(await html('#container')).toBe( + '
content
', + ) + }, + E2E_TIMEOUT, + ) + test( 'w/ KeepAlive + unmount innerChild', async () => {