Skip to content

fix: GameOver overlay blocks click-to-focus, Enter restart dead after tab switch#49

Open
moKshagna-p wants to merge 1 commit into
d1rshan:mainfrom
moKshagna-p:fix/game-over-focus
Open

fix: GameOver overlay blocks click-to-focus, Enter restart dead after tab switch#49
moKshagna-p wants to merge 1 commit into
d1rshan:mainfrom
moKshagna-p:fix/game-over-focus

Conversation

@moKshagna-p

Copy link
Copy Markdown
Contributor

Problem

Clicking the "enter to restart" overlay during game-over doesn't refocus the hidden input — so pressing Enter does nothing.

Two root causes:

  1. GameOver overlay blocks clicks. The falling words game container had no onClick handler. The GameOver overlay renders at z-20 on top of Field (which has onFieldClick={actions.focusInput}). Clicks on the overlay never reach focusInput().
    Survival already handled this correctly — its container div had onClick={actions.focusInput}.

  2. No refocus on tab return. When endGame() fires from visibilitychange (switching to another tab), the browser ignores focus() on hidden tabs. On return, the input stays blurred.

Fix

3 files changed, 30 insertions, 3 deletions:

File Change
falling-words/view.tsx Added onClick={actions.focusInput} to game container (same pattern as survival)
falling-words/engine.ts visibilitychange handler refocuses input when tab becomes visible
survival/engine.ts Added missing visibilitychange + blur handling (was missing entirely)

Tab navigation through header/footer links is completely untouched.

Fixes #47

@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the game-over overlay blocking click-to-focus and adds pause/resume behaviour when the user switches tabs, replacing the previous approach of immediately ending the game on blur or visibility change.

  • view.tsx: adds onClick={actions.focusInput} to the falling-words container so clicks on the GameOver overlay bubble up and refocus the hidden input — a one-line fix matching the survival game pattern.
  • engine.ts: replaces endGame() on blur/visibility-hide with a new pauseGame/resumeGame pair that stops the animation loop and adjusts runStartTime and elapsedBeforeRun to preserve correct elapsed time across pauses. A loopKey signal is used to restart the createEffect-driven animation loop after resume.

Confidence Score: 4/5

Safe to merge after fixing the double-pause timer bug; the view change is clean.

When the user switches tabs, both visibilitychange and window blur fire, causing pauseGame to run twice. The second call re-reads getElapsedMsNow() with the still-original runStartTime, inflating elapsedBeforeRun by roughly the full play-time again. On resume only the gap since the second pause call is compensated, leaving the score approximately 3× the true elapsed time. The view.tsx click-focus fix is correct and risk-free.

apps/frontend/src/features/games/falling-words/engine.ts — specifically the pauseGame function and its interaction with both handleVisibilityChange and handleWindowBlur.

Important Files Changed

Filename Overview
apps/frontend/src/features/games/falling-words/engine.ts Introduces pause/resume logic for tab-switch and window-blur events, but pauseGame is called twice on every tab switch (visibilitychange then blur), corrupting elapsedBeforeRun and inflating the score by roughly 3× after a single pause/resume cycle.
apps/frontend/src/features/games/falling-words/view.tsx Adds onClick={actions.focusInput} to the game container so clicks on the GameOver overlay propagate up and refocus the hidden input, matching the existing survival game pattern. Change is minimal and correct.

Reviews (2): Last reviewed commit: "fix: GameOver overlay blocks click-to-fo..." | Re-trigger Greptile

endGame();
return;
}
if (!document.hidden) setTimeout(focusInput, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unconditional focus grab on tab return

setTimeout(focusInput, 0) fires for every visibility-change to visible, regardless of which element held focus before the user switched tabs. If a player had keyboard-tabbed to a header or footer link and then alt-tabbed to another app, returning to the page will silently move focus back to the hidden game input. The same pattern is added to survival/engine.ts, so both games are affected. Consider only calling focusInput when document.activeElement is not an interactive element outside the game container, or when no focused element exists (i.e. !document.activeElement || document.activeElement === document.body).

Root cause: Falling words game container had no onClick handler.
GameOver overlay (z-20) blocked clicks from reaching Field's
onFieldClick, so focusInput() never fired — Enter restart was dead.

Also: visibilitychange and blur were calling endGame(), but the
user expects the game to resume when returning from GitHub.

Fix:
- Add onClick to game container div so overlay clicks refocus input
- Pause game loop on visibilitychange/blur instead of ending
- Resume loop on visibilitychange/focus with adjusted timing
- Add loopKey signal to force createEffect re-run on resume
Comment on lines +275 to +280
const pauseGame = () => {
if (phase() !== "running") return;
stopLoop();
elapsedBeforeRun = getElapsedMsNow();
pauseStartTime = performance.now();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Double-pause on tab switch corrupts elapsed time and score

When the user switches tabs, the browser fires visibilitychange (hidden) first, then window blur — so pauseGame is called twice in quick succession. The second call overwrites elapsedBeforeRun with getElapsedMsNow(), which at that point returns the already-saved elapsedBeforeRun plus the full performance.now() - runStartTime delta (still using the original runStartTime). This roughly doubles the recorded elapsed time before resume even starts. Then resumeGame only compensates for the gap since the second pause call, not the first, so the final in-game timer shows approximately 3× the actual play time.

Concrete example: 5 s of play, 3 s tab-away → on return the timer reads ~15 s instead of 5 s.

The simplest fix is to make pauseGame idempotent — skip the elapsedBeforeRun update if a pause is already in progress:

const pauseGame = () => {
  if (phase() !== "running") return;
  if (pauseStartTime !== 0) return; // already paused
  stopLoop();
  elapsedBeforeRun = getElapsedMsNow();
  pauseStartTime = performance.now();
};

Alternatively, guard handleWindowBlur with if (document.hidden) return; so it short-circuits when the tab is already hidden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Falling Words: Tab+Enter steals focus to GitHub link, breaks 'Enter to restart' on return

1 participant