diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html
index 040345d69fc3..77c9b68d98fa 100644
--- a/frontend/src/html/pages/test.html
+++ b/frontend/src/html/pages/test.html
@@ -60,7 +60,7 @@
-
+
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss
index d2a97627b7bd..0169242f3685 100644
--- a/frontend/src/styles/test.scss
+++ b/frontend/src/styles/test.scss
@@ -1299,23 +1299,6 @@
padding: 1em 2em;
}
-#compositionDisplay {
- font-size: 0.5em;
- display: flex;
- margin: 1rem auto 0px;
- padding: 1rem 2rem;
- height: 1.25em;
- box-sizing: content-box;
- user-select: none;
- cursor: default;
- color: var(--sub-color);
-
- &.blurred {
- opacity: 0.25;
- filter: blur(4px);
- }
-}
-
.pageTest {
height: 100%;
display: grid;
diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx
index 0fb90c520b10..b269097f4ced 100644
--- a/frontend/src/ts/components/mount.tsx
+++ b/frontend/src/ts/components/mount.tsx
@@ -23,6 +23,7 @@ import { ProfilePage } from "./pages/profile/ProfilePage";
import { ProfileSearchPage } from "./pages/profile/ProfileSearchPage";
import { SettingsPage } from "./pages/settings/SettingsPage";
import { CapsWarning } from "./pages/test/CapsWarning";
+import { CompositionDisplay } from "./pages/test/CompositionDisplay";
import { Keymap } from "./pages/test/Keymap";
import { TestModesNotice } from "./pages/test/modes-notice/TestModesNotice";
import { Monkey } from "./pages/test/Monkey";
@@ -49,6 +50,7 @@ const components: Record JSXElement> = {
commandlinehotkey: () => ,
testmodesnotice: () => ,
capswarning: () => ,
+ compositiondisplay: () => ,
friendspage: () => ,
notfoundpage: () => ,
accountsettingspage: () => ,
diff --git a/frontend/src/ts/components/pages/test/CompositionDisplay.tsx b/frontend/src/ts/components/pages/test/CompositionDisplay.tsx
new file mode 100644
index 000000000000..fa16a8c19d47
--- /dev/null
+++ b/frontend/src/ts/components/pages/test/CompositionDisplay.tsx
@@ -0,0 +1,24 @@
+import { Show } from "solid-js";
+
+import { getConfig } from "../../../config/store";
+import { getCompositionText, isOutOfFocus } from "../../../states/test";
+import { cn } from "../../../utils/cn";
+
+export function CompositionDisplay() {
+ return (
+
+
+ {getCompositionText()}
+
+
+ );
+}
diff --git a/frontend/src/ts/elements/composition-display.ts b/frontend/src/ts/elements/composition-display.ts
deleted file mode 100644
index 987bfef7f2fc..000000000000
--- a/frontend/src/ts/elements/composition-display.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-const compositionDisplay = document.getElementById(
- "compositionDisplay",
-) as HTMLElement;
-
-export function update(data: string): void {
- compositionDisplay.innerText = data;
-}
-
-export function hide(): void {
- compositionDisplay.classList.add("hidden");
-}
-
-export function show(): void {
- compositionDisplay.classList.remove("hidden");
-}
diff --git a/frontend/src/ts/input/listeners/composition.ts b/frontend/src/ts/input/listeners/composition.ts
index 8f7272543782..1b9dc133754c 100644
--- a/frontend/src/ts/input/listeners/composition.ts
+++ b/frontend/src/ts/input/listeners/composition.ts
@@ -3,10 +3,9 @@ import * as CompositionState from "../../legacy-states/composition";
import * as TestState from "../../test/test-state";
import * as TestLogic from "../../test/test-logic";
import { setLastInsertCompositionTextData } from "../state";
-import * as CompositionDisplay from "../../elements/composition-display";
import { onInsertText } from "../handlers/insert-text";
import { logTestEvent } from "../../test/events/data";
-import { isTestActive } from "../../states/test";
+import { isTestActive, setCompositionText } from "../../states/test";
const inputEl = getInputElement();
@@ -40,7 +39,7 @@ inputEl.addEventListener("compositionupdate", (event) => {
if (TestState.testRestarting || TestState.resultCalculating) return;
CompositionState.setData(event.data);
- CompositionDisplay.update(event.data);
+ setCompositionText(event.data);
const now = performance.now();
@@ -57,7 +56,7 @@ inputEl.addEventListener("compositionend", async (event) => {
if (TestState.testRestarting || TestState.resultCalculating) return;
CompositionState.setComposing(false);
CompositionState.setData("");
- CompositionDisplay.update("");
+ setCompositionText("");
setLastInsertCompositionTextData("");
const now = performance.now();
diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts
index ae02ada75acf..5212f3b47252 100644
--- a/frontend/src/ts/states/test.ts
+++ b/frontend/src/ts/states/test.ts
@@ -24,6 +24,11 @@ export const [getLoadedChallenge, setLoadedChallenge] =
createSignal(null);
export const [getResultVisible, setResultVisible] = createSignal(false);
export const [getFocus, setFocus] = createSignal(false);
+// #words is still vanilla so it's blurred imperatively (see test/out-of-focus);
+// the Solid-owned composition display reads this signal instead.
+export const [isOutOfFocus, setOutOfFocus] = createSignal(false);
+// live IME composition text, pushed from the compositionupdate/end events.
+export const [getCompositionText, setCompositionText] = createSignal("");
export const [isTestInvalid, setIsTestInvalid] = createSignal(false);
export const [isLongTest, setIsLongTest] = createSignal(false);
export const [getLastResult, setLastResult] = createSignal {
- qsa("#words, #compositionDisplay")
- ?.setStyle({ transition: "0.25s" })
- ?.addClass("blurred");
+ qsa("#words")?.setStyle({ transition: "0.25s" })?.addClass("blurred");
+ setOutOfFocus(true);
qs(".outOfFocusWarning .text")?.setText(messages[mode]);
qs(".outOfFocusWarning")?.show();
}, 1000),
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 62f2a2b6ca91..ea6df1126d55 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -46,7 +46,6 @@ import {
} from "../input/input-element";
import * as MonkeyPower from "../elements/monkey-power";
import * as SlowTimer from "../legacy-states/slow-timer";
-import * as CompositionDisplay from "../elements/composition-display";
import * as AdController from "../controllers/ad-controller";
import * as Joining from "./break-joining";
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
@@ -65,6 +64,7 @@ import {
getCurrentQuote,
isTestActive,
resetCurrentLiveStats,
+ setCompositionText,
wordsHaveNewline,
} from "../states/test";
import {
@@ -450,9 +450,9 @@ function updateWordWrapperClasses(): void {
wordsEl.removeClass("colorfulMode");
}
- qsa(
- "#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput, #compositionDisplay",
- ).setStyle({ fontSize: `${Config.fontSize}rem` });
+ qsa("#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput").setStyle({
+ fontSize: `${Config.fontSize}rem`,
+ });
if (TestState.isLanguageRightToLeft) {
wordsEl.addClass("rightToLeftTest");
@@ -1905,10 +1905,7 @@ export function onTestRestart(source: "testPage" | "resultPage"): void {
}
AdController.destroyResult();
if (Config.compositionDisplay === "below") {
- CompositionDisplay.update(" ");
- CompositionDisplay.show();
- } else {
- CompositionDisplay.hide();
+ setCompositionText(" ");
}
void SoundController.clearAllSounds();
cancelPendingAnimationFramesStartingWith("test-ui");
@@ -2028,13 +2025,8 @@ configEvent.subscribe(({ key, newValue }) => {
if (key === "showOutOfFocusWarning" && !newValue) {
OutOfFocus.hide();
}
- if (key === "compositionDisplay") {
- if (newValue === "below") {
- CompositionDisplay.update(" ");
- CompositionDisplay.show();
- } else {
- CompositionDisplay.hide();
- }
+ if (key === "compositionDisplay" && newValue === "below") {
+ setCompositionText(" ");
}
if (
["fontSize", "fontFamily", "blindMode", "hideExtraLetters"].includes(