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<T = () => 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<HostElement = RendererElement> { 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<HostElement = RendererElement> { render: RootRenderFunction<HostElement> @@ -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 + } 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: ` + <div id="container"> + <KeepAlive :include="['Comp1', 'Comp2']"> + <component :is="state === 1 ? 'Comp1' : 'Comp2'"/> + </KeepAlive> + </div> + <button id="toggleBtn" @click="click">button</button> + `, + 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: ` + <Transition name="test"> + <div v-show="show" > + <h2>{{ show ? "I should show" : "I shouldn't show " }}</h2> + </div> + </Transition> + `, + }, + }, + name: 'Comp1', + setup() { + const toggle = () => (show.value = !show.value) + return { show, toggle } + }, + template: ` + <Item /> + <h2>This is page1</h2> + <button id="changeShowBtn" @click="toggle">{{ show }}</button> + `, + }, + Comp2: { + name: 'Comp2', + template: `<h2>This is page2</h2>`, + }, + }, + }).mount('#app') + }) + + expect(await html('#container')).toBe( + `<div><h2>I should show</h2></div>` + + `<h2>This is page1</h2>` + + `<button id="changeShowBtn">true</button>`, + ) + + // trigger v-show transition leave + await click('#changeShowBtn') + await nextTick() + expect(await html('#container')).toBe( + `<div class="test-leave-from test-leave-active"><h2>I shouldn't show </h2></div>` + + `<h2>This is page1</h2>` + + `<button id="changeShowBtn">false</button>`, + ) + + // 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( + `<div class="test-leave-from test-leave-active" style="display: none;"><h2>I shouldn't show </h2></div>` + + `<h2>This is page2</h2>`, + ) + + // switch back to page1 + // expect v-show element's display to be none + await click('#toggleBtn') + await nextTick() + expect(await html('#container')).toBe( + `<div class="test-enter-from test-enter-active" style="display: none;"><h2>I shouldn't show </h2></div>` + + `<h2>This is page1</h2>` + + `<button id="changeShowBtn">false</button>`, + ) + + await transitionFinish() + expect(await html('#container')).toBe( + `<div class="" style="display: none;"><h2>I shouldn't show </h2></div>` + + `<h2>This is page1</h2>` + + `<button id="changeShowBtn">false</button>`, + ) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => {