Skip to content

Commit

Permalink
feat(transition): support directly nesting Teleport inside Transition (
Browse files Browse the repository at this point in the history
  • Loading branch information
edison1105 committed Apr 25, 2024
1 parent 0c3a920 commit 0e6e3c7
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 24 deletions.
57 changes: 33 additions & 24 deletions packages/runtime-core/src/components/BaseTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared'
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
import { isTeleport } from './Teleport'
import type { RendererElement } from '../renderer'
import { SchedulerJobFlags } from '../scheduler'

Expand Down Expand Up @@ -152,27 +153,7 @@ const BaseTransitionImpl: ComponentOptions = {
return
}

let child: VNode = children[0]
if (children.length > 1) {
let hasFound = false
// locate first non-comment child
for (const c of children) {
if (c.type !== Comment) {
if (__DEV__ && hasFound) {
// warn more than one non-comment child
warn(
'<transition> can only be used on a single element or component. ' +
'Use <transition-group> for lists.',
)
break
}
child = c
hasFound = true
if (!__DEV__) break
}
}
}

const child: VNode = findNonCommentChild(children)
// there's no need to track reactivity for these props so use the raw
// props for a bit better perf
const rawProps = toRaw(props)
Expand All @@ -194,7 +175,7 @@ const BaseTransitionImpl: ComponentOptions = {

// in the case of <transition><keep-alive/></transition>, we need to
// compare the type of the kept-alive children.
const innerChild = getKeepAliveChild(child)
const innerChild = getInnerChild(child)
if (!innerChild) {
return emptyPlaceholder(child)
}
Expand All @@ -208,7 +189,7 @@ const BaseTransitionImpl: ComponentOptions = {
setTransitionHooks(innerChild, enterHooks)

const oldChild = instance.subTree
const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
const oldInnerChild = oldChild && getInnerChild(oldChild)

// handle mode
if (
Expand Down Expand Up @@ -268,6 +249,30 @@ if (__COMPAT__) {
BaseTransitionImpl.__isBuiltIn = true
}

function findNonCommentChild(children: VNode[]): VNode {
let child: VNode = children[0]
if (children.length > 1) {
let hasFound = false
// locate first non-comment child
for (const c of children) {
if (c.type !== Comment) {
if (__DEV__ && hasFound) {
// warn more than one non-comment child
warn(
'<transition> can only be used on a single element or component. ' +
'Use <transition-group> for lists.',
)
break
}
child = c
hasFound = true
if (!__DEV__) break
}
}
}
return child
}

// export the public type for h/tsx inference
// also to avoid inline import() in generated d.ts files
export const BaseTransition = BaseTransitionImpl as unknown as {
Expand Down Expand Up @@ -458,8 +463,12 @@ function emptyPlaceholder(vnode: VNode): VNode | undefined {
}
}

function getKeepAliveChild(vnode: VNode): VNode | undefined {
function getInnerChild(vnode: VNode): VNode | undefined {
if (!isKeepAlive(vnode)) {
if (isTeleport(vnode.type) && vnode.children) {
return findNonCommentChild(vnode.children as VNode[])
}

return vnode
}
// #7121 ensure get the child component subtree in case
Expand Down
89 changes: 89 additions & 0 deletions packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1725,6 +1725,95 @@ describe('e2e: Transition', () => {
)
})

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

expect(await html('#target')).toBe('<!-- comment --><!--v-if-->')
expect(await html('#container')).toBe(
'<!--teleport start--><!--teleport end-->',
)

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(
'<!-- comment --><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('<!-- comment --><!--v-if-->')
expect(await html('#container')).toBe(
'<!--teleport start--><!--teleport end-->',
)
},
E2E_TIMEOUT,
)
})

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

0 comments on commit 0e6e3c7

Please sign in to comment.