your personal website on atproto - mirror blento.app

fix gamecard focus

+126 -16
+18 -7
src/lib/cards/GameCards/DinoGameCard/DinoGameCard.svelte
··· 1 1 <script lang="ts"> 2 - import { isTyping } from '$lib/helper'; 3 2 import type { ContentComponentProps } from '../../types'; 4 3 import { onMount, onDestroy } from 'svelte'; 5 4 6 5 let { item }: ContentComponentProps = $props(); 7 6 8 7 let canvas: HTMLCanvasElement; 8 + let container: HTMLDivElement; 9 9 let ctx: CanvasRenderingContext2D | null = null; 10 10 let animationId: number; 11 11 ··· 181 181 function startGame() { 182 182 resetGame(); 183 183 gameState = 'playing'; 184 + // Focus container so keyboard events work for this game 185 + container?.focus(); 184 186 } 185 187 186 188 function jump() { ··· 207 209 } 208 210 } 209 211 212 + // Handle keyboard input (only responds when this game container is focused) 210 213 function handleKeyDown(e: KeyboardEvent) { 211 - if(isTyping()) return; 212 - 213 214 if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { 214 215 e.preventDefault(); 215 216 jump(); ··· 530 531 }); 531 532 </script> 532 533 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} 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} 537 548 ></canvas> 538 549 539 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 2 import type { ContentComponentProps } from '../../types'; 3 3 import { onMount, onDestroy } from 'svelte'; 4 4 import Tetris8BitMusic from './Tetris8Bit.mp3'; 5 - import { isTyping } from '$lib/helper'; 6 5 7 6 let { item }: ContentComponentProps = $props(); 8 7 ··· 485 484 lockPiece(); 486 485 } 487 486 488 - // Handle keyboard input 487 + // Handle keyboard input (only responds when this game container is focused) 489 488 function handleKeyDown(e: KeyboardEvent) { 490 - // Don't capture keys when user is typing in an input field 491 - if (isTyping()) return; 492 - 493 489 if (gameState !== 'playing' || isClearingAnimation) { 494 490 if (e.code === 'Space' || e.code === 'Enter') { 495 491 e.preventDefault(); ··· 669 665 gameState = 'playing'; 670 666 lastDrop = performance.now(); 671 667 startMusic(); 668 + // Focus container so keyboard events work for this game 669 + container?.focus(); 672 670 } 673 671 674 672 function calculateSize() { ··· 1008 1006 }); 1009 1007 </script> 1010 1008 1011 - <svelte:window onkeydown={handleKeyDown} /> 1012 - 1013 - <div bind:this={container} class="relative h-full w-full overflow-hidden"> 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 + > 1014 1018 <canvas 1015 1019 bind:this={canvas} 1016 - class="h-full w-full touch-none" 1020 + class="h-full w-full touch-none select-none" 1017 1021 ontouchstart={handleTouchStart} 1018 1022 ontouchmove={handleTouchMove} 1019 1023 ontouchend={handleTouchEnd}