diff --git a/frontend/src/ts/input/listeners/composition.ts b/frontend/src/ts/input/listeners/composition.ts index e3388a557..81c81dfd3 100644 --- a/frontend/src/ts/input/listeners/composition.ts +++ b/frontend/src/ts/input/listeners/composition.ts @@ -6,6 +6,9 @@ import { setLastInsertCompositionTextData } from "../state"; import * as CompositionDisplay from "../../elements/composition-display"; import { onInsertText } from "../handlers/insert-text"; import * as TestUI from "../../test/test-ui"; +import * as TestWords from "../../test/test-words"; +import * as TestInput from "../../test/test-input"; +import * as Strings from "../../utils/strings"; const inputEl = getInputElement(); @@ -31,8 +34,22 @@ inputEl.addEventListener("compositionupdate", (event) => { }); if (TestState.testRestarting || TestUI.resultCalculating) return; - CompositionState.setData(event.data); - CompositionDisplay.update(event.data); + const currentWord = TestWords.words.getCurrent(); + const typedSoFar = TestInput.input.current; + const remainingChars = + Strings.splitIntoCharacters(currentWord).length - + Strings.splitIntoCharacters(typedSoFar).length; + // Prevent rendering more composition glyphs than the word has remaining letters, + // so IME preedit strings (e.g. romaji) don't push text to the next line. + const limitedCompositionData = + remainingChars > 0 + ? Strings.splitIntoCharacters(event.data) + .slice(0, remainingChars) + .join("") + : ""; + + CompositionState.setData(limitedCompositionData); + CompositionDisplay.update(limitedCompositionData); }); inputEl.addEventListener("compositionend", async (event) => { diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 86dba54a5..53aa31e50 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -805,15 +805,18 @@ export async function updateWordLetters({ for (let i = 0; i < compositionData.length; i++) { const compositionChar = compositionData[i]; - let charToShow = - currentWordChars[input.length + i] ?? compositionChar; + // Render the target character (if known) during composition to keep line width stable, + // falling back to the preedit char when beyond the word length. + const targetChar = currentWordChars[input.length + i]; + let charToShow = targetChar ?? compositionChar; if (Config.compositionDisplay === "replace") { - charToShow = compositionChar === " " ? "_" : compositionChar; + charToShow = + targetChar ?? (compositionChar === " " ? "_" : compositionChar); } let correctClass = ""; - if (compositionChar === currentWordChars[input.length + i]) { + if (compositionChar === targetChar) { correctClass = "correct"; }