shadow-dom-overlay-insertion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Shadow 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.parentElement.appendChild(overlay)
. This breaks when A is slotted inside a shadow DOM host (e.g. EFWorkbench).
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
  • efCanvas.parentElement
    returns the shadow host (EFWorkbench), not the internal container
  • Calling
    shadowHost.appendChild(overlay)
    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
当组件A(例如EFCanvas)尝试在DOM中自身旁边插入一个覆盖元素时,通常会使用
this.parentElement.appendChild(overlay)
。但当A被插入到Shadow DOM宿主(例如EFWorkbench)的插槽中时,这种方式会失效。
EFWorkbench (shadow host)
  shadow root
    <div class="canvas-slot-container">   ← 实际DOM父元素
      <slot name="canvas">                ← 插槽元素
        [EFCanvas被分配到此处]       ← 外部DOM,通过插槽分配
  • efCanvas.parentElement
    返回Shadow DOM宿主(EFWorkbench),而非内部容器
  • 调用
    shadowHost.appendChild(overlay)
    会将覆盖元素放置在宿主的外部DOM中,此时Shadow DOM的CSS和z-index不会生效——覆盖元素会不可见

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,
connectedCallback
fires before Lit has rendered the shadow DOM. At that point:
  • this.assignedSlot
    is
    null
    (slot assignment hasn't happened)
  • this.parentElement
    is non-null (the shadow host)
  • this.parentElement.shadowRoot
    is also null (shadow DOM not yet attached)
Later in the same microtask tick (after Lit's
performUpdate
):
  • this.assignedSlot
    becomes the
    <slot>
    element
  • slot.parentElement
    is the internal container
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解析过程中,
connectedCallback
会在Lit渲染Shadow DOM之前触发。此时:
  • this.assignedSlot
    null
    (插槽分配尚未完成)
  • this.parentElement
    不为null(即Shadow DOM宿主)
  • this.parentElement.shadowRoot
    也为null(Shadow DOM尚未附加)
在同一个微任务周期的后续阶段(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
assignedSlot
parentElement.shadowRoot
Action
Not slottednullnull/undefinedUse
parentElement.appendChild
Correctly slottednon-null(any)Use
assignedSlot.parentElement.appendChild
Too early (Lit not rendered)nullnon-nullReturn early, retry via RAF
Slotted, parent has no shadownullnullUse
parentElement.appendChild
Never use
this.parentElement
directly when the parent is a shadow DOM host.
场景
assignedSlot
parentElement.shadowRoot
操作
未插入插槽nullnull/undefined使用
parentElement.appendChild
已正确插入插槽non-null(任意)使用
assignedSlot.parentElement.appendChild
时机过早(Lit未渲染)nullnon-null提前返回,通过RAF重试
已插入插槽,父元素无Shadow DOMnullnull使用
parentElement.appendChild
当父元素是Shadow DOM宿主时,切勿直接使用
this.parentElement