diff --git a/packages/runtime-core/__tests__/components/Teleport.spec.ts b/packages/runtime-core/__tests__/components/Teleport.spec.ts index 9c85cd8be..aca9432b6 100644 --- a/packages/runtime-core/__tests__/components/Teleport.spec.ts +++ b/packages/runtime-core/__tests__/components/Teleport.spec.ts @@ -16,7 +16,7 @@ import { serializeInner, withDirectives, } from '@vue/runtime-test' -import { Fragment, createVNode } from '../../src/vnode' +import { Fragment, createCommentVNode, createVNode } from '../../src/vnode' import { compile, render as domRender } from 'vue' describe('renderer: teleport', () => { @@ -553,4 +553,71 @@ describe('renderer: teleport', () => { `"
teleported
"`, ) }) + + //#9071 + test('toggle sibling node inside target node', async () => { + const root = document.createElement('div') + const show = ref(false) + const App = defineComponent({ + setup() { + return () => { + return show.value + ? h(Teleport, { to: root }, [h('div', 'teleported')]) + : h('div', 'foo') + } + }, + }) + + domRender(h(App), root) + expect(root.innerHTML).toMatchInlineSnapshot('"
foo
"') + + show.value = true + await nextTick() + + expect(root.innerHTML).toMatchInlineSnapshot( + '"
teleported
"', + ) + + show.value = false + await nextTick() + + expect(root.innerHTML).toMatchInlineSnapshot('"
foo
"') + }) + + test('unmount previous sibling node inside target node', async () => { + const root = document.createElement('div') + const parentShow = ref(false) + const childShow = ref(true) + + const Comp = { + setup() { + return () => h(Teleport, { to: root }, [h('div', 'foo')]) + }, + } + + const App = defineComponent({ + setup() { + return () => { + return parentShow.value + ? h(Fragment, { key: 0 }, [ + childShow.value ? h(Comp) : createCommentVNode('v-if'), + ]) + : createCommentVNode('v-if') + } + }, + }) + + domRender(h(App), root) + expect(root.innerHTML).toMatchInlineSnapshot('""') + + parentShow.value = true + await nextTick() + expect(root.innerHTML).toMatchInlineSnapshot( + '"
foo
"', + ) + + parentShow.value = false + await nextTick() + expect(root.innerHTML).toMatchInlineSnapshot('""') + }) }) diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index a10ae84d4..65437300c 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -21,6 +21,8 @@ export interface TeleportProps { disabled?: boolean } +export const TeleportEndKey = Symbol('_vte') + export const isTeleport = (type: any): boolean => type.__isTeleport const isTeleportDisabled = (props: VNode['props']): boolean => @@ -105,11 +107,16 @@ export const TeleportImpl = { const mainAnchor = (n2.anchor = __DEV__ ? createComment('teleport end') : createText('')) + const target = (n2.target = resolveTarget(n2.props, querySelector)) + const targetStart = (n2.targetStart = createText('')) + const targetAnchor = (n2.targetAnchor = createText('')) insert(placeholder, container, anchor) insert(mainAnchor, container, anchor) - const target = (n2.target = resolveTarget(n2.props, querySelector)) - const targetAnchor = (n2.targetAnchor = createText('')) + // attach a special property so we can skip teleported content in + // renderer's nextSibling search + targetStart[TeleportEndKey] = targetAnchor if (target) { + insert(targetStart, target) insert(targetAnchor, target) // #2652 we could be teleporting from a non-SVG tree into an SVG tree if (namespace === 'svg' || isTargetSVG(target)) { @@ -146,6 +153,7 @@ export const TeleportImpl = { } else { // update content n2.el = n1.el + n2.targetStart = n1.targetStart const mainAnchor = (n2.anchor = n1.anchor)! const target = (n2.target = n1.target)! const targetAnchor = (n2.targetAnchor = n1.targetAnchor)! @@ -253,9 +261,18 @@ export const TeleportImpl = { { um: unmount, o: { remove: hostRemove } }: RendererInternals, doRemove: boolean, ) { - const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props, + } = vnode if (target) { + hostRemove(targetStart!) hostRemove(targetAnchor!) } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 8a64baad1..088d1565c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -58,7 +58,11 @@ import { type SuspenseImpl, queueEffectWithSuspense, } from './components/Suspense' -import type { TeleportImpl, TeleportVNode } from './components/Teleport' +import { + TeleportEndKey, + type TeleportImpl, + type TeleportVNode, +} from './components/Teleport' import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive' import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr' import { type RootHydrateFunction, createHydrationFunctions } from './hydration' @@ -140,7 +144,7 @@ export interface RendererOptions< // functions provided via options, so the internal constraint is really just // a generic object. export interface RendererNode { - [key: string]: any + [key: string | symbol]: any } export interface RendererElement extends RendererNode {} @@ -2368,7 +2372,12 @@ function baseCreateRenderer( if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) { return vnode.suspense!.next() } - return hostNextSibling((vnode.anchor || vnode.el)!) + const el = hostNextSibling((vnode.anchor || vnode.el)!) + // #9071, #9313 + // teleported content can mess up nextSibling searches during patch so + // we need to skip them during nextSibling search + const teleportEnd = el && el[TeleportEndKey] + return teleportEnd ? hostNextSibling(teleportEnd) : el } let isFlushing = false diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a0d4074aa..75024b73c 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -198,6 +198,7 @@ export interface VNode< el: HostNode | null anchor: HostNode | null // fragment anchor target: HostElement | null // teleport target + targetStart: HostNode | null // teleport target start anchor targetAnchor: HostNode | null // teleport target anchor /** * number of elements contained in a static vnode @@ -477,6 +478,7 @@ function createBaseVNode( el: null, anchor: null, target: null, + targetStart: null, targetAnchor: null, staticCount: 0, shapeFlag, @@ -677,6 +679,7 @@ export function cloneVNode( ? (children as VNode[]).map(deepCloneVNode) : children, target: vnode.target, + targetStart: vnode.targetStart, targetAnchor: vnode.targetAnchor, staticCount: vnode.staticCount, shapeFlag: vnode.shapeFlag,