Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/runtime-core/__tests__/components/BaseTransition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BaseTransition,
type BaseTransitionProps,
KeepAlive,
Teleport,
type TestElement,
type VNodeProps,
h,
Expand Down Expand Up @@ -1293,4 +1294,66 @@ describe('BaseTransition', () => {
await runTestWithKeepAlive(testOutIn)
__DEV__ = true
})

test('should preserve transition for component with KeepAlive root', async () => {
const root = nodeOps.createElement('div')
const show = ref(false)
const onBeforeEnter = vi.fn()
const Child = { render: () => h('div', 'content') }
const Comp = {
render: () => h(KeepAlive, null, { default: () => h(Child) }),
}
const App = () =>
h(BaseTransition, { onBeforeEnter }, () => (show.value ? h(Comp) : null))

render(h(App), root)

show.value = true
await nextTick()
expect(serializeInner(root)).toBe('<div>content</div>')
expect(onBeforeEnter).toHaveBeenCalledTimes(1)
})

test('should not error on component with empty Teleport root', () => {
const root = nodeOps.createElement('div')
const target = nodeOps.createElement('div')
const Comp = () => h(Teleport, { to: target }, [])

expect(() =>
render(
h(BaseTransition, null, () => h(Comp)),
root,
),
).not.toThrow()
expect(serializeInner(root)).toBe(
'<!--teleport start--><!--teleport end-->',
)
expect(serializeInner(target)).toBe('')
expect(
'Component inside <Transition> renders non-element root node',
).toHaveBeenWarned()
})

test('should update transition hooks for component with Teleport root', async () => {
const root = nodeOps.createElement('div')
const target = nodeOps.createElement('div')
const show = ref(true)
const firstBeforeLeave = vi.fn()
const nextBeforeLeave = vi.fn()
const beforeLeave = ref(firstBeforeLeave)
const Comp = () => h(Teleport, { to: target }, h('div', 'content'))
const App = () =>
h(BaseTransition, { onBeforeLeave: beforeLeave.value }, () =>
show.value ? h(Comp) : null,
)

render(h(App), root)

beforeLeave.value = nextBeforeLeave
show.value = false
await nextTick()

expect(firstBeforeLeave).not.toHaveBeenCalled()
expect(nextBeforeLeave).toHaveBeenCalledTimes(1)
})
})
8 changes: 5 additions & 3 deletions packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import {
warnDeprecation,
} from './compat/compatConfig'
import { shallowReadonly } from '@vue/reactivity'
import { setTransitionHooks } from './components/BaseTransition'
import { getInnerChild, setTransitionHooks } from './components/BaseTransition'
import { isTeleport } from './components/Teleport'

/**
* dev only flag to track whether $attrs was used during render.
Expand Down Expand Up @@ -255,13 +256,14 @@ export function renderComponentRoot(
}
// inherit transition data
if (vnode.transition) {
if (__DEV__ && !isElementRoot(root)) {
const child = isTeleport(root.type) ? getInnerChild(root) || root : root
if (__DEV__ && !isElementRoot(child)) {
warn(
`Component inside <Transition> renders non-element root node ` +
`that cannot be animated.`,
)
}
setTransitionHooks(root, vnode.transition)
setTransitionHooks(child, vnode.transition)
}

if (__DEV__ && setRoot) {
Expand Down
8 changes: 6 additions & 2 deletions packages/runtime-core/src/components/BaseTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
}
}

function getInnerChild(vnode: VNode): VNode | undefined {
export function getInnerChild(vnode: VNode): VNode | undefined {
if (!isKeepAlive(vnode)) {
if (isTeleport(vnode.type) && vnode.children) {
return findNonCommentChild(vnode.children as VNode[])
Expand Down Expand Up @@ -538,7 +538,11 @@ function getInnerChild(vnode: VNode): VNode | undefined {
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
vnode.transition = hooks
setTransitionHooks(vnode.component.subTree, hooks)
const subTree = vnode.component.subTree
setTransitionHooks(
isTeleport(subTree.type) ? getInnerChild(subTree) || subTree : subTree,
hooks,
)
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
Expand Down
81 changes: 81 additions & 0 deletions packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2782,6 +2782,87 @@ describe('e2e: Transition', () => {
},
E2E_TIMEOUT,
)

// #11910
test(
'apply transition to teleport component child',
async () => {
await page().evaluate(() => {
const { createApp, ref } = (window as any).Vue
createApp({
template: `
<div id="target"></div>
<div id="container">
<transition>
<Comp v-if="toggle"></Comp>
</transition>
</div>
<button id="toggleBtn" @click="click">button</button>
`,
components: {
Comp: {
template: `
<Teleport to="#target">
<div class="test">content</div>
</Teleport>
`,
},
},
setup: () => {
const toggle = ref(false)
const click = () => (toggle.value = !toggle.value)
return { toggle, click }
},
}).mount('#app')
})

expect(await html('#target')).toBe('')
expect(await html('#container')).toBe('<!--v-if-->')

const classWhenTransitionStart = () =>
page().evaluate(() => {
;(document.querySelector('#toggleBtn') as any)!.click()
return Promise.resolve().then(() => {
// find the class of teleported node
return document
.querySelector('#target div')!
.className.split(/\s+/g)
})
})

// enter
expect(await classWhenTransitionStart()).toStrictEqual([
'test',
'v-enter-from',
'v-enter-active',
])
await nextFrame()
expect(await classList('.test')).toStrictEqual([
'test',
'v-enter-active',
'v-enter-to',
])
await transitionFinish()
expect(await html('#target')).toBe('<div class="test">content</div>')

// leave
expect(await classWhenTransitionStart()).toStrictEqual([
'test',
'v-leave-from',
'v-leave-active',
])
await nextFrame()
expect(await classList('.test')).toStrictEqual([
'test',
'v-leave-active',
'v-leave-to',
])
await transitionFinish()
expect(await html('#target')).toBe('')
expect(await html('#container')).toBe('<!--v-if-->')
},
E2E_TIMEOUT,
)
})

describe('transition with v-show', () => {
Expand Down
Loading