サイト削除のたびに一覧が先頭に戻る UX バグ — keepScroll 引数の適用漏れと横断修正
症状
WordPress Maintenance Manager のサイト一覧で、サイトを削除するたびに画面が最上部へスクロールする問題が発生した。管理対象が多い環境では、リストの途中にある項目を削除するたびに先頭に飛ばされ、次の操作のために毎回スクロールし直す手間が生じていた。
同様の「先頭戻り」はサイト削除だけでなく、D&D による並び替え・サムネイル取得・カテゴリ削除・タグ削除・メンテナンス完了後にも発生していた。
原因:keepScroll 引数の省略
fetchSites() 関数は keepPage と keepScroll の 2 つの引数を受け取る。keepScroll=true を渡すと renderPage 内の scrollTo({top: 0}) が抑止される仕組みは、v1.6.6 の時点ですでに実装済みだった。
async function fetchSites(keepPage = false, keepScroll = false) {
// ...
renderPage(keepPage, keepScroll);
}
function renderPage(keepPage, keepScroll) {
if (!keepScroll) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// ...
}
スクロール維持のロジック自体は正しく動いていた。問題は、削除・並び替え・各種操作後の fetchSites() 呼び出しで keepScroll 引数を省略していたことにある。省略するとデフォルト値 false が渡り、処理のたびに scrollTo({top: 0}) が発火していた。
// 修正前: keepScroll を省略(false が渡り、先頭に飛ぶ)
fetchSites(true);
// 修正後: keepScroll=true を明示
fetchSites(true, true);
6 箇所の横断修正
同じ省略が計 6 箇所に存在していた。
| 呼び出し箇所 | 修正前 | 修正後 |
|---|---|---|
| deleteSiteByIndex(サイト削除) | fetchSites(true) |
fetchSites(true, true) |
| D&D 並び替え完了後 | fetchSites(true) |
fetchSites(true, true) |
| サムネイル取得後(モーダル裏) | fetchSites(true) |
fetchSites(true, true) |
| カテゴリー削除後 | fetchSites(true) |
fetchSites(true, true) |
| タグ削除後 | fetchSites(true) |
fetchSites(true, true) |
| メンテナンス完了後 | fetchSites(true) |
fetchSites(true, true) |
特にサムネイル取得はモーダルの背後で非同期に発火するため、ユーザーが操作していない間に一覧が先頭へ移動する挙動として現れていた。メンテナンス完了後についてはストリーミング中に scrollToTop を別途抑止する仕組みがあったが、完了時の fetchSites 呼び出しがその意図と不整合になっていた。今回の修正で 6 箇所の挙動が統一された。
初期起動は対象外
アプリ起動時の最初の fetchSites() 呼び出しは意図的に対象外とした。起動時はページ 1・スクロール位置 0 にリセットするのが正しい挙動であり、keepScroll=true を付けるべきではない。
削除後に現在ページが範囲外になるケースについては、filterSites 内に Math.min によるページ範囲クランプが入っているため、安全に処理される。
学び:安全機構を新しい呼び出し箇所に引き継ぐ
今回のバグは「機能そのものが壊れていた」のではなく、「正しく動く安全機構が、後から追加された呼び出し箇所に引き継がれていなかった」パターンだった。
スクロール位置維持機構は v1.6.6 で設計した。その後、サムネイル取得・カテゴリ削除・タグ削除・メンテ完了後などを追加した際に、それぞれの fetchSites 呼び出しが既存の引数設計を踏まえないまま省略した状態で書かれ、動作はするが意図した挙動にならない呼び出しが積み重なった。
新しい呼び出し箇所を追加するときに「同じ関数の既存呼び出しがどの引数を渡しているか」を確認する習慣が、この種の適用漏れを防ぐ一つの手がかりになる。