Improve search-detail back navigation and focus restore

This commit is contained in:
2026-05-27 21:12:39 +00:00
parent 81be850e0c
commit 29cda926a2
3 changed files with 66 additions and 16 deletions
+64 -15
View File
@@ -30,7 +30,7 @@
<!-- Persistent Header overlay - single instance -->
<Header
:current-view="currentView"
:current-view="headerCurrentView"
:search-query="searchQuery"
:mpc-be-connected="mpcBeConnected"
:nav-row="1"
@@ -426,6 +426,25 @@ function restoreBrowseFocus(path: string) {
}, 100)
}
function getSearchExitTargetFromFocusedCard(): { path: "/movies" | "/series"; itemId: string } | null {
const active = document.activeElement as HTMLElement | null
const focusedCard = active?.closest("[data-item-id]") as HTMLElement | null
if (!focusedCard) return null
const itemId = focusedCard.getAttribute("data-item-id")
const itemType = focusedCard.getAttribute("data-item-type")
if (!itemId || !itemType) return null
if (itemType === "movies") {
return { path: "/movies", itemId }
}
if (itemType === "series") {
return { path: "/series", itemId }
}
return null
}
function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}) {
const currentQuery = getRouteSearchQuery()
const targetPath = options.targetPath ?? getBrowsePath()
@@ -441,11 +460,6 @@ function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}
return
}
if (route.path === targetPath) {
searchReturnPath.value = null
return
}
const backPath =
typeof window.history.state?.back === "string"
? normalizeHistoryPath(window.history.state.back)
@@ -459,11 +473,12 @@ function clearSearch(options: { preferBack?: boolean; targetPath?: string } = {}
if (canRestoreWithBack) {
router.back()
} else {
void router.replace({ path: targetPath })
}
restoreBrowseFocus(targetPath)
} else {
void router.replace({ path: targetPath }).finally(() => {
restoreBrowseFocus(targetPath)
})
}
}
function updateSearchQuery(nextValue: string) {
@@ -501,6 +516,15 @@ function handleEscapeKey(event: KeyboardEvent) {
}
const path = route.path
const activeSearchQuery = getRouteSearchQuery()
if (route.params.id && activeSearchQuery) {
event.preventDefault()
const targetPath = getBrowsePath()
router.push({ path: targetPath, query: { q: activeSearchQuery } })
restoreFocusForPage(getBrowseViewFromPath(targetPath))
return
}
// From movie detail -> movies list
if (path.startsWith("/movies/") && route.params.id) {
@@ -520,6 +544,12 @@ function handleEscapeKey(event: KeyboardEvent) {
if (getRouteSearchQuery()) {
event.preventDefault()
const target = getSearchExitTargetFromFocusedCard()
if (target) {
lastViewedItemId.value = target.itemId
clearSearch({ preferBack: false, targetPath: target.path })
return
}
clearSearch({ preferBack: true })
return
}
@@ -552,6 +582,14 @@ onUnmounted(() => {
// Handle back navigation (Escape key or Back button)
function goBack() {
const path = route.path
const activeSearchQuery = getRouteSearchQuery()
if (route.params.id && activeSearchQuery) {
const targetPath = getBrowsePath()
router.push({ path: targetPath, query: { q: activeSearchQuery } })
restoreFocusForPage(getBrowseViewFromPath(targetPath))
return
}
// From movie detail -> movies list with focus restoration
if (path.startsWith("/movies/") && route.params.id) {
@@ -576,6 +614,10 @@ const currentView = computed(() => {
return (route.meta.view as "movies" | "series") || "movies"
})
const headerCurrentView = computed<"movies" | "series" | "search">(() => {
return selectedItem.value && searchQuery.value ? "search" : currentView.value
})
// Header position based on current page
const headerPosition = computed(() => {
if (selectedItem.value) {
@@ -619,7 +661,7 @@ watch(selectedItem, (item) => {
}
})
// Show detail by navigating to URL (clears search)
// Show detail by navigating to URL
function showDetail(item: MediaItem) {
// Save the item ID to restore focus when returning
lastViewedItemId.value = item.id
@@ -646,17 +688,24 @@ function showDetail(item: MediaItem) {
if (playableFile) {
handlePlay(playableFile)
} else {
router.push(`/series/${epData.series.id}`)
const query = searchQuery.value ? { q: searchQuery.value } : undefined
router.push({ path: `/series/${epData.series.id}`, query })
}
} else {
// Push without query to clear search and add to history
router.push({ path: `/${item.type}/${item.id}` })
const query = searchQuery.value ? { q: searchQuery.value } : undefined
router.push({ path: `/${item.type}/${item.id}`, query })
}
}
// Close detail by navigating back to list
function closeDetail() {
router.push(`/${currentView.value}`)
const activeSearchQuery = getRouteSearchQuery()
const targetPath = getBrowsePath()
if (activeSearchQuery) {
router.push({ path: targetPath, query: { q: activeSearchQuery } })
} else {
router.push(targetPath)
}
}
function handleActorSearch(actorName: string) {
+2 -2
View File
@@ -29,7 +29,7 @@
<!-- Detail mode: show current category + Details -->
<template v-else>
<button class="header-nav-item" v-bind="navAttrs(navRow, 0)" @focus="goToCategory">
{{ currentView === "movies" ? "Movies" : "Series" }}
{{ currentView === "search" ? "Search" : currentView === "movies" ? "Movies" : "Series" }}
</button>
<button class="header-nav-item active" v-bind="navAttrs(navRow, 1, 1)">Details</button>
</template>
@@ -163,7 +163,7 @@ interface RootEntry {
}
const props = defineProps<{
currentView: "movies" | "series"
currentView: "movies" | "series" | "search"
searchQuery: string
mpcBeConnected: boolean
navRow: number
+1
View File
@@ -4,6 +4,7 @@
class="media-card"
v-bind="navAttributes"
:data-item-id="item.id"
:data-item-type="item.type"
:href="href || undefined"
@click="handleClick"
@keydown.enter.prevent="$emit('click')"