shadow-dom-overlay-insertion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseShadow DOM Overlay Insertion
Shadow DOM 覆盖元素插入
The Problem
问题
When component A (e.g. EFCanvas) tries to insert an overlay into the DOM next to itself, it typically uses . This breaks when A is slotted inside a shadow DOM host (e.g. EFWorkbench).
this.parentElement.appendChild(overlay)EFWorkbench (shadow host)
shadow root
<div class="canvas-slot-container"> ← the real DOM parent
<slot name="canvas"> ← slot element
[EFCanvas is assigned here] ← light DOM, assigned via slot- returns the shadow host (EFWorkbench), not the internal container
efCanvas.parentElement - Calling places the overlay in the host's light DOM, where CSS and z-index from the shadow DOM won't apply — the overlay is invisible
shadowHost.appendChild(overlay)
当组件A(例如EFCanvas)尝试在DOM中自身旁边插入一个覆盖元素时,通常会使用。但当A被插入到Shadow DOM宿主(例如EFWorkbench)的插槽中时,这种方式会失效。
this.parentElement.appendChild(overlay)EFWorkbench (shadow host)
shadow root
<div class="canvas-slot-container"> ← 实际DOM父元素
<slot name="canvas"> ← 插槽元素
[EFCanvas被分配到此处] ← 外部DOM,通过插槽分配- 返回Shadow DOM宿主(EFWorkbench),而非内部容器
efCanvas.parentElement - 调用会将覆盖元素放置在宿主的外部DOM中,此时Shadow DOM的CSS和z-index不会生效——覆盖元素会不可见
shadowHost.appendChild(overlay)
The Fix Pattern
修复模式
ts
private insertOverlay(overlay: HTMLElement): void {
const slot = this.assignedSlot;
if (slot) {
// Correctly slotted: insert into the shadow-internal container
slot.parentElement?.appendChild(overlay);
return;
}
if (this.parentElement?.shadowRoot) {
// Shadow DOM exists but slot assignment hasn't happened yet
// (connectedCallback fires before Lit renders shadow DOM).
// Return early; caller should retry via requestAnimationFrame.
return;
}
// Not slotted — plain DOM, safe to use parentElement
this.parentElement?.appendChild(overlay);
}ts
private insertOverlay(overlay: HTMLElement): void {
const slot = this.assignedSlot;
if (slot) {
// 正确插入插槽:插入到Shadow DOM内部容器中
slot.parentElement?.appendChild(overlay);
return;
}
if (this.parentElement?.shadowRoot) {
// Shadow DOM已存在但插槽分配尚未完成
// (connectedCallback在Lit渲染Shadow DOM之前触发)。
// 提前返回;调用者应通过requestAnimationFrame重试。
return;
}
// 未插入插槽——普通DOM,可安全使用parentElement
this.parentElement?.appendChild(overlay);
}The Timing Hazard
时序风险
During HTML parsing, fires before Lit has rendered the shadow DOM. At that point:
connectedCallback- is
this.assignedSlot(slot assignment hasn't happened)null - is non-null (the shadow host)
this.parentElement - is also null (shadow DOM not yet attached)
this.parentElement.shadowRoot
Later in the same microtask tick (after Lit's ):
performUpdate- becomes the
this.assignedSlotelement<slot> - is the internal container
slot.parentElement
Guard pattern:
ts
connectedCallback() {
super.connectedCallback();
this.tryInsertOverlay();
}
private tryInsertOverlay(): void {
if (!this.assignedSlot && this.parentElement?.shadowRoot) {
// Too early — shadow host exists but slot not yet assigned.
// Lit will assign the slot after performUpdate.
requestAnimationFrame(() => this.tryInsertOverlay());
return;
}
this.insertOverlay(this.overlay);
}在HTML解析过程中,会在Lit渲染Shadow DOM之前触发。此时:
connectedCallback- 为
this.assignedSlot(插槽分配尚未完成)null - 不为null(即Shadow DOM宿主)
this.parentElement - 也为null(Shadow DOM尚未附加)
this.parentElement.shadowRoot
在同一个微任务周期的后续阶段(Lit的之后):
performUpdate- 变为
this.assignedSlot元素<slot> - 是内部容器
slot.parentElement
防护模式:
ts
connectedCallback() {
super.connectedCallback();
this.tryInsertOverlay();
}
private tryInsertOverlay(): void {
if (!this.assignedSlot && this.parentElement?.shadowRoot) {
// 时机过早——Shadow DOM宿主已存在但插槽尚未分配。
// Lit会在performUpdate之后完成插槽分配。
requestAnimationFrame(() => this.tryInsertOverlay());
return;
}
this.insertOverlay(this.overlay);
}Summary of Rules
规则总结
| Situation | | | Action |
|---|---|---|---|
| Not slotted | null | null/undefined | Use |
| Correctly slotted | non-null | (any) | Use |
| Too early (Lit not rendered) | null | non-null | Return early, retry via RAF |
| Slotted, parent has no shadow | null | null | Use |
Never use directly when the parent is a shadow DOM host.
this.parentElement| 场景 | | | 操作 |
|---|---|---|---|
| 未插入插槽 | null | null/undefined | 使用 |
| 已正确插入插槽 | non-null | (任意) | 使用 |
| 时机过早(Lit未渲染) | null | non-null | 提前返回,通过RAF重试 |
| 已插入插槽,父元素无Shadow DOM | null | null | 使用 |
当父元素是Shadow DOM宿主时,切勿直接使用。
this.parentElement