Preserve browse scroll positions exactly when returning from details
Skip ensure-visible and synced-row animations on focus restore, and snapshot/restore panel scrollTop plus synced-row scrollLeft around detail navigation so the browse page comes back pixel-identical.
This commit is contained in:
+36
-4
@@ -211,6 +211,7 @@ import {
|
||||
} from "./api"
|
||||
import { useSettings } from "./composables/useSettings"
|
||||
import { useKeyboardNavigation, setActiveNavigationScope } from "./composables/useKeyboardNavigation"
|
||||
import type { SyncedRowScrollSnapshot } from "./composables/useKeyboardNavigation"
|
||||
import { useMediaWebSocket } from "./composables/useMediaWebSocket"
|
||||
import Header from "./components/Header.vue"
|
||||
import CollageHero from "./components/CollageHero.vue"
|
||||
@@ -219,7 +220,13 @@ import MediaDetail from "./components/MediaDetail.vue"
|
||||
import type { SearchResultItem, SearchResponseMessage } from "./search-worker"
|
||||
|
||||
// Initialize keyboard navigation
|
||||
const { getFocusState, restoreFocusState, focusElement } = useKeyboardNavigation()
|
||||
const {
|
||||
getFocusState,
|
||||
restoreFocusState,
|
||||
focusElement,
|
||||
snapshotSyncedRowScroll,
|
||||
restoreSyncedRowScroll,
|
||||
} = useKeyboardNavigation()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -674,6 +681,23 @@ const searchCategories = ref<{ name: string; items: MediaItem[] }[]>([])
|
||||
const focusStateMap = new Map<string, { row: number; col: number }>()
|
||||
// Track the last viewed item ID to restore focus to the right card
|
||||
const lastViewedItemId = ref<string | null>(null)
|
||||
let browseScrollSnapshot: { panelTop: number; rows: SyncedRowScrollSnapshot } | null = null
|
||||
|
||||
function captureBrowseScrollSnapshot() {
|
||||
browseScrollSnapshot = {
|
||||
panelTop: browsePanelRef.value?.scrollTop ?? 0,
|
||||
rows: snapshotSyncedRowScroll(),
|
||||
}
|
||||
}
|
||||
|
||||
function restoreBrowseScrollSnapshot() {
|
||||
if (!browseScrollSnapshot) return
|
||||
if (browsePanelRef.value) {
|
||||
browsePanelRef.value.scrollTop = browseScrollSnapshot.panelTop
|
||||
}
|
||||
restoreSyncedRowScroll(browseScrollSnapshot.rows)
|
||||
browseScrollSnapshot = null
|
||||
}
|
||||
|
||||
// Save current focus state for a page
|
||||
function saveFocusForPage(page: string) {
|
||||
@@ -693,22 +717,24 @@ function restoreFocusForPage(page: string) {
|
||||
if (lastViewedItemId.value) {
|
||||
// Use nextTick + timeout to ensure DOM is updated after navigation
|
||||
setTimeout(() => {
|
||||
restoreBrowseScrollSnapshot()
|
||||
const itemId = lastViewedItemId.value
|
||||
// Find the element with matching item id
|
||||
const element = document.querySelector(`[data-item-id="${itemId}"]`) as HTMLElement | null
|
||||
if (element) {
|
||||
focusElement(element)
|
||||
focusElement(element, { preserveScroll: true })
|
||||
lastViewedItemId.value = null
|
||||
return
|
||||
}
|
||||
// Fallback to saved focus state
|
||||
const state = focusStateMap.get(page)
|
||||
restoreFocusState(state || null)
|
||||
restoreFocusState(state || null, { preserveScroll: true })
|
||||
lastViewedItemId.value = null
|
||||
}, 100)
|
||||
} else {
|
||||
restoreBrowseScrollSnapshot()
|
||||
const state = focusStateMap.get(page)
|
||||
restoreFocusState(state || null)
|
||||
restoreFocusState(state || null, { preserveScroll: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,6 +1051,12 @@ function showDetail(item: MediaItem) {
|
||||
const currentPage = route.path === "/series" ? "series" : "movies"
|
||||
saveFocusForPage(currentPage)
|
||||
|
||||
// Capture the exact browse scroll positions to restore on return,
|
||||
// but only when leaving the browse page (not for detail-to-detail hops)
|
||||
if (!isDetailOpen.value) {
|
||||
captureBrowseScrollSnapshot()
|
||||
}
|
||||
|
||||
// Check if there are matched episodes to focus on
|
||||
if (item.type === "series" && item.searchMatchInfo?.matchedEpisodes?.length) {
|
||||
const firstMatch = item.searchMatchInfo.matchedEpisodes[0]
|
||||
|
||||
@@ -702,7 +702,7 @@ function findNextElement(
|
||||
}
|
||||
}
|
||||
|
||||
function focusElement(element: HTMLElement | null) {
|
||||
function focusElement(element: HTMLElement | null, options?: { preserveScroll?: boolean }) {
|
||||
if (!element) return
|
||||
if (!isElementInActiveScope(element)) return
|
||||
|
||||
@@ -714,8 +714,10 @@ function focusElement(element: HTMLElement | null) {
|
||||
element.classList.add("nav-focused")
|
||||
element.focus({ preventScroll: true })
|
||||
|
||||
ensureElementVisibleVertically(element)
|
||||
syncRowsToElement(element)
|
||||
if (!options?.preserveScroll) {
|
||||
ensureElementVisibleVertically(element)
|
||||
syncRowsToElement(element)
|
||||
}
|
||||
|
||||
focusedElement.value = element
|
||||
}
|
||||
@@ -727,13 +729,16 @@ function getFocusState(): { row: number; col: number } | null {
|
||||
return { row, col }
|
||||
}
|
||||
|
||||
function restoreFocusState(state: { row: number; col: number } | null) {
|
||||
function restoreFocusState(
|
||||
state: { row: number; col: number } | null,
|
||||
options?: { preserveScroll?: boolean },
|
||||
) {
|
||||
if (!state) return
|
||||
|
||||
const target = findElementAt(state.row, state.col)
|
||||
if (target) {
|
||||
setTimeout(() => {
|
||||
focusElement(target.element)
|
||||
focusElement(target.element, options)
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
@@ -893,6 +898,32 @@ export function installKeyboardNavigation() {
|
||||
})
|
||||
}
|
||||
|
||||
export interface SyncedRowScrollSnapshot {
|
||||
rows: [HTMLElement, number][]
|
||||
offset: number
|
||||
targetOffset: number
|
||||
}
|
||||
|
||||
function snapshotSyncedRowScroll(): SyncedRowScrollSnapshot {
|
||||
return {
|
||||
rows: getSyncedRows().map((row) => [row, row.scrollLeft]),
|
||||
offset: syncedRowsCurrentOffset,
|
||||
targetOffset: syncedRowsTargetOffset,
|
||||
}
|
||||
}
|
||||
|
||||
function restoreSyncedRowScroll(snapshot: SyncedRowScrollSnapshot) {
|
||||
stopSyncedRowAnimation()
|
||||
for (const [row, scrollLeft] of snapshot.rows) {
|
||||
if (row.isConnected) {
|
||||
row.scrollLeft = scrollLeft
|
||||
}
|
||||
}
|
||||
syncedRowsCurrentOffset = snapshot.offset
|
||||
syncedRowsTargetOffset = snapshot.targetOffset
|
||||
lastSyncedRowsAnimationAt = null
|
||||
}
|
||||
|
||||
export function useKeyboardNavigation() {
|
||||
return {
|
||||
focusedElement,
|
||||
@@ -901,6 +932,8 @@ export function useKeyboardNavigation() {
|
||||
focusAt,
|
||||
getFocusState,
|
||||
restoreFocusState,
|
||||
snapshotSyncedRowScroll,
|
||||
restoreSyncedRowScroll,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user