From e8ea89726b4ba8a00dc1130cf5fcd08287467dfd Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sun, 7 Jun 2026 21:05:21 +0800 Subject: [PATCH] =?UTF-8?q?tv=E6=A8=A1=E5=BC=8F=E5=B7=A6=E5=8F=B3=E7=84=A6?= =?UTF-8?q?=E7=82=B9=E9=94=81=E5=AE=9A=EF=BC=8C=E9=BB=98=E8=AE=A4=E8=81=9A?= =?UTF-8?q?=E7=84=A6=E9=A6=96=E9=A1=B5=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/tv/TVLayout.tsx | 16 +++++++++- src/components/tv/TVRow.tsx | 2 +- src/components/tv/TVVirtualRemote.tsx | 43 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/components/tv/TVLayout.tsx b/src/components/tv/TVLayout.tsx index 8a63e41..34772dc 100644 --- a/src/components/tv/TVLayout.tsx +++ b/src/components/tv/TVLayout.tsx @@ -13,7 +13,7 @@ import { } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import TVVirtualRemote from './TVVirtualRemote'; @@ -38,6 +38,19 @@ export default function TVLayout({ }) { const pathname = usePathname(); + useEffect(() => { + if (!showNav || pathname !== '/tv') return; + + window.requestAnimationFrame(() => { + const active = document.activeElement; + if (active instanceof HTMLElement && active !== document.body) return; + + document + .querySelector('[data-tv-home-nav="true"]') + ?.focus({ preventScroll: true }); + }); + }, [pathname, showNav]); + return (
@@ -54,6 +67,7 @@ export default function TVLayout({ )}
-
+
{section.items.map((item) => )}
diff --git a/src/components/tv/TVVirtualRemote.tsx b/src/components/tv/TVVirtualRemote.tsx index 34bb043..7e01340 100644 --- a/src/components/tv/TVVirtualRemote.tsx +++ b/src/components/tv/TVVirtualRemote.tsx @@ -159,6 +159,41 @@ function focusNearestTopNavigationElement(active: HTMLElement) { return true; } +function moveTopNavigationFocus(active: HTMLElement, direction: 'left' | 'right') { + const topNavigationElements = getTopNavigationElements(); + const currentIndex = topNavigationElements.indexOf(active); + if (currentIndex === -1) return false; + + const nextIndex = direction === 'right' + ? Math.min(currentIndex + 1, topNavigationElements.length - 1) + : Math.max(currentIndex - 1, 0); + + if (nextIndex === currentIndex) return true; + + focusElement(topNavigationElements[nextIndex]); + return true; +} + +function moveHorizontalRowFocus(active: HTMLElement, direction: 'left' | 'right') { + const row = active.closest('[data-tv-focus-row="horizontal"]'); + if (!row) return false; + + const rowElements = Array.from(row.querySelectorAll(focusableSelector)) + .filter((element) => !element.closest('[data-tv-no-focus="true"]')) + .filter(isVisible); + const currentIndex = rowElements.indexOf(active); + if (currentIndex === -1) return false; + + const nextIndex = direction === 'right' + ? Math.min(currentIndex + 1, rowElements.length - 1) + : Math.max(currentIndex - 1, 0); + + if (nextIndex === currentIndex) return true; + + focusElement(rowElements[nextIndex]); + return true; +} + function focusElement(element: HTMLElement) { element.focus({ preventScroll: true }); @@ -202,6 +237,14 @@ function moveSpatialFocus(direction: 'up' | 'down' | 'left' | 'right', lastFocus return; } + if ((direction === 'left' || direction === 'right') && isTopNavigationElement(active)) { + if (moveTopNavigationFocus(active, direction)) return; + } + + if (direction === 'left' || direction === 'right') { + if (moveHorizontalRowFocus(active, direction)) return; + } + if (direction === 'down' && isTopNavigationElement(active)) { const firstContentElement = getContentFocusableElements()[0]; if (firstContentElement) {