mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
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:
@@ -41,6 +41,7 @@ export function SourceBadgeItem({
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
data-focusable
|
||||
aria-pressed={isSelected}
|
||||
aria-label={`${name} 视频源,${count} 个视频${isSelected ? ',已选中' : ''}`}
|
||||
className={`
|
||||
|
||||
@@ -39,6 +39,7 @@ export function TypeBadgeItem({
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
data-focusable
|
||||
aria-pressed={isSelected}
|
||||
aria-label={`${type} 类型,${count} 个视频${isSelected ? ',已选中' : ''}`}
|
||||
className={`
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.3.4",
|
||||
"version": "4.3.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user