From 577bac2bd7dfba4dcaf3772401e6fe6c66e78365 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 06:34:49 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20GameCard=20keyboard=20?= =?UTF-8?q?accessibility=20and=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert interactive spans to semantic buttons in GameCard - Add tabIndex and role="button" to GameCard containers - Implement onKeyDown for card navigation (Enter/Space) - Add focus-visible styles for better navigation visibility - Remove unused hasIgdbId state in GameScreenshotsCarousel to fix build Co-authored-by: Gamepulse <8333979+Gamepulse@users.noreply.github.com> --- .Jules/palette.md | 3 + src/components/Library/GameCard.tsx | 60 ++++++++++++++----- .../Library/GameScreenshotsCarousel.tsx | 2 - 3 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 .Jules/palette.md diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..ea026fd --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2025-05-14 - [Semantic Buttons and Card Accessibility] +**Learning:** Using semantic ` ))} {remainingCount > 0 && ( @@ -73,19 +73,19 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, return (
{allMetadata.map((item) => ( - { e.stopPropagation(); e.preventDefault(); console.log('[GameCard] Metadata tag clicked:', item.type, item.name); onFilter?.(item.type, item.name); }} - className="px-1.5 py-0.5 text-xs rounded bg-gray-700/50 theme-text-muted hover:bg-indigo-600/30 transition-colors cursor-pointer select-none" - role="button" + className="px-1.5 py-0.5 text-xs rounded bg-gray-700/50 theme-text-muted hover:bg-indigo-600/30 transition-colors cursor-pointer select-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-indigo-500 outline-none" > {item.name} - + ))}
); @@ -132,18 +132,18 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, const label = COMPLETION_STATUS_LABELS[completion_status as keyof typeof COMPLETION_STATUS_LABELS] || completion_status; return ( - { e.stopPropagation(); e.preventDefault(); console.log('[GameCard] Status badge clicked:', completion_status); onFilter?.('status', completion_status); }} - className={`absolute top-2 left-2 px-2 py-1 rounded-full text-xs text-white ${statusColors[completion_status] || 'bg-gray-600'} hover:opacity-80 transition-opacity cursor-pointer select-none`} - role="button" + className={`absolute top-2 left-2 px-2 py-1 rounded-full text-xs text-white ${statusColors[completion_status] || 'bg-gray-600'} hover:opacity-80 transition-opacity cursor-pointer select-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-indigo-500 outline-none`} > {label} - + ); }; @@ -166,7 +166,10 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, if (viewMode === "grid") { return (
{ // Don't navigate if clicking on an interactive element const target = e.target as HTMLElement; @@ -177,6 +180,13 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, console.log('[GameCard] Grid click - navigating to game:', game.id); onClick(game.id); }} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(game.id); + } + }} >
{renderCover()} @@ -235,7 +245,10 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, if (viewMode === "compact") { return (
{ const target = e.target as HTMLElement; if (target.closest('[role="button"]') || target.closest('button') || target.closest('a')) { @@ -243,6 +256,13 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, } onClick(game.id); }} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(game.id); + } + }} >
{cover_url ? ( @@ -339,7 +359,10 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, // List view mode return (
{ const target = e.target as HTMLElement; if (target.closest('[role="button"]') || target.closest('button') || target.closest('a')) { @@ -347,6 +370,13 @@ const GameCard: React.FC = ({ game, onClick, viewMode, onFilter, } onClick(game.id); }} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(game.id); + } + }} >
{cover_url ? ( diff --git a/src/components/Library/GameScreenshotsCarousel.tsx b/src/components/Library/GameScreenshotsCarousel.tsx index 6a4e37c..bfae3b8 100644 --- a/src/components/Library/GameScreenshotsCarousel.tsx +++ b/src/components/Library/GameScreenshotsCarousel.tsx @@ -20,7 +20,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps const [igdbScreenshots, setIgdbScreenshots] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [loading, setLoading] = useState(true); - const [hasIgdbId, setHasIgdbId] = useState(false); useEffect(() => { const loadScreenshots = async () => { @@ -29,7 +28,6 @@ export function GameScreenshotsCarousel({ gameId }: GameScreenshotsCarouselProps // Load game data to check IGDB ID try { const game = await invoke<{ igdb_id: number | null }>("get_game_by_id", { id: gameId }); - setHasIgdbId(!!game?.igdb_id); // Load IGDB screenshots if available if (game?.igdb_id) {