Carousel に loop / centerItem を追加 - #989
Open
naporin0624 wants to merge 24 commits into
Open
Conversation
loop と defaultScroll の型排他テストで @ts-expect-error が diagnostic の出ない行にあり、テストが何も検証していなかった問題を修正。 JSX の opening tag 直上に移動し、defaultScroll を CarouselDefaultScroll で事前に型付けして literal-widening による偽陽性を排除した。 併せて本ブランチで新規追加した for-of ループが Symbol.iterator の 診断を発生させていたため forEach に変更し、新規の型エラーが 発生しないことを確認した。
effect のみを持つ useCenterReport hook を、children をラップする renderless component (CenterReportProvider) に置き換えて provider 化した。 また Children.toArray の再生成と store 更新による全 item 再レンダーを 抑えるため、slides/slideKeys を useMemo 化し、CarouselItem / CarouselCloneItem / CarouselIndicatorItem / CarouselNavigationButton を memo でラップした。
- 各端の clone を「1.5 viewport を覆う最小枚数 + 1」の実測で決める loopCloneCount を useCarouselScroller から返し、renderClones が参照する - scrollend 非対応環境向けに scroll の途切れを検出する debounce フォールバックを追加し、静止後テレポートを全環境で機能させる
- target ref の受け渡しをやめ forwardRef + useObjectRef で自身の div を観測する - className / data 属性は div 属性としてそのまま透過する
- 強フリックは clone の滑走路(帯域端から物理端まで bandLower ぶん)を scrollend 前に使い切り、静止を待つ間クランプ位置で「詰まり」に見えていた - 端へのクランプを検出したときだけ scroll イベント中に合同位置へ即時 テレポートする computeWallEscape を追加(クランプ時点で motion は 停止済みのため momentum は殺さず、描画も変化しない) - loop 時は overscroll-behavior-x: none にして rubber-band の範囲外座標が 壁クランプ検出と衝突しないようにする
clone 被覆比を 1.5 → 3.5 に引き上げ、1 セットで覆えない要求はセット周回で 埋めるよう computeLoopCloneCount を拡張。実セットが viewport を覆えない 場合は 0 を返しループ不成立として扱う。ページ送りは scrollBy の相対移動を やめ、pendingScrollTarget に目標を積算する scrollTo へ変更し、走行中の テレポートや壁クランプで慣性が死ぬがくつきを根治する。
ユーザー操作でプログラム由来のスクロール意図(初期位置の再適用・ ページ送りの目標位置)を破棄する処理を 1 つの effect に統合し、 静止検出の購読もテレポートと目標破棄で共有する。テレポートは loop 幾何が無ければ no-op のため loop ガードと依存を削除できる。 scroller の ResizeObserver は CarouselItem と同じ observeResize シングルトンに統一し、SSR ガードの重複を解消する。
同一 element の使い回しだと slide に渡された ref が clone のマウント
でも発火し、最後にマウントされた clone を指してしまう。clone は
見た目だけの複製として cloneElement(children, { ref: null }) で
render し、ユーザーの ref が実スライドだけを指すことを保証する。
探索のためだけの counts 配列と ?? n の重複をやめ、「satisfies を 満たす最初の要素までの個数」を返す構造的再帰の汎用関数へ分離する。 述語は添字演算ではなく item そのものを受け、fromTail は toReversed() の反転配列を渡すだけになる。空配列は 0 を返し、 末尾要素は評価せず全個数に丸める仕様を単体テストで固定する。
fromTail 用の反転配列を last の取得と共有し、items[0] / items[n - 1] の添字アクセスを [first] / [last] の分解に置き換える。先頭系は items、 末尾系は reversed という対称が揃い、添字演算が関数から消える。
renderClones の剰余演算による論理 index の復元をやめ、slide と index の対を実セット周回ぶん繰り返した配列から before は末尾、after は 先頭を slice で切り出す。周回は繰り返し配列が担い、multi-set 化前の 宣言的な slice の形に戻る。
挙動修正: - 初期スクロール effect の再武装を撤去し、ユーザー操作後に clone 枚数が 変わっても初期位置へ引き戻さないようにする - indicator の dot など scroll 命令の dispatch でページ送り目標と初期位置 再適用の意図を破棄する(dot は scroller 外で INTERACTION_EVENTS では 拾えない) - 実測 → 状態反映の一連を remeasure に統一し、scroller の ResizeObserver 購読を張りっぱなしにして re-observe 初回通知による偽 onResize を防ぐ。 ref の更新は paint 前に行い、stale 閉包の窓を閉じる - clone 枚数は itemCount === 0 で同期的に 0 へ丸め、children が空に 変わった直後の render でも stale な枚数を消費させない - 範囲外・非整数(NaN 含む)の centerItem は実セット先頭へフォールバック API / ドキュメント: - centerItem を loop 専用として CarouselLoopProps の union へ移動 - CarouselDefaultScroll / CarouselLoopProps を barrel から export - 走行中の scrollStep へ渡る scrollLeft が未到達の目標位置である仕様を JSDoc と MIGRATION に明記 - MIGRATION に id / name 重複・ネスト ref・dot ナビ長距離移動の制約を追記 その他: - clone 帯を useMemo 化し activeIndex 更新のたびの再構築を抑制 - inert 付与を paint 前の layout effect に変更 - fake timers の復元を describe の afterEach に集約 - toReversed は実効ブラウザ下限を上げるため reverse() のコピーに戻す
…nter # Conflicts: # packages/react/src/components/Carousel/MIGRATION.md # packages/react/src/components/Carousel/index.story.tsx # packages/react/src/components/Carousel/index.tsx
naporin0624
had a problem deploying
to
preview-channel
August 18, 2026 11:55 — with
GitHub Actions
Failure
ビルドの babel は .ts も JSX 有効で解析するため、arrow の <T> が JSX タグと誤解釈されて build が落ちる。<T,> は prettier が .ts では 剥がすので、曖昧さのない function 宣言にする。
naporin0624
temporarily deployed
to
preview-channel
August 18, 2026 12:06 — with
GitHub Actions
Inactive
|
Size Change: +20.9 kB (+3.94%) Total Size: 550 kB 📦 View Changed
ℹ️ View Unchanged
|
|
Visit the preview URL for this PR (updated for commit a917571): https://pixiv-charcoal-web--pr989-feat-carousel-loop-c-t392w3b9.web.app (expires Tue, 25 Aug 2026 12:18:24 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 314b26d3adca98a761c7e4d9922ebb206ff024a0 |
main 側で追加された story の DOM snapshot テストが、本ブランチの data-loop 属性(全 Carousel story)と loop 系 4 story の未登録 snapshot で不一致になるため再生成する。
naporin0624
marked this pull request as ready for review
August 18, 2026 12:22
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
やったこと
loopprop を追加: 実セットの前後に clone 帯(各端 3.5 viewport を覆う実測枚数。1 セットで足りなければセット丸ごと周回)を描画し、スクロール静止後に維持帯域へ instant テレポートすることで無限ループを実現centerItemprop を追加: loop 成立時に指定 index のスライドを viewport 中央へ初期配置aria-hidden+ inert 扱いで、indicator / activeIndex は clone 帯でも実 index を報告。cloneElementで ref を剥がした複製として render し、ユーザーの ref は実スライドだけを指すcomputeWallEscape)。クランプ時点で motion は停止済みのため momentum は殺さず、描画も変化しないscrollByから目標積算のscrollToに変更: 走行中の smooth scroll は新しい呼び出しで残距離が破棄される(CSSOM View 仕様)ため、連打時は未到達の目標を起点に絶対座標を積算する。目標は静止・ユーザー操作・テレポートで破棄overscroll-behavior-x: noneにして rubber-band の範囲外座標が壁クランプ検出と衝突しないようにするobserveResizeシングルトンに統一動作確認環境
チェックリスト
🤖 Generated with Claude Code
https://claude.ai/code/session_01XqPDaERdGYNNhELPnFbexe