tv模式左右焦点锁定,默认聚焦首页导航

This commit is contained in:
mtvpls
2026-06-07 21:05:21 +08:00
parent 7fea2da185
commit e8ea89726b
3 changed files with 59 additions and 2 deletions
+15 -1
View File
@@ -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<HTMLElement>('[data-tv-home-nav="true"]')
?.focus({ preventScroll: true });
});
}, [pathname, showNav]);
return (
<main className='min-h-screen overflow-x-hidden bg-black text-slate-50'>
<div className='fixed inset-0 pointer-events-none bg-[radial-gradient(circle_at_20%_0%,rgba(225,29,72,0.22),transparent_34%),radial-gradient(circle_at_90%_10%,rgba(79,70,229,0.24),transparent_30%),linear-gradient(180deg,#05050b_0%,#000_55%)]' />
@@ -54,6 +67,7 @@ export default function TVLayout({
<Link
key={item.href}
href={item.href}
data-tv-home-nav={item.href === '/tv' ? 'true' : undefined}
className={`group flex shrink-0 cursor-pointer items-center gap-2 rounded-2xl px-5 py-3 text-xl font-semibold outline-none transition duration-200 tv-focusable ${
active
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
+1 -1
View File
@@ -20,7 +20,7 @@ export default function TVRow({ section }: { section: TVSection }) {
</Link>
)}
</div>
<div className='flex gap-5 overflow-x-auto px-5 py-6 [scrollbar-width:none]'>
<div data-tv-focus-row='horizontal' className='flex gap-5 overflow-x-auto px-5 py-6 [scrollbar-width:none]'>
{section.items.map((item) => <TVCard key={`${section.title}-${item.id}`} item={item} />)}
</div>
</section>
+43
View File
@@ -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<HTMLElement>('[data-tv-focus-row="horizontal"]');
if (!row) return false;
const rowElements = Array.from(row.querySelectorAll<HTMLElement>(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) {