diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 2b58bc3fc43..f8755e7e765 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -24,7 +24,7 @@ import { SchedulerJobFlags } from '../scheduler' type Hook void> = T | T[] -const leaveCbKey: unique symbol = Symbol('_leaveCb') +export const leaveCbKey: unique symbol = Symbol('_leaveCb') const enterCbKey: unique symbol = Symbol('_enterCb') export interface BaseTransitionProps { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 05c4ac345eb..b0edfcfe2f3 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -85,7 +85,7 @@ import { initFeatureFlags } from './featureFlags' import { isAsyncWrapper } from './apiAsyncComponent' import { isCompatEnabled } from './compat/compatConfig' import { DeprecationTypes } from './compat/compatConfig' -import type { TransitionHooks } from './components/BaseTransition' +import { type TransitionHooks, leaveCbKey } from './components/BaseTransition' export interface Renderer { render: RootRenderFunction @@ -2057,6 +2057,12 @@ function baseCreateRenderer( } } const performLeave = () => { + // #13153 move kept-alive node before v-show transition leave finishes + // it needs to call the leaving callback to ensure element's `display` + // is `none` + if (el!._isLeaving) { + el![leaveCbKey](true /* cancelled */) + } leave(el!, () => { remove() afterLeave && afterLeave() diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 14441bd823b..68f7075828c 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -1722,6 +1722,107 @@ describe('e2e: Transition', () => { }, E2E_TIMEOUT, ) + + // #13153 + test( + 'move kept-alive node before v-show transition leave finishes', + async () => { + await page().evaluate(() => { + const { createApp, ref } = (window as any).Vue + const show = ref(true) + createApp({ + template: ` +
+ + + +
+ + `, + setup: () => { + const state = ref(1) + const click = () => (state.value = state.value === 1 ? 2 : 1) + return { state, click } + }, + components: { + Comp1: { + components: { + Item: { + name: 'Item', + setup() { + return { show } + }, + template: ` + +
+

{{ show ? "I should show" : "I shouldn't show " }}

+
+
+ `, + }, + }, + name: 'Comp1', + setup() { + const toggle = () => (show.value = !show.value) + return { show, toggle } + }, + template: ` + +

This is page1

+ + `, + }, + Comp2: { + name: 'Comp2', + template: `

This is page2

`, + }, + }, + }).mount('#app') + }) + + expect(await html('#container')).toBe( + `

I should show

` + + `

This is page1

` + + ``, + ) + + // trigger v-show transition leave + await click('#changeShowBtn') + await nextTick() + expect(await html('#container')).toBe( + `

I shouldn't show

` + + `

This is page1

` + + ``, + ) + + // switch to page2, before leave finishes + // expect v-show element's display to be none + await click('#toggleBtn') + await nextTick() + expect(await html('#container')).toBe( + `` + + `

This is page2

`, + ) + + // switch back to page1 + // expect v-show element's display to be none + await click('#toggleBtn') + await nextTick() + expect(await html('#container')).toBe( + `` + + `

This is page1

` + + ``, + ) + + await transitionFinish() + expect(await html('#container')).toBe( + `

I shouldn't show

` + + `

This is page1

` + + ``, + ) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => {