From 5e1e791880238380a1038ae2c505e206ceb34d77 Mon Sep 17 00:00:00 2001 From: linzhe <40790268+linzhe141@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:15:36 +0800 Subject: [PATCH] fix(custom-element): properly mount multiple Teleports in custom element component w/ shadowRoot false (#13900) close #13899 --- packages/runtime-core/src/component.ts | 2 +- .../runtime-core/src/components/Teleport.ts | 12 ++- .../__tests__/customElement.spec.ts | 77 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 21 ++++- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9eb9193da..d51bbe1d2 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1273,5 +1273,5 @@ export interface ComponentCustomElementInterface { /** * @internal attached by the nested Teleport when shadowRoot is false. */ - _teleportTarget?: RendererElement + _teleportTargets?: Set } diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index 9b32989f0..4961d0fe0 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -119,9 +119,6 @@ export const TeleportImpl = { // Teleport *always* has Array children. This is enforced in both the // compiler and vnode children normalization. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { - if (parentComponent && parentComponent.isCE) { - parentComponent.ce!._teleportTarget = container - } mountChildren( children as VNodeArrayChildren, container, @@ -145,6 +142,15 @@ export const TeleportImpl = { } else if (namespace !== 'mathml' && isTargetMathML(target)) { namespace = 'mathml' } + + // track CE teleport targets + if (parentComponent && parentComponent.isCE) { + ;( + parentComponent.ce!._teleportTargets || + (parentComponent.ce!._teleportTargets = new Set()) + ).add(target) + } + if (!disabled) { mount(target, targetAnchor) updateCssVars(n2, false) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 52350dfd2..9db0de9cf 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1308,6 +1308,83 @@ describe('defineCustomElement', () => { app.unmount() }) + test('render two Teleports w/ shadowRoot false', async () => { + const target1 = document.createElement('div') + const target2 = document.createElement('span') + const Child = defineCustomElement( + { + render() { + return [ + h(Teleport, { to: target1 }, [renderSlot(this.$slots, 'header')]), + h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), + ] + }, + }, + { shadowRoot: false }, + ) + customElements.define('my-el-two-teleport-child', Child) + + const App = { + render() { + return h('my-el-two-teleport-child', null, { + default: () => [ + h('div', { slot: 'header' }, 'header'), + h('span', { slot: 'body' }, 'body'), + ], + }) + }, + } + const app = createApp(App) + app.mount(container) + await nextTick() + expect(target1.outerHTML).toBe( + `
header
`, + ) + expect(target2.outerHTML).toBe( + `body`, + ) + app.unmount() + }) + + test('render two Teleports w/ shadowRoot false (with disabled)', async () => { + const target1 = document.createElement('div') + const target2 = document.createElement('span') + const Child = defineCustomElement( + { + render() { + return [ + // with disabled: true + h(Teleport, { to: target1, disabled: true }, [ + renderSlot(this.$slots, 'header'), + ]), + h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]), + ] + }, + }, + { shadowRoot: false }, + ) + customElements.define('my-el-two-teleport-child-0', Child) + + const App = { + render() { + return h('my-el-two-teleport-child-0', null, { + default: () => [ + h('div', { slot: 'header' }, 'header'), + h('span', { slot: 'body' }, 'body'), + ], + }) + }, + } + const app = createApp(App) + app.mount(container) + await nextTick() + expect(target1.outerHTML).toBe(`
`) + expect(target2.outerHTML).toBe( + `body`, + ) + app.unmount() + }) + test('toggle nested custom element with shadowRoot: false', async () => { customElements.define( 'my-el-child-shadow-false', diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index d1f10777f..d99a9341b 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -224,7 +224,7 @@ export class VueElement /** * @internal */ - _teleportTarget?: HTMLElement + _teleportTargets?: Set private _connected = false private _resolved = false @@ -338,6 +338,10 @@ export class VueElement this._app && this._app.unmount() if (this._instance) this._instance.ce = undefined this._app = this._instance = null + if (this._teleportTargets) { + this._teleportTargets.clear() + this._teleportTargets = undefined + } } }) } @@ -635,7 +639,7 @@ export class VueElement * Only called when shadowRoot is false */ private _renderSlots() { - const outlets = (this._teleportTarget || this).querySelectorAll('slot') + const outlets = this._getSlots() const scopeId = this._instance!.type.__scopeId for (let i = 0; i < outlets.length; i++) { const o = outlets[i] as HTMLSlotElement @@ -663,6 +667,19 @@ export class VueElement } } + /** + * @internal + */ + private _getSlots(): HTMLSlotElement[] { + const roots: Element[] = [this] + if (this._teleportTargets) { + roots.push(...this._teleportTargets) + } + return roots.reduce((res, i) => { + res.push(...Array.from(i.querySelectorAll('slot'))) + return res + }, []) + } /** * @internal */ -- 2.47.3