diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 90cc22f5470..cb68d558d5e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -72,7 +72,11 @@ import { } from './components/Teleport' import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive' import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr' -import { type RootHydrateFunction, createHydrationFunctions } from './hydration' +import { + DOMNodeTypes, + type RootHydrateFunction, + createHydrationFunctions, +} from './hydration' import { invokeDirectiveHook } from './directives' import { endMeasure, startMeasure } from './profiling' import { @@ -2185,24 +2189,10 @@ function baseCreateRenderer( const remove: RemoveFn = vnode => { const { type, el, anchor, transition } = vnode - if (type === Fragment) { - if ( - __DEV__ && - vnode.patchFlag > 0 && - vnode.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT && - transition && - !transition.persisted - ) { - ;(vnode.children as VNode[]).forEach(child => { - if (child.type === Comment) { - hostRemove(child.el!) - } else { - remove(child) - } - }) - } else { - removeFragment(el!, anchor!) - } + const isFragment = type === Fragment + + if ((!__DEV__ || !transition) && isFragment) { + removeFragment(el!, anchor!) return } @@ -2212,21 +2202,23 @@ function baseCreateRenderer( } const performRemove = () => { - hostRemove(el!) + isFragment ? removeFragment(el!, anchor!) : hostRemove(el!) if (transition && !transition.persisted && transition.afterLeave) { transition.afterLeave() } } if ( - vnode.shapeFlag & ShapeFlags.ELEMENT && + (isFragment || vnode.shapeFlag & ShapeFlags.ELEMENT) && transition && !transition.persisted ) { const { leave, delayLeave } = transition - const performLeave = () => leave(el!, performRemove) + const effectiveEl = + __DEV__ && !isFragment ? el! : getFirstElement(el!, anchor!) + const performLeave = () => leave(effectiveEl, performRemove) if (delayLeave) { - delayLeave(vnode.el!, performRemove, performLeave) + delayLeave(el!, performRemove, performLeave) } else { performLeave() } @@ -2247,6 +2239,14 @@ function baseCreateRenderer( hostRemove(end) } + const getFirstElement = (cur: RendererNode, end: RendererNode) => { + while (cur.nodeType !== DOMNodeTypes.ELEMENT && cur !== end) { + cur = hostNextSibling(cur)! + } + + return cur + } + const unmountComponent = ( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null, diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index c0863a75991..869dc4694d5 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -3053,7 +3053,7 @@ describe('e2e: Transition', () => { }) test( - 'should work with dev root fragment', + 'toggle single component with comments before the single root element', async () => { await page().evaluate(() => { const { createApp, ref } = (window as any).Vue @@ -3061,9 +3061,9 @@ describe('e2e: Transition', () => { components: { Comp: { template: ` - -
- `, + +
+ `, }, }, template: ` @@ -3121,4 +3121,77 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + test( + 'toggle multiple components with comments before the single root element', + async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + createApp({ + components: { + One: { + template: ` + +
One
+ `, + }, + Two: { + template: `
Two
`, + }, + }, + template: ` + +
+ + + + +
+ `, + setup: () => { + const toggle = ref(true) + const click = () => (toggle.value = !toggle.value) + return { toggle, click } + }, + }).mount('#app') + }) + + expect(await html('#container')).toBe( + '
One
', + ) + + // one -> two + expect(await classWhenTransitionStart()).toStrictEqual([ + 'one', + 'v-leave-from', + 'v-leave-active', + ]) + await nextFrame() + expect(await classList('.two')).toStrictEqual([ + 'two', + 'v-enter-from', + 'v-enter-active', + ]) + await transitionFinish(duration * 2) + expect(await html('#container')).toBe('
Two
') + + // two -> one + expect(await classWhenTransitionStart()).toStrictEqual([ + 'two', + 'v-leave-from', + 'v-leave-active', + ]) + await nextFrame() + expect(await classList('.one')).toStrictEqual([ + 'one', + 'v-enter-from', + 'v-enter-active', + ]) + await transitionFinish(duration * 2) + expect(await html('#container')).toBe( + '
One
', + ) + }, + E2E_TIMEOUT, + ) })