]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(custom-element): properly mount multiple Teleports in custom element component...
authorlinzhe <40790268+linzhe141@users.noreply.github.com>
Wed, 24 Sep 2025 09:15:36 +0000 (17:15 +0800)
committerGitHub <noreply@github.com>
Wed, 24 Sep 2025 09:15:36 +0000 (17:15 +0800)
close #13899

packages/runtime-core/src/component.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-dom/__tests__/customElement.spec.ts
packages/runtime-dom/src/apiCustomElement.ts

index 9eb9193da070312d285d343b079cc6edc65bf351..d51bbe1d2f5e64b5f043480e402a033fb50039f5 100644 (file)
@@ -1273,5 +1273,5 @@ export interface ComponentCustomElementInterface {
   /**
    * @internal attached by the nested Teleport when shadowRoot is false.
    */
-  _teleportTarget?: RendererElement
+  _teleportTargets?: Set<RendererElement>
 }
index 9b32989f0b8137f1a4b07410ef439f22f8e97cf7..4961d0fe0482828b2338d1ca7d4a4cec179e076f 100644 (file)
@@ -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)
index 52350dfd27ac893a566d0addca31eb3bf752fc11..9db0de9cf74e117b629a70c4c4c309700016e2dd 100644 (file)
@@ -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(
+        `<div><div slot="header">header</div></div>`,
+      )
+      expect(target2.outerHTML).toBe(
+        `<span><span slot="body">body</span></span>`,
+      )
+      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(`<div></div>`)
+      expect(target2.outerHTML).toBe(
+        `<span><span slot="body">body</span></span>`,
+      )
+      app.unmount()
+    })
+
     test('toggle nested custom element with shadowRoot: false', async () => {
       customElements.define(
         'my-el-child-shadow-false',
index d1f10777f73fc3aacd126252225a1a3a04df08d6..d99a9341bd99f47085edba8de66ce16cabb4c5b9 100644 (file)
@@ -224,7 +224,7 @@ export class VueElement
   /**
    * @internal
    */
-  _teleportTarget?: HTMLElement
+  _teleportTargets?: Set<Element>
 
   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<HTMLSlotElement[]>((res, i) => {
+      res.push(...Array.from(i.querySelectorAll('slot')))
+      return res
+    }, [])
+  }
   /**
    * @internal
    */