your personal website on atproto - mirror blento.app

fix gamecard focus

+126 -16
+18 -7
src/lib/cards/GameCards/DinoGameCard/DinoGameCard.svelte
··· 1 <script lang="ts"> 2 - import { isTyping } from '$lib/helper'; 3 import type { ContentComponentProps } from '../../types'; 4 import { onMount, onDestroy } from 'svelte'; 5 6 let { item }: ContentComponentProps = $props(); 7 8 let canvas: HTMLCanvasElement; 9 let ctx: CanvasRenderingContext2D | null = null; 10 let animationId: number; 11 ··· 181 function startGame() { 182 resetGame(); 183 gameState = 'playing'; 184 } 185 186 function jump() { ··· 207 } 208 } 209 210 function handleKeyDown(e: KeyboardEvent) { 211 - if(isTyping()) return; 212 - 213 if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { 214 e.preventDefault(); 215 jump(); ··· 530 }); 531 </script> 532 533 - <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} /> 534 - 535 - <div class="relative h-full w-full overflow-hidden"> 536 - <canvas bind:this={canvas} class="h-full w-full invert dark:invert-0" ontouchstart={handleTouch} 537 ></canvas> 538 539 {#if gameState === 'idle' || gameState === 'gameover'}
··· 1 <script lang="ts"> 2 import type { ContentComponentProps } from '../../types'; 3 import { onMount, onDestroy } from 'svelte'; 4 5 let { item }: ContentComponentProps = $props(); 6 7 let canvas: HTMLCanvasElement; 8 + let container: HTMLDivElement; 9 let ctx: CanvasRenderingContext2D | null = null; 10 let animationId: number; 11 ··· 181 function startGame() { 182 resetGame(); 183 gameState = 'playing'; 184 + // Focus container so keyboard events work for this game 185 + container?.focus(); 186 } 187 188 function jump() { ··· 209 } 210 } 211 212 + // Handle keyboard input (only responds when this game container is focused) 213 function handleKeyDown(e: KeyboardEvent) { 214 if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { 215 e.preventDefault(); 216 jump(); ··· 531 }); 532 </script> 533 534 + <!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions --> 535 + <div 536 + bind:this={container} 537 + class="relative h-full w-full overflow-hidden outline-none" 538 + tabindex="0" 539 + role="application" 540 + aria-label="Dino game" 541 + onkeydown={handleKeyDown} 542 + onkeyup={handleKeyUp} 543 + > 544 + <canvas 545 + bind:this={canvas} 546 + class="h-full w-full touch-none select-none invert dark:invert-0" 547 + ontouchstart={handleTouch} 548 ></canvas> 549 550 {#if gameState === 'idle' || gameState === 'gameover'}
+95
src/lib/cards/GameCards/README.md
···
··· 1 + # Game Cards 2 + 3 + This folder contains interactive game cards (Tetris, Dino, etc.). 4 + 5 + ## Implementation Requirements 6 + 7 + When creating a new game card, follow these patterns to ensure multiple games on the same page work independently. 8 + 9 + ### 1. Container Setup 10 + 11 + Make the game container focusable and handle keyboard events on it (not on `svelte:window`): 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + let container: HTMLDivElement; 16 + </script> 17 + 18 + <!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions --> 19 + <div 20 + bind:this={container} 21 + class="relative h-full w-full overflow-hidden outline-none" 22 + tabindex="0" 23 + role="application" 24 + aria-label="Your game name" 25 + onkeydown={handleKeyDown} 26 + onkeyup={handleKeyUp} 27 + > 28 + <!-- game content --> 29 + </div> 30 + ``` 31 + 32 + ### 2. Focus on Game Start 33 + 34 + When the game starts, focus the container so keyboard events work: 35 + 36 + ```typescript 37 + function startGame() { 38 + // ... game initialization 39 + container?.focus(); 40 + } 41 + ``` 42 + 43 + ### 3. Keyboard Handlers 44 + 45 + Do NOT use `<svelte:window onkeydown={...} />` - this captures all keyboard events globally and causes all games to respond at once. 46 + 47 + Instead, attach handlers directly to the container div. The handlers will only fire when that specific game is focused. 48 + 49 + ```typescript 50 + function handleKeyDown(e: KeyboardEvent) { 51 + if (e.code === 'Space') { 52 + e.preventDefault(); 53 + // handle action 54 + } 55 + } 56 + ``` 57 + 58 + ### 4. Canvas Setup 59 + 60 + Add `touch-none` and `select-none` classes to prevent scrolling and text selection during gameplay: 61 + 62 + ```svelte 63 + <canvas 64 + bind:this={canvas} 65 + class="h-full w-full touch-none select-none" 66 + ontouchstart={handleTouchStart} 67 + ontouchmove={handleTouchMove} 68 + ontouchend={handleTouchEnd} 69 + ></canvas> 70 + ``` 71 + 72 + ### 5. Touch Controls 73 + 74 + For mobile support, add touch event handlers to the canvas. Prevent default to stop page scrolling: 75 + 76 + ```typescript 77 + function handleTouchStart(e: TouchEvent) { 78 + if (gameState === 'playing') { 79 + e.preventDefault(); 80 + } 81 + // handle touch 82 + } 83 + ``` 84 + 85 + ## Checklist for New Game Cards 86 + 87 + - [ ] Container has `bind:this={container}` reference 88 + - [ ] Container has `tabindex="0"` for focusability 89 + - [ ] Container has `role="application"` and `aria-label` 90 + - [ ] Container has `outline-none` class 91 + - [ ] Keyboard handlers are on container, NOT `svelte:window` 92 + - [ ] `startGame()` calls `container?.focus()` 93 + - [ ] Canvas has `touch-none select-none` classes 94 + - [ ] Touch handlers call `e.preventDefault()` when game is active 95 + - [ ] No `isTyping()` check needed (focus handles this automatically)
+13 -9
src/lib/cards/GameCards/TetrisCard/TetrisCard.svelte
··· 2 import type { ContentComponentProps } from '../../types'; 3 import { onMount, onDestroy } from 'svelte'; 4 import Tetris8BitMusic from './Tetris8Bit.mp3'; 5 - import { isTyping } from '$lib/helper'; 6 7 let { item }: ContentComponentProps = $props(); 8 ··· 485 lockPiece(); 486 } 487 488 - // Handle keyboard input 489 function handleKeyDown(e: KeyboardEvent) { 490 - // Don't capture keys when user is typing in an input field 491 - if (isTyping()) return; 492 - 493 if (gameState !== 'playing' || isClearingAnimation) { 494 if (e.code === 'Space' || e.code === 'Enter') { 495 e.preventDefault(); ··· 669 gameState = 'playing'; 670 lastDrop = performance.now(); 671 startMusic(); 672 } 673 674 function calculateSize() { ··· 1008 }); 1009 </script> 1010 1011 - <svelte:window onkeydown={handleKeyDown} /> 1012 - 1013 - <div bind:this={container} class="relative h-full w-full overflow-hidden"> 1014 <canvas 1015 bind:this={canvas} 1016 - class="h-full w-full touch-none" 1017 ontouchstart={handleTouchStart} 1018 ontouchmove={handleTouchMove} 1019 ontouchend={handleTouchEnd}
··· 2 import type { ContentComponentProps } from '../../types'; 3 import { onMount, onDestroy } from 'svelte'; 4 import Tetris8BitMusic from './Tetris8Bit.mp3'; 5 6 let { item }: ContentComponentProps = $props(); 7 ··· 484 lockPiece(); 485 } 486 487 + // Handle keyboard input (only responds when this game container is focused) 488 function handleKeyDown(e: KeyboardEvent) { 489 if (gameState !== 'playing' || isClearingAnimation) { 490 if (e.code === 'Space' || e.code === 'Enter') { 491 e.preventDefault(); ··· 665 gameState = 'playing'; 666 lastDrop = performance.now(); 667 startMusic(); 668 + // Focus container so keyboard events work for this game 669 + container?.focus(); 670 } 671 672 function calculateSize() { ··· 1006 }); 1007 </script> 1008 1009 + <!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions --> 1010 + <div 1011 + bind:this={container} 1012 + class="relative h-full w-full overflow-hidden outline-none" 1013 + tabindex="0" 1014 + role="application" 1015 + aria-label="Tetris game" 1016 + onkeydown={handleKeyDown} 1017 + > 1018 <canvas 1019 bind:this={canvas} 1020 + class="h-full w-full touch-none select-none" 1021 ontouchstart={handleTouchStart} 1022 ontouchmove={handleTouchMove} 1023 ontouchend={handleTouchEnd}