From 0f0cc3a3574e54d4204265eb983ecebdc0a55295 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 8 Sep 2026 13:07:38 +0700 Subject: [PATCH] fix(online): leave focus alone while a player is typing in chat A turn arriving pulled focus into the word field even mid-sentence in the chat, dropping the rest of the message into the game. Focus is now only claimed when no other text field holds it; syllable seeding is unchanged. --- web/e2e/pvp-game.spec.js | 18 ++++++++++++++++++ web/src/lib/components/WordInput.svelte | 21 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/web/e2e/pvp-game.spec.js b/web/e2e/pvp-game.spec.js index 0b6feb4..3f002f9 100644 --- a/web/e2e/pvp-game.spec.js +++ b/web/e2e/pvp-game.spec.js @@ -313,6 +313,24 @@ test.describe('playing a stranger', () => { await close(); }); + test('a turn arriving does not take the chat field away', async ({ browser }) => { + const { host, guest, close } = await playingPair(browser); + + // The guest starts typing while the host is still on turn. The word + // field wants focus the moment a turn lands, and taking it here would + // drop the rest of the sentence into the game. + await chat(guest).input.click(); + await chat(guest).input.fill('đang gõ dở'); + + await playLegalMove(host, new Set(await chainWords(host))); + await waitForMyTurn(guest); + + await expect(chat(guest).input).toBeFocused(); + await expect(chat(guest).input).toHaveValue('đang gõ dở'); + + await close(); + }); + test('a message is rendered as text, never as markup', async ({ browser }) => { const { host, guest, close } = await twoPlayers(browser); diff --git a/web/src/lib/components/WordInput.svelte b/web/src/lib/components/WordInput.svelte index 3cafae5..a7d034f 100644 --- a/web/src/lib/components/WordInput.svelte +++ b/web/src/lib/components/WordInput.svelte @@ -25,6 +25,23 @@ /** @type {unknown} */ let seededRejection = null; + /** + * Whether the player is typing somewhere else — the chat, in practice, + * which now sits beside the board rather than folded away under it. + * + * Taking focus off a field somebody is mid-sentence in would drop the rest + * of that sentence into the word field, so a turn arriving is allowed to + * ask for focus only when nothing else holds it. + */ + function typingElsewhere() { + const active = document.activeElement; + if (!active || active === field) return false; + return ( + active instanceof HTMLElement && + (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable) + ); + } + // Focus when the turn arrives, so a player on a phone can type without // reaching for the field, and seed it with the syllable the word has to // start with — that part of the answer is already decided, and typing it @@ -37,7 +54,9 @@ const syllable = game.state.currentSyllable; const rejection = game.state.rejection; - field?.focus(); + // The seeding below happens either way: it writes into the field + // without disturbing wherever the player actually is. + if (!typingElsewhere()) field?.focus(); if (!field || (turn === seededTurn && rejection === seededRejection)) return; seededTurn = turn; seededRejection = rejection;