From 23bb99212117aba3c2e9fc09af4298214bb577cc Mon Sep 17 00:00:00 2001 From: Kissa Ruokanen Date: Wed, 3 Jun 2026 09:07:17 +0900 Subject: [PATCH] enhance(frontend): scroll position restoration in the drive page and "Files" tab on the user page (#17497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ユーザーページのFilesタブでスクロール位置が正しく復元されない問題を修正 * fix: ドライブページでスクロール位置が保持されない問題を修正 * fix: 変更履歴の文言を調整 * fix: 同じファイルを複数ノートに添付した場合にスクロール位置が先頭要素へ戻る問題を修正 * fix: viewPosition の計算誤りと KeepAlive によるスクロール位置消失を修正 * fix: スクロール位置復元が下端要素の見切れを引き起こす問題を修正 * fix: スクロール位置復元がコンテナ高さ変化時にオーバーシュートする問題を修正 * Update CHANGELOG.md Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> * fix: 最下部スクロール時にスクロール位置のキャプチャが失敗する問題を修正 min-height による空白領域にビューポート中央が入るケースで anchorId が更新 されず、復元が古い位置に飛んでしまう問題を修正した。 アンカー選択条件を「中央を跨ぐ要素のみ」から「上端が中央以下の最も下の要素」 に変更することで、空白領域スクロール時も最後のアイテムを正しくキャプチャできる。 * Update CHANGELOG.md * fix: MkNoteMediaGrid のスクロールアンカーをノートIDとファイルIDの複合キーに変更 Co-Authored-By: Claude Sonnet 4.6 * refactor: anchorIndex による重複アンカー対応を削除 * refactor: unused になった onUnmounted の import を削除 * refactor: querySelectorAll/querySelector にジェネリクスを渡し型キャストを削除 --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- CHANGELOG.md | 2 + packages/frontend/src/components/MkDrive.vue | 3 ++ .../src/components/MkNoteMediaGrid.vue | 3 +- .../composables/use-scroll-position-keeper.ts | 48 ++++++++++++------- packages/frontend/src/pages/drive.vue | 8 +++- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc6569bb2d..d084006562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035) ### Client +- Enhance: ユーザーページのファイルタブでスクロール位置が保持されるように +- Enhance: ドライブページでスクロール位置が保持されるように - Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正 - Fix: 一部の実績が正しく表示されない問題を修正 - Fix: アクセストークン発行時のダイアログのタイトルが「確認コード」となっているのを修正 diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index e0eb858047..0386322de9 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -69,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="(f, i) in foldersPaginator.items.value" :key="f.id" v-anim="i" + :data-scroll-anchor="f.id" :folder="f" :selectMode="select === 'folder'" :isSelected="selectedFolders.some(x => x.id === f.id)" @@ -101,6 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only > - + ): void { let anchorId: string | null = null; + // キャプチャ時のアンカー要素上端のコンテナ上端からの距離 + let anchorContainerLocalY = 0; + let savedScrollTop = 0; let ready = true; watch(scrollContainerRef, (el) => { if (!el) return; - const onScroll = () => { + const captureAnchor = () => { if (!el) return; if (!ready) return; @@ -29,16 +32,17 @@ export function useScrollPositionKeeper(scrollContainerRef: Ref('[data-scroll-anchor]'); for (let i = anchorEls.length - 1; i > -1; i--) { // 下から見た方が速い - const anchorEl = anchorEls[i] as HTMLElement; - const anchorRect = anchorEl.getBoundingClientRect(); - const anchorTop = anchorRect.top; - const anchorBottom = anchorRect.bottom; - if (anchorTop <= viewPosition && anchorBottom >= viewPosition) { + const anchorEl = anchorEls[i]; + const anchorTop = anchorEl.getBoundingClientRect().top; + // 上端が viewPosition 以下の最初の要素(=中央を跨ぐか、中央より上にある中で最も近いもの)を選択する + // 最下部スクロール時に min-height による空白に viewPosition が入った場合も最後のアイテムをキャプチャできる + if (anchorTop <= viewPosition) { anchorId = anchorEl.getAttribute('data-scroll-anchor'); + anchorContainerLocalY = anchorTop - scrollContainerRect.top; break; } } @@ -47,7 +51,10 @@ export function useScrollPositionKeeper(scrollContainerRef: Ref(`[data-scroll-anchor="${CSS.escape(anchorId)}"]`); if (!scrollAnchorEl) return; - scrollAnchorEl.scrollIntoView({ - behavior: 'instant', - block: 'center', - inline: 'center', - }); + const anchorRect = scrollAnchorEl.getBoundingClientRect(); + // anchorContentY: コンテンツ先頭からのアンカー要素上端の距離(scrollTopに依存しない) + const anchorContentY = scrollContainer.scrollTop + anchorRect.top - scrollContainer.getBoundingClientRect().top; + // キャプチャ時と同じ scrollTop になるよう直接セット(コンテナ高さ変化に依存しない) + scrollContainer.scrollTop = anchorContentY - anchorContainerLocalY; }; onDeactivated(() => { + const el = scrollContainerRef.value; + if (el) savedScrollTop = el.scrollTop; ready = false; }); @@ -76,6 +85,13 @@ export function useScrollPositionKeeper(scrollContainerRef: Ref { restore(); + // anchor方式が失敗した場合(anchorIdがnullまたは要素が見つからない場合)の + // フォールバック + const el = scrollContainerRef.value; + if (el && el.scrollTop === 0 && savedScrollTop > 0) { + el.scrollTop = savedScrollTop; + } + ready = true; }, 100); }); diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue index 38939f9503..9ed7097bd3 100644 --- a/packages/frontend/src/pages/drive.vue +++ b/packages/frontend/src/pages/drive.vue @@ -4,17 +4,21 @@ SPDX-License-Identifier: AGPL-3.0-only -->