feat: Enhance spatial navigation for TV remotes by making more components focusable, improving input field handling, and refining video card image fallback UI.

This commit is contained in:
kuekhaoyang
2026-02-17 12:56:22 +08:00
parent acf8cff63a
commit 91280f250f
7 changed files with 33 additions and 19 deletions
+1
View File
@@ -41,6 +41,7 @@ export function SourceBadgeItem({
}
}}
tabIndex={0}
data-focusable
aria-pressed={isSelected}
aria-label={`${name} 视频源,${count} 个视频${isSelected ? ',已选中' : ''}`}
className={`
+1
View File
@@ -39,6 +39,7 @@ export function TypeBadgeItem({
}
}}
tabIndex={0}
data-focusable
aria-pressed={isSelected}
aria-label={`${type} 类型,${count} 个视频${isSelected ? ',已选中' : ''}`}
className={`
+5 -3
View File
@@ -48,6 +48,7 @@ export const VideoCard = memo<VideoCardProps>(({
role="listitem"
aria-label={`${video.vod_name}${video.vod_remarks ? ` - ${video.vod_remarks}` : ''}`}
prefetch={false}
data-focusable
className="group cursor-pointer hover:translate-y-[-2px] transition-transform duration-200 ease-out block h-full"
>
<Card
@@ -81,9 +82,10 @@ export const VideoCard = memo<VideoCardProps>(({
</div>
)}
{/* Fallback Icon */}
<div className="absolute inset-0 flex items-center justify-center -z-10">
<Icons.Film size={64} className="text-[var(--text-color-secondary)] opacity-20" />
{/* Fallback Icon - visible when image fails */}
<div className="absolute inset-0 flex flex-col items-center justify-center -z-10 gap-2">
<Icons.Film size={48} className="text-[var(--text-color-secondary)] opacity-40" />
<span className="text-xs text-[var(--text-color-secondary)] opacity-60 px-2 text-center line-clamp-2">{video.vod_name}</span>
</div>
{/* Badge Container */}
+5 -3
View File
@@ -89,6 +89,7 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
role="listitem"
aria-label={`${name} - ${videos.length} 个源${representative.vod_remarks ? ` - ${representative.vod_remarks}` : ''}`}
prefetch={false}
data-focusable
className="group cursor-pointer hover:translate-y-[-2px] transition-transform duration-200 ease-out block h-full"
>
<Card
@@ -122,9 +123,10 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
</div>
)}
{/* Fallback Icon */}
<div className="absolute inset-0 flex items-center justify-center -z-10">
<Icons.Film size={64} className="text-[var(--text-color-secondary)] opacity-20" />
{/* Fallback Icon - visible when image fails */}
<div className="absolute inset-0 flex flex-col items-center justify-center -z-10 gap-2">
<Icons.Film size={48} className="text-[var(--text-color-secondary)] opacity-40" />
<span className="text-xs text-[var(--text-color-secondary)] opacity-60 px-2 text-center line-clamp-2">{name}</span>
</div>
{/* Badge Container */}
+18 -10
View File
@@ -80,16 +80,6 @@ export function useSpatialNavigation(enabled: boolean) {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!enabled) return;
// Skip if target is input/textarea
const target = e.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable
) {
return;
}
const directionMap: Record<string, Direction> = {
ArrowUp: 'up',
ArrowDown: 'down',
@@ -99,6 +89,24 @@ export function useSpatialNavigation(enabled: boolean) {
const direction = directionMap[e.key];
// For input/textarea: allow Left/Right for cursor movement,
// but let Up/Down navigate spatially so the user can escape the input on TV.
// Skip if the event was already handled (e.g., by search history dropdown).
const target = e.target as HTMLElement;
const isInput = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
if (isInput) {
if (!direction || direction === 'left' || direction === 'right') {
return;
}
// If a React handler already called preventDefault (e.g., dropdown navigation), skip
if (e.defaultPrevented) {
return;
}
}
if (direction) {
// Check if the focused element is inside a [data-no-spatial] container
const focused = document.activeElement as HTMLElement | null;
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "kvideo",
"version": "4.3.4",
"version": "4.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvideo",
"version": "4.3.4",
"version": "4.3.5",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "kvideo",
"version": "4.3.4",
"version": "4.3.5",
"private": true,
"scripts": {
"dev": "next dev",