fix(custom-element): properly mount multiple Teleports in custom element component w/ shadowRoot false (#13900)

close #13899
This commit is contained in:
linzhe 2025-09-24 17:15:36 +08:00 committed by GitHub
parent 95c1975604
commit 5e1e791880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 6 deletions

View File

@ -1273,5 +1273,5 @@ export interface ComponentCustomElementInterface {
/** /**
* @internal attached by the nested Teleport when shadowRoot is false. * @internal attached by the nested Teleport when shadowRoot is false.
*/ */
_teleportTarget?: RendererElement _teleportTargets?: Set<RendererElement>
} }

View File

@ -119,9 +119,6 @@ export const TeleportImpl = {
// Teleport *always* has Array children. This is enforced in both the // Teleport *always* has Array children. This is enforced in both the
// compiler and vnode children normalization. // compiler and vnode children normalization.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (parentComponent && parentComponent.isCE) {
parentComponent.ce!._teleportTarget = container
}
mountChildren( mountChildren(
children as VNodeArrayChildren, children as VNodeArrayChildren,
container, container,
@ -145,6 +142,15 @@ export const TeleportImpl = {
} else if (namespace !== 'mathml' && isTargetMathML(target)) { } else if (namespace !== 'mathml' && isTargetMathML(target)) {
namespace = 'mathml' namespace = 'mathml'
} }
// track CE teleport targets
if (parentComponent && parentComponent.isCE) {
;(
parentComponent.ce!._teleportTargets ||
(parentComponent.ce!._teleportTargets = new Set())
).add(target)
}
if (!disabled) { if (!disabled) {
mount(target, targetAnchor) mount(target, targetAnchor)
updateCssVars(n2, false) updateCssVars(n2, false)

View File

@ -1308,6 +1308,83 @@ describe('defineCustomElement', () => {
app.unmount() 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 () => { test('toggle nested custom element with shadowRoot: false', async () => {
customElements.define( customElements.define(
'my-el-child-shadow-false', 'my-el-child-shadow-false',

View File

@ -224,7 +224,7 @@ export class VueElement
/** /**
* @internal * @internal
*/ */
_teleportTarget?: HTMLElement _teleportTargets?: Set<Element>
private _connected = false private _connected = false
private _resolved = false private _resolved = false
@ -338,6 +338,10 @@ export class VueElement
this._app && this._app.unmount() this._app && this._app.unmount()
if (this._instance) this._instance.ce = undefined if (this._instance) this._instance.ce = undefined
this._app = this._instance = null 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 * Only called when shadowRoot is false
*/ */
private _renderSlots() { private _renderSlots() {
const outlets = (this._teleportTarget || this).querySelectorAll('slot') const outlets = this._getSlots()
const scopeId = this._instance!.type.__scopeId const scopeId = this._instance!.type.__scopeId
for (let i = 0; i < outlets.length; i++) { for (let i = 0; i < outlets.length; i++) {
const o = outlets[i] as HTMLSlotElement 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 * @internal
*/ */