fix: prevent button focus from interfering with game controls when changing modes

- Add blur() to mode buttons to remove focus after clicking
- Add preventDefault() to space/escape handlers to stop button activation
- Fix stale closures in useKeyBindings by accessing gameManager directly
- Remove all useCallback hooks per React 19 best practices
- Remove function dependencies from useEffect hooks
This commit is contained in:
2025-11-04 00:09:24 +00:00
parent 308eafac62
commit 2795931d56
6 changed files with 77 additions and 66 deletions
+17 -16
View File
@@ -4,8 +4,8 @@ import "@vimazing/typing-chud/game.css";
import { useKeyBindings } from "./useKeyBindings";
function App() {
const [gameMode, setGameMode] = useState<GameMode>("words");
const gameManager = useGame({ gameMode }, useKeyBindings);
const [gameMode, setGameMode] = useState<GameMode>("letters");
const gameManager = useGame({ initialGameMode: gameMode }, useKeyBindings);
const { containerRef, gameStatus, scoreManager } = gameManager;
const formatTime = (ms: number) => {
@@ -21,20 +21,21 @@ function App() {
<h1 className="text-2xl font-bold text-center">Typing Chud</h1>
<div className="flex gap-2 justify-center text-sm">
{(['letters', 'words', 'sentences', 'paragraphs'] as const).map((mode) => (
<button
key={mode}
onClick={() => {
setGameMode(mode);
gameManager.quitGame();
}}
className={`px-3 py-1 rounded capitalize font-medium transition ${
gameMode === mode
? 'bg-blue-600 text-white'
: 'bg-muted text-foreground hover:bg-blue-500 hover:text-white'
}`}
>
{mode}
</button>
<button
key={mode}
onClick={(e) => {
setGameMode(mode);
gameManager.changeGameMode(mode);
e.currentTarget.blur(); // Remove focus from button
}}
className={`px-3 py-1 rounded capitalize font-medium transition ${
gameMode === mode
? 'bg-blue-600 text-white'
: 'bg-muted text-foreground hover:bg-blue-500 hover:text-white'
}`}
>
{mode}
</button>
))}
</div>
</div>
+14 -14
View File
@@ -2,44 +2,44 @@ import { useEffect } from "react";
import type { GameManager } from "../../src";
export const useKeyBindings = (gameManager: GameManager) => {
const { gameStatus, renderBoard, startGame, quitGame, clearKeyLog, handleTypedLetter } = gameManager;
useEffect(() => {
const handler = (ev: KeyboardEvent) => {
// start game
if (ev.code === "Space") {
if (["waiting", "game-over", "game-won"].includes(gameStatus)) {
clearKeyLog();
renderBoard();
if (gameStatus === "game-over" || gameStatus === "game-won") {
quitGame();
setTimeout(() => startGame(), 0);
ev.preventDefault(); // Prevent button activation
if (["waiting", "game-over", "game-won"].includes(gameManager.gameStatus)) {
gameManager.clearKeyLog();
gameManager.renderBoard();
if (gameManager.gameStatus === "game-over" || gameManager.gameStatus === "game-won") {
gameManager.quitGame();
setTimeout(() => gameManager.startGame(), 0);
} else {
startGame();
gameManager.startGame();
}
return;
}
}
// ignore other keys while waiting
if (gameStatus === "waiting") return;
if (gameManager.gameStatus === "waiting") return;
// quit
if (ev.key === "Escape") {
quitGame();
ev.preventDefault();
gameManager.quitGame();
return;
}
// handle letter typing during game
if (gameStatus === "started") {
if (gameManager.gameStatus === "started") {
if (ev.key.length === 1 && /[a-zA-Z]/.test(ev.key)) {
handleTypedLetter(ev.key);
gameManager.handleTypedLetter(ev.key);
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [gameStatus, handleTypedLetter]);
}, [gameManager.gameStatus]);
};