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;