Skip to content

Commit 638a79f

Browse files
authored
fix(runtime-core): properly handle inherit transition during clone VNode (#10809)
close #3716 close #10497 close #4091
1 parent e8fd644 commit 638a79f

File tree

3 files changed

+71
-8
lines changed

3 files changed

+71
-8
lines changed

packages/runtime-core/src/componentRenderUtils.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export function renderComponentRoot(
166166
propsOptions,
167167
)
168168
}
169-
root = cloneVNode(root, fallthroughAttrs)
169+
root = cloneVNode(root, fallthroughAttrs, false, true)
170170
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
171171
const allAttrs = Object.keys(attrs)
172172
const eventAttrs: string[] = []
@@ -221,10 +221,15 @@ export function renderComponentRoot(
221221
getComponentName(instance.type),
222222
)
223223
}
224-
root = cloneVNode(root, {
225-
class: cls,
226-
style: style,
227-
})
224+
root = cloneVNode(
225+
root,
226+
{
227+
class: cls,
228+
style: style,
229+
},
230+
false,
231+
true,
232+
)
228233
}
229234
}
230235

@@ -237,7 +242,7 @@ export function renderComponentRoot(
237242
)
238243
}
239244
// clone before mutating since the root may be a hoisted vnode
240-
root = cloneVNode(root)
245+
root = cloneVNode(root, null, false, true)
241246
root.dirs = root.dirs ? root.dirs.concat(vnode.dirs) : vnode.dirs
242247
}
243248
// inherit transition data

packages/runtime-core/src/vnode.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,11 @@ export function cloneVNode<T, U>(
624624
vnode: VNode<T, U>,
625625
extraProps?: (Data & VNodeProps) | null,
626626
mergeRef = false,
627+
cloneTransition = false,
627628
): VNode<T, U> {
628629
// This is intentionally NOT using spread or extend to avoid the runtime
629630
// key enumeration cost.
630-
const { props, ref, patchFlag, children } = vnode
631+
const { props, ref, patchFlag, children, transition } = vnode
631632
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
632633
const cloned: VNode<T, U> = {
633634
__v_isVNode: true,
@@ -670,7 +671,7 @@ export function cloneVNode<T, U>(
670671
dynamicChildren: vnode.dynamicChildren,
671672
appContext: vnode.appContext,
672673
dirs: vnode.dirs,
673-
transition: vnode.transition,
674+
transition,
674675

675676
// These should technically only be non-null on mounted VNodes. However,
676677
// they *should* be copied for kept-alive vnodes. So we just always copy
@@ -685,9 +686,18 @@ export function cloneVNode<T, U>(
685686
ctx: vnode.ctx,
686687
ce: vnode.ce,
687688
}
689+
690+
// if the vnode will be replaced by the cloned one, it is necessary
691+
// to clone the transition to ensure that the vnode referenced within
692+
// the transition hooks is fresh.
693+
if (transition && cloneTransition) {
694+
cloned.transition = transition.clone(cloned as VNode)
695+
}
696+
688697
if (__COMPAT__) {
689698
defineLegacyVNodeProperties(cloned as VNode)
690699
}
700+
691701
return cloned
692702
}
693703

packages/vue/__tests__/e2e/Transition.spec.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,54 @@ describe('e2e: Transition', () => {
12151215
E2E_TIMEOUT,
12161216
)
12171217

1218+
// #3716
1219+
test(
1220+
'wrapping transition + fallthrough attrs',
1221+
async () => {
1222+
await page().goto(baseUrl)
1223+
await page().waitForSelector('#app')
1224+
await page().evaluate(() => {
1225+
const { createApp, ref } = (window as any).Vue
1226+
createApp({
1227+
components: {
1228+
'my-transition': {
1229+
template: `
1230+
<transition foo="1" name="test">
1231+
<slot></slot>
1232+
</transition>
1233+
`,
1234+
},
1235+
},
1236+
template: `
1237+
<div id="container">
1238+
<my-transition>
1239+
<div v-if="toggle">content</div>
1240+
</my-transition>
1241+
</div>
1242+
<button id="toggleBtn" @click="click">button</button>
1243+
`,
1244+
setup: () => {
1245+
const toggle = ref(true)
1246+
const click = () => (toggle.value = !toggle.value)
1247+
return { toggle, click }
1248+
},
1249+
}).mount('#app')
1250+
})
1251+
expect(await html('#container')).toBe('<div foo="1">content</div>')
1252+
1253+
await click('#toggleBtn')
1254+
// toggle again before leave finishes
1255+
await nextTick()
1256+
await click('#toggleBtn')
1257+
1258+
await transitionFinish()
1259+
expect(await html('#container')).toBe(
1260+
'<div foo="1" class="">content</div>',
1261+
)
1262+
},
1263+
E2E_TIMEOUT,
1264+
)
1265+
12181266
test(
12191267
'w/ KeepAlive + unmount innerChild',
12201268
async () => {

0 commit comments

Comments
 (0)