アニメーション中の状態不整合をジェネレーション番号で解決:Tree・Detailsのバグ修正

shoelace-style/webawesome

wa-tree-itemwa-details コンポーネントで、展開・折り畳みアニメーション中に連続クリックすると視覚状態と実際の表示が乖離するバグが修正されました。アニメーションジェネレーション番号を導入することで、古いアニメーション完了後の後処理を無効化し、最終状態の整合性を保証します。

背景

wa-tree-item の展開ボタンをダブルクリックまたはトリプルクリックすると、展開/折り畳みインジケーターが実際の子要素の表示状態と乖離するバグが #2258 で報告されていました。例えばインジケーターは「展開済み」を示しているにもかかわらず、子要素リストは非表示のままになるという状態不整合です。

この問題の根本原因は、アニメーション処理が非同期(async/await)で実装されているにもかかわらず、複数のアニメーションが並行して起動された場合の制御機構が存在しなかったことにあります。展開アニメーションの完了後に hidden = false を設定するコードが、後から起動された折り畳みアニメーションの完了後にも実行されてしまい、最終的な状態が不定になっていました。同様の問題は wa-details コンポーネントにも存在しており、本PRで両コンポーネントが同時に修正されています。

技術的な変更

両コンポーネントに共通する修正方針は、アニメーションジェネレーション番号を用いた「後勝ち制御」の導入です。アニメーション処理が開始されるたびにカウンターをインクリメントし、処理完了時にカウンターの値が変化していれば、それ以降の後処理をスキップします。

wa-tree-item では、クラスフィールドとして animationGeneration を追加し、animateCollapseanimateExpand の両メソッドに generation パラメータを追加しました。

変更前:

private async animateCollapse() {
  // ...
  // アニメーション完了後、無条件に状態を確定
  this.childrenContainer.hidden = true;
  this.dispatchEvent(new WaAfterCollapseEvent());
}

変更後:

private animationGeneration = 0;

private async animateCollapse(generation: number) {
  // ...
  // アニメーション完了後、世代番号が変化していればスキップ
  if (this.animationGeneration !== generation) {
    return;
  }
  this.childrenContainer.hidden = true;
  this.dispatchEvent(new WaAfterCollapseEvent());
}

wa-details でも同様のパターンが適用されています。handleOpenChange の先頭で animationGeneration をインクリメントしてスナップショットを取得し、展開・折り畳みアニメーション完了後のスタイル確定処理(body.style.height = 'auto' / body.style.height = '0')の前でジェネレーションを照合します。

private animationGeneration = 0;

@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
  this.animationGeneration++;
  const generation = this.animationGeneration;

  if (this.open) {
    // ... 展開アニメーション ...
    if (this.animationGeneration !== generation) {
      return;
    }
    this.body.style.height = 'auto';
  } else {
    // ... 折り畳みアニメーション ...
    if (this.animationGeneration !== generation) {
      return;
    }
    this.body.style.height = '0';
  }
}

加えて、修正前のコードには wa-details の折り畳み時に this.body.style.height = 'auto' を設定するバグが存在していました(正しくは '0')。この修正もあわせて含まれています。

テストは両コンポーネントともに3つのシナリオを追加しています。「折り畳み中に展開で中断」「展開中に折り畳みで中断」「中断後の最終イベント発火が1回のみであること」の3パターンを、タイマーで意図的に中間タイミングを作り出して検証しています。

設計判断

ジェネレーション番号による後勝ち制御は、非同期アニメーションの競合状態を解決する軽量なパターンです。この方式は「最後に発行された意図が最終状態を決定する」というUIの直感的な期待に沿っており、ユーザーがすばやく操作を繰り返した場合に最後の操作結果が確実に反映されます。

また、本PRには wa-details のドキュメントへの追加として、open 属性で初期展開状態を設定する使用例も含まれています。これはコンポーネントの既存機能の文書化であり、バグ修正とあわせてメンテナンスを行ったものです。

まとめ

アニメーションジェネレーション番号という軽量なパターンにより、wa-tree-itemwa-details の両コンポーネントで非同期アニメーションの競合状態が解消されました。DOM状態の確定処理を世代番号でガードするこのアプローチは、既存の非同期アニメーション構造を大きく変えずに競合問題を封じ込める再利用可能な設計パターンとして、他のアニメーション付きコンポーネントにも適用できる知見を示しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
afefbdc5

この記事は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/to/file.ts)とGitHubのPR/Issueリンク記法([#123](URL))が、ガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

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

非同期処理や競合状態といった概念を前提としており、専門知識を持つエンジニアという対象読者に適した技術レベルで記述されています。冗長な初心者向けの説明はありません。

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

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

各セクションが総論→各論の構成になっており、各段落がトピックセンテンスで始まっています。1段落1トピックの原則が守られ、段落の長さも適切であるため、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、提供されたDiff情報と正確に一致しています。`animationGeneration`の導入や、`wa-details`の`height`設定に関するバグ修正の指摘もDiff内容と整合性が取れています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「アニメーションジェネレーション番号」「後勝ち制御」「競合状態」など、変更内容を的確に表現する技術用語が正しく使用されています。特に「ジェネレーション番号」はコード内の変数名とも一致しており、理解を助けています。

説明の技術的正確性 ✓ PASS

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

アニメーションの競合による状態不整合という問題の根本原因と、ジェネレーション番号を用いた解決策の技術的な説明が論理的かつ正確です。Diffで追加されたテストコードの内容とも一致しています。

事実の突合 ✓ PASS

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

記事内のすべての主張(対象コンポーネント、関連Issue、修正内容、テストシナリオ、ドキュメント更新など)は、PRのDescriptionやDiff情報によって裏付けられています。ハルシネーション(捏造)は見られません。

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

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

PR番号(#2261)とIssue番号(#2258)が正確に記載されています。

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

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

記事のタイトルは、PRの変更内容(TreeとDetailsのバグ修正)と、その技術的な解決策(ジェネレーション番号)を的確に要約しており、PR内容と完全に一致しています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

「バグが修正されました」といった過去形の使用は、完了したPRの内容を記述するものとして適切であり、時間表現の歪曲はありません。