アニメーション中の状態不整合をジェネレーション番号で解決:Tree・Detailsのバグ修正
wa-tree-item と wa-details コンポーネントで、展開・折り畳みアニメーション中に連続クリックすると視覚状態と実際の表示が乖離するバグが修正されました。アニメーションジェネレーション番号を導入することで、古いアニメーション完了後の後処理を無効化し、最終状態の整合性を保証します。
背景
wa-tree-item の展開ボタンをダブルクリックまたはトリプルクリックすると、展開/折り畳みインジケーターが実際の子要素の表示状態と乖離するバグが #2258 で報告されていました。例えばインジケーターは「展開済み」を示しているにもかかわらず、子要素リストは非表示のままになるという状態不整合です。
この問題の根本原因は、アニメーション処理が非同期(async/await)で実装されているにもかかわらず、複数のアニメーションが並行して起動された場合の制御機構が存在しなかったことにあります。展開アニメーションの完了後に hidden = false を設定するコードが、後から起動された折り畳みアニメーションの完了後にも実行されてしまい、最終的な状態が不定になっていました。同様の問題は wa-details コンポーネントにも存在しており、本PRで両コンポーネントが同時に修正されています。
技術的な変更
両コンポーネントに共通する修正方針は、アニメーションジェネレーション番号を用いた「後勝ち制御」の導入です。アニメーション処理が開始されるたびにカウンターをインクリメントし、処理完了時にカウンターの値が変化していれば、それ以降の後処理をスキップします。
wa-tree-item では、クラスフィールドとして animationGeneration を追加し、animateCollapse と animateExpand の両メソッドに 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-item と wa-details の両コンポーネントで非同期アニメーションの競合状態が解消されました。DOM状態の確定処理を世代番号でガードするこのアプローチは、既存の非同期アニメーション構造を大きく変えずに競合問題を封じ込める再利用可能な設計パターンとして、他のアニメーション付きコンポーネントにも適用できる知見を示しています。