非表示コンテナ内のカルーセルでスライドが操作不能になるバグを修正

shoelace-style/webawesome

<wa-carousel> を非表示のタブパネル内に配置すると、タブを開いてもスライドのコンテンツが操作できないバグが修正されました。ResizeObserver を用いた一度限りの再同期処理により、レイアウト確定後に正しく inert 属性が解除されるようになります。

背景

<wa-carousel>display: none のコンテナ(例: 非アクティブなタブパネル)内に配置すると、スライド内のボタンやリンクが一切クリックできないという問題が発生していました。この問題は #1164 として報告されていました。

原因は初期化タイミングの競合にあります。firstUpdated() が呼ばれて initializeSlides() が実行される時点では、親コンテナが display: none のためレイアウト計算が行われていません。initializeSlides() が内部で呼ぶ synchronizeSlides()root: this.scrollContainer を指定した IntersectionObserver を生成しますが、レイアウト寸法がゼロのため全スライドが「非交差」と判定され、全スライドに inert 属性が付与されます。

問題を深刻にしているのは、IntersectionObserver がその後切断されることです。タブが後から表示されても再同期をトリガーするイベントが存在しないため、ユーザーがカルーセルを手動で操作(スクロールなど)するまで inert 属性は残り続けます。視覚的には正常に見えるにもかかわらず、スライド内のすべての対話要素が無効化されるという、見つけにくいバグでした。

技術的な変更

firstUpdated() に一度限りの ResizeObserver を追加し、カルーセルが実際に表示されてレイアウト寸法を持った時点で synchronizeSlides() を再実行する仕組みが導入されました。

追加コード(carousel.ts):

this.resizeObserver = new ResizeObserver(() => {
  if (this.scrollContainer?.clientWidth || this.scrollContainer?.clientHeight) {
    this.synchronizeSlides();
    this.resizeObserver?.disconnect();
    this.resizeObserver = undefined;
  }
});
this.resizeObserver.observe(this);

scrollContainerclientWidth または clientHeight が非ゼロになった瞬間を検知し、synchronizeSlides() を呼び出してスライドの inert 状態を修正します。その後、オブザーバーは即座に自分自身を切断し、undefined に設定されます。これにより、再同期は厳密に1回だけ実行されます。

安全網として、disconnectedCallback() にも resizeObserver?.disconnect() が追加されています。カルーセルが表示される前に DOM から削除された場合でも、オブザーバーが確実にクリーンアップされます。

disconnectedCallback(): void {
  super.disconnectedCallback();
  this.mutationObserver?.disconnect();
  this.resizeObserver?.disconnect(); // 追加
}

最初から表示されているカルーセルへの影響はありません。ResizeObserver は非ゼロ寸法で即座に発火し、synchronizeSlides() を1回呼び出して切断するため、既存の動作と実質的に同等です。

設計判断

ResizeObserver を一度限りのトリガーとして使用する方式 が採用されました。

このアプローチが合理的な理由は、問題の本質が「表示タイミングの検知」にあるからです。display: none から display: block への変化を直接観察するAPIは存在しませんが、その副作用としてレイアウト寸法が非ゼロになるという事実を ResizeObserver で捉えられます。既存の synchronizeSlides() ロジックを再利用するだけで修正が完結しており、コンポーネントの状態管理の複雑さを最小限に抑えています。

オブザーバーが役割を果たした後すぐに切断・破棄される設計も重要です。resizeObserver フィールドを undefined に戻すことで、参照を明示的に解放し、ライフサイクル管理を明確にしています。

テストでは、display: none のラッパー要素内にカルーセルを配置し、container.style.display = 'block' で表示に切り替えた後、ResizeObserverIntersectionObserver のコールバックが落ち着くまで待機してからアサートする形式が採られています。これにより、今後の変更でこの挙動がリグレッションした場合に確実に検出できます。

まとめ

この修正は、コンポーネントの初期化タイミングとブラウザのレイアウトサイクルのずれという本質的な課題に対して、既存APIの組み合わせで最小限の変更として対処した好例です。ResizeObserver の「寸法がゼロでなくなった瞬間」というセマンティクスを活用することで、display: none から display: block への変化という直接観察できないイベントを間接的に捉えています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
520dafb5

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の3部構成が記事全体に適用されており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```typescript:path)とGitHubのIssue/PRへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

`ResizeObserver`やWeb Componentsのライフサイクル(`firstUpdated`)など、専門的なトピックを前提としており、対象読者であるエンジニアに適した技術レベルで書かれています。

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

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

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まっています。1段落1トピックの原則も守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されている`carousel.ts`への変更点は、提供されたDiff情報と完全に一致しており、正確にコードが引用されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`ResizeObserver`, `IntersectionObserver`, `inert`, `firstUpdated`などの技術用語が、文脈に応じて正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

「`display: none`のコンテナ内ではレイアウト寸法がゼロのため、`IntersectionObserver`が全スライドを`inert`にしてしまう」という原因の説明が、PRのDescriptionと一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の原因、解決策、テスト内容など)は、PRのDescriptionやDiff内のコードで裏付けられており、ハルシネーション(創作)は見られません。

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

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

PR番号(#2133)と関連Issue番号(#1164)が正確に記載されています。

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

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

記事のタイトル「非表示コンテナ内のカルーセルでスライドが操作不能になるバグを修正」は、PRの主題をユーザーへの影響という観点から分かりやすく要約しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

バージョンサポート状況やリリース日程など、PR情報に基づかない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

「バグが修正されました」「問題が発生していました」など、過去の事象に対する時間表現が正確に使用されています。