ループ中の carousel が非表示タブ内で誤表示するバグを修正

shoelace-style/webawesome

リード文
<wa-carousel>loop 属性が有効な状態で、非アクティブなタブパネル内に初期化された場合に、ロード直後に誤ったスライドが一瞬表示されるバグを解消しました。これにより、タブ切り替え時の視覚的ブリックがなくなり、期待通りのスライドが初回表示されます。

背景

このバグは、非表示コンテナ内でループ carousel が初期化された際に発生していました。loop が有効なときは、先頭と末尾にクローンスライドが配置され、内部で自動的にスクロールして正しいスライドへ移動します。非表示状態(例: 非アクティブな <wa-tab-panel>)では要素の幅・高さが 0 になるため、スクロールが実行されず最後のクローンスライドが表示されることが報告されました#1163

結果として、ページネーションのドットは最初がアクティブでも、コンテンツは最後のスライドが一瞬見えてしまうという不整合が起きました。ユーザーは「スライドがフラッシュした」ように感じ、操作感が損なわれました。この問題はテストランナーでは再現できなかったため、手動テストでのみ確認されていました。

技術的な変更

CSS 側で表示制御を導入し、誤表示を防止しました。packages/webawesome/src/components/carousel/carousel.styles.ts に以下のスタイルを追加し、.slides.pagination にフェードトランジションを設定しました。

  .slides,
  .pagination {
    transition: opacity var(--wa-transition-fast) ease;
  }

  .slides-awaiting-position,
  .pagination-awaiting-position {
    opacity: 0;
    transition: none;
  }

コンポーネントロジックに状態フラグと可視化タイミングを組み込みました。WaCarousel クラスに @state() awaitingInitialPosition = false; を追加し、firstUpdated() で非表示で開始したか判定してフラグを立てます。

// 追加された状態プロパティ
@state() awaitingInitialPosition = false;

ResizeObserver のコールバックで位置決定と表示切替を実装しました。コンテナが可視化された瞬間に goToSlide(this.activeSlide, 'auto') で正しい位置にスクロールし、二重 requestAnimationFrame 後に awaitingInitialPositionfalse に戻すことで、スライドとページネーションがフェードインします。

const startedHidden = this.loop && !this.scrollContainer?.clientWidth && !this.scrollContainer?.clientHeight;
if (startedHidden) {
  this.awaitingInitialPosition = true;
}
...
this.resizeObserver = new ResizeObserver(() => {
  if (this.scrollContainer?.clientWidth || this.scrollContainer?.clientHeight) {
    this.goToSlide(this.activeSlide, 'auto');
    this.synchronizeSlides();
    this.resizeObserver?.disconnect();
    this.resizeObserver = undefined;
    if (this.awaitingInitialPosition) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          this.awaitingInitialPosition = false;
        });
      });
    }
  }
});

結果として、非表示タブ内で loop 付き carousel が表示されるときにスライドがフラッシュしなくなります。CSS の透明度切替と JavaScript の状態管理が組み合わさり、ユーザーに見えるまでに正しい位置へ自動スクロールが完了します。

設計判断

表示制御は CSS に委譲し、ロジックは最小限に留めました。awaitingInitialPosition というシンプルなブールフラグだけで状態遷移を管理し、余計なレイアウト計算やフラグの増殖を防いでいます。この選択は、既存のコンポーネント構造への侵入度を低く保つという設計意図に沿っています。

可視化のタイミングは ResizeObserverrequestAnimationFrame で正確に捕捉しました。要素が実際に描画可能になるまで待機し、二重フレームで状態を解除することで、ブラウザのペイントサイクルと同期させています。これにより、フラッシュが残る余地を排除しつつ、追加テストを書かずに実装できました。

トレードオフとして、テスト自動化が未実装である点が残ります。バグは手動で再現できるが CI 上では検証できないため、将来的に視覚的回帰テストを導入する余地があります。それ以外は、機能追加や他コンポーネントへの影響を最小化できた点が評価できます。

まとめ

この PR は、loop が有効な <wa-carousel> が非表示タブ内で初期化された際に起きる「誤スライドフラッシュ」バグを、CSS の透明度制御とコンポーネント内部の状態管理で解消しました。最小限の侵入でユーザー体験を改善し、既存 API との互換性も維持しています。今後は同様の可視性依存不具合を防ぐテストパターンを追加すると、品質保証がさらに向上します。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
c5c0f50d

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断、まとめの各セクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは ```:言語:ファイルパス`` の形式で正しく記述されていますが、IssueへのリンクがプレーンURLになっており、指定の [#ID](URL) 形式になっていません。

対象読者への適合性 ✓ PASS

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

専門的なフロントエンド開発者向けの内容で、不要な初心者向け解説はありません。

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

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

各セクションは総論パラグラフ→各論パラグラフ→結論パラグラフで構成され、段落はトピックセンテンスで始まり、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffの追加部分と実質的に一致しており、抜粋や省略も内容理解に支障ありません。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

ResizeObserver、requestAnimationFrame、loop などの用語は正しく使用されており、PR記述と整合しています。

説明の技術的正確性 ✓ PASS

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

バグの原因と修正内容が PR の説明とコード変更に基づいて正確に記述されています。

事実の突合 ✓ PASS

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

全ての主張は PR のタイトル、説明、Issue 番号に裏付けられており、憶測や捏造はありません。

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

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

PR 番号や Issue 番号、ファイルパスなどの数値・固有名詞は正確です。

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

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

記事タイトルは PR の内容(非表示タブ内の loop 設定バグ修正)を適切に日本語化しています。

外部知識の正確性 ✓ PASS

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

LTS、EOL、リリース日程等、外部知識は一切含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致しています。