Skip to content

Commit 5e1e791

Browse files
authored
fix(custom-element): properly mount multiple Teleports in custom element component w/ shadowRoot false (#13900)
close #13899
1 parent 95c1975 commit 5e1e791

File tree

4 files changed

+106
-6
lines changed

4 files changed

+106
-6
lines changed

packages/runtime-core/src/component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1273,5 +1273,5 @@ export interface ComponentCustomElementInterface {
12731273
/**
12741274
* @internal attached by the nested Teleport when shadowRoot is false.
12751275
*/
1276-
_teleportTarget?: RendererElement
1276+
_teleportTargets?: Set<RendererElement>
12771277
}

packages/runtime-core/src/components/Teleport.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@ export const TeleportImpl = {
119119
// Teleport *always* has Array children. This is enforced in both the
120120
// compiler and vnode children normalization.
121121
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
122-
if (parentComponent && parentComponent.isCE) {
123-
parentComponent.ce!._teleportTarget = container
124-
}
125122
mountChildren(
126123
children as VNodeArrayChildren,
127124
container,
@@ -145,6 +142,15 @@ export const TeleportImpl = {
145142
} else if (namespace !== 'mathml' && isTargetMathML(target)) {
146143
namespace = 'mathml'
147144
}
145+
146+
// track CE teleport targets
147+
if (parentComponent && parentComponent.isCE) {
148+
;(
149+
parentComponent.ce!._teleportTargets ||
150+
(parentComponent.ce!._teleportTargets = new Set())
151+
).add(target)
152+
}
153+
148154
if (!disabled) {
149155
mount(target, targetAnchor)
150156
updateCssVars(n2, false)

packages/runtime-dom/__tests__/customElement.spec.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,83 @@ describe('defineCustomElement', () => {
13081308
app.unmount()
13091309
})
13101310

1311+
test('render two Teleports w/ shadowRoot false', async () => {
1312+
const target1 = document.createElement('div')
1313+
const target2 = document.createElement('span')
1314+
const Child = defineCustomElement(
1315+
{
1316+
render() {
1317+
return [
1318+
h(Teleport, { to: target1 }, [renderSlot(this.$slots, 'header')]),
1319+
h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]),
1320+
]
1321+
},
1322+
},
1323+
{ shadowRoot: false },
1324+
)
1325+
customElements.define('my-el-two-teleport-child', Child)
1326+
1327+
const App = {
1328+
render() {
1329+
return h('my-el-two-teleport-child', null, {
1330+
default: () => [
1331+
h('div', { slot: 'header' }, 'header'),
1332+
h('span', { slot: 'body' }, 'body'),
1333+
],
1334+
})
1335+
},
1336+
}
1337+
const app = createApp(App)
1338+
app.mount(container)
1339+
await nextTick()
1340+
expect(target1.outerHTML).toBe(
1341+
`<div><div slot="header">header</div></div>`,
1342+
)
1343+
expect(target2.outerHTML).toBe(
1344+
`<span><span slot="body">body</span></span>`,
1345+
)
1346+
app.unmount()
1347+
})
1348+
1349+
test('render two Teleports w/ shadowRoot false (with disabled)', async () => {
1350+
const target1 = document.createElement('div')
1351+
const target2 = document.createElement('span')
1352+
const Child = defineCustomElement(
1353+
{
1354+
render() {
1355+
return [
1356+
// with disabled: true
1357+
h(Teleport, { to: target1, disabled: true }, [
1358+
renderSlot(this.$slots, 'header'),
1359+
]),
1360+
h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]),
1361+
]
1362+
},
1363+
},
1364+
{ shadowRoot: false },
1365+
)
1366+
customElements.define('my-el-two-teleport-child-0', Child)
1367+
1368+
const App = {
1369+
render() {
1370+
return h('my-el-two-teleport-child-0', null, {
1371+
default: () => [
1372+
h('div', { slot: 'header' }, 'header'),
1373+
h('span', { slot: 'body' }, 'body'),
1374+
],
1375+
})
1376+
},
1377+
}
1378+
const app = createApp(App)
1379+
app.mount(container)
1380+
await nextTick()
1381+
expect(target1.outerHTML).toBe(`<div></div>`)
1382+
expect(target2.outerHTML).toBe(
1383+
`<span><span slot="body">body</span></span>`,
1384+
)
1385+
app.unmount()
1386+
})
1387+
13111388
test('toggle nested custom element with shadowRoot: false', async () => {
13121389
customElements.define(
13131390
'my-el-child-shadow-false',

packages/runtime-dom/src/apiCustomElement.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export class VueElement
224224
/**
225225
* @internal
226226
*/
227-
_teleportTarget?: HTMLElement
227+
_teleportTargets?: Set<Element>
228228

229229
private _connected = false
230230
private _resolved = false
@@ -338,6 +338,10 @@ export class VueElement
338338
this._app && this._app.unmount()
339339
if (this._instance) this._instance.ce = undefined
340340
this._app = this._instance = null
341+
if (this._teleportTargets) {
342+
this._teleportTargets.clear()
343+
this._teleportTargets = undefined
344+
}
341345
}
342346
})
343347
}
@@ -635,7 +639,7 @@ export class VueElement
635639
* Only called when shadowRoot is false
636640
*/
637641
private _renderSlots() {
638-
const outlets = (this._teleportTarget || this).querySelectorAll('slot')
642+
const outlets = this._getSlots()
639643
const scopeId = this._instance!.type.__scopeId
640644
for (let i = 0; i < outlets.length; i++) {
641645
const o = outlets[i] as HTMLSlotElement
@@ -663,6 +667,19 @@ export class VueElement
663667
}
664668
}
665669

670+
/**
671+
* @internal
672+
*/
673+
private _getSlots(): HTMLSlotElement[] {
674+
const roots: Element[] = [this]
675+
if (this._teleportTargets) {
676+
roots.push(...this._teleportTargets)
677+
}
678+
return roots.reduce<HTMLSlotElement[]>((res, i) => {
679+
res.push(...Array.from(i.querySelectorAll('slot')))
680+
return res
681+
}, [])
682+
}
666683
/**
667684
* @internal
668685
*/

0 commit comments

Comments
 (0)