Shadow DOM境界を超えたPopoverのクリック検出を修正

shoelace-style/webawesome

Web ComponentsのShadow DOM内で wa-popover を使用すると、ポップオーバー内の要素をクリックした際に意図せず閉じてしまう問題が修正されました。この変更により、Shadow DOM境界を越えたイベント伝播を正しく追跡できるようになります。

背景

wa-popover コンポーネントは、ユーザーがポップオーバーの外側をクリックしたときに閉じる機能を持っています。しかし、このコンポーネントを別のカスタム要素のShadow DOM内に配置すると、ポップオーバー内部の textareabutton などの要素をクリックしただけで、ポップオーバーが即座に閉じてしまう問題が報告されていました。#1969 がこの問題を報告しています。

問題の原因は handleDocumentClick() メソッドにありました。このメソッドは Element.closest() を使用してクリックイベントのターゲットが自身の要素内かどうかを判定していましたが、closest() はShadow DOM境界を越えて要素を探索できないため、ネストされたShadow DOM内では常に null を返していました。

この制約により、Shadow DOM内に配置されたポップオーバーは、内部要素のクリックを「外側のクリック」と誤認識し、意図せず閉じる動作をしていました。

技術的な変更

packages/webawesome/src/components/popover/popover.tshandleDocumentClick() メソッドが、closest() から composedPath() を使用した実装に変更されました。

変更前:

private handleDocumentClick = (event: PointerEvent) => {
  const target = event.target as HTMLElement;

  // Ignore clicks on the anchor so it will be closed by the anchor's click handler
  if (this.anchor && event.composedPath().includes(this.anchor)) {
    return;
  }

  // Detect when clicks occur outside the popover
  if (target.closest('wa-popover') !== this) {
    this.open = false;
  }
};

変更後:

private handleDocumentClick = (event: PointerEvent) => {
  // Ignore clicks on the anchor so it will be closed by the anchor's click handler
  if (this.anchor && event.composedPath().includes(this.anchor)) {
    return;
  }

  // Detect when clicks occur outside the popover (using composedPath to traverse shadow DOM boundaries)
  if (!event.composedPath().includes(this)) {
    this.open = false;
  }
};

event.composedPath() はイベントが伝播した全ての要素を配列で返すメソッドで、Shadow DOM境界を越えて要素を追跡できます。includes(this) による判定により、クリックイベントのパスに自身が含まれていない場合のみポップオーバーを閉じるようになりました。

不要になった target 変数の宣言も削除され、コードが簡潔になっています。この変更により、ポップオーバー内部のどの要素がクリックされても、そのイベントパスには必ずポップオーバー自身が含まれるため、誤って閉じることがなくなります。

設計判断

既存のアンカー要素チェックと同じパターンを採用する方式 が選択されました。

PRの説明によると、同じメソッド内で既にアンカー要素の判定に event.composedPath().includes(this.anchor) が使用されていました。今回の修正は、この既存パターンをポップオーバー自身の判定にも適用したものです。

このアプローチは、closest() の制約を回避しつつ、ファイル内の既存コードと一貫性を保つための選択です。同一のメソッド内でイベント追跡の方法を統一することで、コードの保守性が向上しています。

まとめ

本PRは、Shadow DOM環境下でのポップオーバーの誤動作を修正した変更です。closest() から composedPath() への変更により、Shadow DOM境界を越えたイベント追跡が可能になり、ネストされたWeb Components内でもポップオーバーが正しく動作するようになりました。この修正は、既存のアンカー判定ロジックとの一貫性を保ちながら、Shadow DOMのカプセル化を尊重した実装といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確で、リード文、背景、技術詳細、設計判断、まとめの各要素が論理的に配置されています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト、GitHubのIssue/PRリンク記法ともに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Shadow DOMやイベント伝播に関する専門的な内容でありながら、過度な説明がなく、対象読者であるエンジニアに適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論→結論で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、提供されたDiff情報を正確に反映しています。ファイル名も一致しており、変更点が明確に示されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`closest()`や`composedPath()`、Shadow DOMといった技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

`closest()`がShadow DOM境界を越えられないという問題点と、`composedPath()`による解決策の説明は技術的に正確で論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのタイトル、Description、Diffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2014)および関連するIssue番号(#1969)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題「Shadow DOM内でのPopoverの意図しないクローズ問題の修正」を技術的に的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に完全に準拠しており、バージョン情報やサポート状況といったPR外の知識の持ち込みはありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

完了した変更について「〜されました」という過去形の表現が使われており、時間表現はPRの状況と一致しています。