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