tv模式左右焦点锁定,默认聚焦首页导航
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
|
||||||
import TVVirtualRemote from './TVVirtualRemote';
|
import TVVirtualRemote from './TVVirtualRemote';
|
||||||
|
|
||||||
@@ -38,6 +38,19 @@ export default function TVLayout({
|
|||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
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 (
|
return (
|
||||||
<main className='min-h-screen overflow-x-hidden bg-black text-slate-50'>
|
<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%)]' />
|
<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
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={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 ${
|
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
|
active
|
||||||
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
|
? 'bg-rose-600 text-white shadow-lg shadow-rose-950/40'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function TVRow({ section }: { section: TVSection }) {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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} />)}
|
{section.items.map((item) => <TVCard key={`${section.title}-${item.id}`} item={item} />)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -159,6 +159,41 @@ function focusNearestTopNavigationElement(active: HTMLElement) {
|
|||||||
return true;
|
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) {
|
function focusElement(element: HTMLElement) {
|
||||||
element.focus({ preventScroll: true });
|
element.focus({ preventScroll: true });
|
||||||
|
|
||||||
@@ -202,6 +237,14 @@ function moveSpatialFocus(direction: 'up' | 'down' | 'left' | 'right', lastFocus
|
|||||||
return;
|
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)) {
|
if (direction === 'down' && isTopNavigationElement(active)) {
|
||||||
const firstContentElement = getContentFocusableElements()[0];
|
const firstContentElement = getContentFocusableElements()[0];
|
||||||
if (firstContentElement) {
|
if (firstContentElement) {
|
||||||
|
|||||||
Reference in New Issue
Block a user