From ba7783c52c3ed264252ad0c68e4bfdf19503db9f Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 16 Jul 2026 18:21:03 +0200 Subject: [PATCH] chore: solid out of focus warning --- frontend/src/html/pages/test.html | 7 +--- frontend/src/styles/test.scss | 15 -------- frontend/src/ts/commandline/commandline.ts | 6 ++-- frontend/src/ts/components/mount.tsx | 2 ++ .../pages/test/CompositionDisplay.tsx | 3 +- .../pages/test/OutOfFocusWarning.tsx | 34 ++++++++++++++++++ frontend/src/ts/states/test.ts | 33 +++++++++++++++-- frontend/src/ts/test/out-of-focus.ts | 30 ---------------- frontend/src/ts/test/test-ui.ts | 36 ++++++++++++------- 9 files changed, 95 insertions(+), 71 deletions(-) create mode 100644 frontend/src/ts/components/pages/test/OutOfFocusWarning.tsx delete mode 100644 frontend/src/ts/test/out-of-focus.ts diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 77c9b68d98fa..b2b1e1606fb0 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -49,12 +49,7 @@ list="autocompleteOff" spellcheck="false" > - +
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index 0169242f3685..c946b6347ea4 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -1342,21 +1342,6 @@ pointer-events: none; opacity: 0; } - .outOfFocusWarning { - display: flex; - place-content: center; - align-items: center; - gap: 0.5em; - text-align: center; - height: 100%; - align-content: center; - font-size: 1rem; - z-index: 999; - position: absolute; - width: 100%; - user-select: none; - pointer-events: none; - } #liveStatsMini { width: 0; diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 6ef92d7e36d1..40a09f4a5db7 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -6,7 +6,6 @@ import * as ThemeController from "../controllers/theme-controller"; import { clearFontPreview } from "../ui"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { showNoticeNotification } from "../states/notifications"; -import * as OutOfFocus from "../test/out-of-focus"; import { getActivePage, getCommandlineSubgroup, @@ -34,6 +33,7 @@ import { isModalOpen, } from "../states/modals"; import { ValidationResult } from "../types/validation"; +import { setTestFocusState } from "../states/test"; type CommandlineMode = "search" | "input"; type InputModeParams = { @@ -74,14 +74,14 @@ let lastState: function removeCommandlineBackground(): void { qs("#commandLine")?.addClass("noBackground"); if (Config.showOutOfFocusWarning) { - OutOfFocus.hide(); + setTestFocusState("focused"); } } function addCommandlineBackground(): void { qs("#commandLine")?.removeClass("noBackground"); if (!isInputElementFocused()) { - OutOfFocus.show(); + setTestFocusState("unfocused"); } } diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index b269097f4ced..bcc5a5ce4d93 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -27,6 +27,7 @@ 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"; +import { OutOfFocusWarning } from "./pages/test/OutOfFocusWarning"; import { TestConfig } from "./pages/test/TestConfig"; import { Popups } from "./popups/Popups"; @@ -56,6 +57,7 @@ const components: Record JSXElement> = { accountsettingspage: () => , keymap: () => , monkey: () => , + outoffocuswarning: () => , }; function mountToMountpoint(name: string, component: () => JSXElement): void { diff --git a/frontend/src/ts/components/pages/test/CompositionDisplay.tsx b/frontend/src/ts/components/pages/test/CompositionDisplay.tsx index fa16a8c19d47..f864d1562a7c 100644 --- a/frontend/src/ts/components/pages/test/CompositionDisplay.tsx +++ b/frontend/src/ts/components/pages/test/CompositionDisplay.tsx @@ -1,10 +1,11 @@ import { Show } from "solid-js"; import { getConfig } from "../../../config/store"; -import { getCompositionText, isOutOfFocus } from "../../../states/test"; +import { getCompositionText, testFocusState } from "../../../states/test"; import { cn } from "../../../utils/cn"; export function CompositionDisplay() { + const isOutOfFocus = () => testFocusState() !== "focused"; return (
+ testFocusState() === "unfocusedWindow" + ? "Click anywhere to focus the window" + : "Click here or press any key to focus"; + + return ( + +
+
+ +
+
{message()}
+
+
+ ); +} diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts index 5212f3b47252..56bbed8d1190 100644 --- a/frontend/src/ts/states/test.ts +++ b/frontend/src/ts/states/test.ts @@ -15,6 +15,7 @@ import { canQuickRestart } from "../utils/quick-restart"; import { replaceUnderscoresWithSpaces } from "../utils/strings"; import { getActivePage, getCustomTextIndicator } from "./core"; import { useResourceWithPromise } from "../hooks/useResourceWithPromise"; +import { clearTimeouts } from "../utils/misc"; export const [wordsHaveNewline, setWordsHaveNewline] = createSignal(false); export const [wordsHaveTab, setWordsHaveTab] = createSignal(false); @@ -24,9 +25,35 @@ 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); +// #words is still vanilla so it's blurred imperatively (see test/test-ui); +// the Solid-owned composition display + OutOfFocusWarning read this signal. +const outOfFocusTimeouts: (number | NodeJS.Timeout)[] = []; +export type TestFocusState = "focused" | "unfocused" | "unfocusedWindow"; +export const [testFocusState, { setTestFocusState }] = + createSignalWithSetters("focused")({ + setTestFocusState: (set, val: TestFocusState) => { + if (val === "focused") { + clearTimeouts(outOfFocusTimeouts); + set(val); + } else { + outOfFocusTimeouts.push( + setTimeout(() => { + set(val); + }, 1000), + ); + } + }, + }); + +export const showOutOfFocusWarning = createMemo( + () => getConfig.showOutOfFocusWarning && testFocusState() !== "focused", +); + +// max-height of the warning, kept in sync with the words wrapper by test-ui. +export const [outOfFocusMaxHeight, setOutOfFocusMaxHeight] = createSignal< + number | undefined +>(undefined); + // live IME composition text, pushed from the compositionupdate/end events. export const [getCompositionText, setCompositionText] = createSignal(""); export const [isTestInvalid, setIsTestInvalid] = createSignal(false); diff --git a/frontend/src/ts/test/out-of-focus.ts b/frontend/src/ts/test/out-of-focus.ts deleted file mode 100644 index 4d8d77c42457..000000000000 --- a/frontend/src/ts/test/out-of-focus.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Misc from "../utils/misc"; -import { Config } from "../config/store"; -import { qs, qsa } from "../utils/dom"; -import { setOutOfFocus } from "../states/test"; - -const outOfFocusTimeouts: (number | NodeJS.Timeout)[] = []; - -const messages = { - default: "Click here or press any key to focus", - window: "Click anywhere to focus the window", -}; - -export function hide(): void { - qsa("#words")?.setStyle({ transition: "none" })?.removeClass("blurred"); - setOutOfFocus(false); - qs(".outOfFocusWarning")?.hide(); - Misc.clearTimeouts(outOfFocusTimeouts); -} - -export function show(mode: "default" | "window" = "default"): void { - if (!Config.showOutOfFocusWarning) return; - outOfFocusTimeouts.push( - setTimeout(() => { - 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 ea6df1126d55..449ffa0ca468 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -10,7 +10,6 @@ import { getCurrentInput } from "./events/data"; import { getLiveCachedAccuracy } from "./events/live-cache"; import * as CustomText from "./custom-text"; import * as Caret from "./caret"; -import * as OutOfFocus from "./out-of-focus"; import * as Misc from "../utils/misc"; import * as Strings from "../utils/strings"; import { blendTwoHexColors } from "../utils/colors"; @@ -65,8 +64,12 @@ import { isTestActive, resetCurrentLiveStats, setCompositionText, + setOutOfFocusMaxHeight, wordsHaveNewline, + setTestFocusState, + showOutOfFocusWarning, } from "../states/test"; +import { createEffect } from "solid-js"; import { getCorrectedWordsHistory, getInputHistory, @@ -87,6 +90,16 @@ export let activeWordTop = 0; export let activeWordHeight = 0; let wordTopBeforeLineJump = 0; let lineTransition = false; + +// #words is still vanilla; the warning itself is Solid (OutOfFocusWarning.tsx). +// show/hideOutOfFocus live in states/test so commandline needn't import test-ui. +createEffect(() => { + if (showOutOfFocusWarning()) { + wordsEl.setStyle({ transition: "0.25s" })?.addClass("blurred"); + } else { + wordsEl.setStyle({ transition: "none" })?.removeClass("blurred"); + } +}); let currentTestLine = 0; export function focusWords(force = false): void { @@ -404,7 +417,7 @@ function buildWordHTML(word: string, wordIndex: number): string { function updateWordWrapperClasses(): void { // outoffocus applies transition, need to remove it - OutOfFocus.hide(); + setTestFocusState("focused"); if (Config.tapeMode !== "off") { wordsEl.addClass("tape"); @@ -492,7 +505,7 @@ function updateWordWrapperClasses(): void { Caret.updatePosition(); if (!isInputElementFocused()) { - OutOfFocus.show(); + setTestFocusState("unfocused"); } } @@ -607,9 +620,6 @@ export async function centerActiveLine(): Promise { export function updateWordsWrapperHeight(force = false): void { if (getActivePage() !== "test" || TestState.resultVisible) return; if (!force && Config.mode !== "custom") return; - const outOfFocusEl = document.querySelector( - ".outOfFocusWarning", - ) as HTMLElement; const activeWordEl = getActiveWordElement(); if (!activeWordEl) return; @@ -669,7 +679,7 @@ export function updateWordsWrapperHeight(force = false): void { } } - outOfFocusEl.style.maxHeight = `${wordHeight * 3}px`; + setOutOfFocusMaxHeight(wordHeight * 3); } function updateWordsMargin(): void { @@ -1918,7 +1928,7 @@ export function onTestFinish(): void { LiveAcc.hide(); LiveBurst.hide(); TimerProgress.hide(); - OutOfFocus.hide(); + setTestFocusState("focused"); if (Config.playSoundOnClick === "16") { void SoundController.playFartReverb(); } @@ -1982,14 +1992,14 @@ addEventListener("resize", () => { qs("#wordsInput")?.on("focus", (e) => { if (!isInputElementFocused()) return; if (!TestState.resultVisible && Config.showOutOfFocusWarning) { - OutOfFocus.hide(); + setTestFocusState("focused"); } Caret.show(true); }); qs("#wordsInput")?.on("focusout", () => { if (!isInputElementFocused()) { - OutOfFocus.show(); + setTestFocusState("unfocused"); } Caret.hide(); }); @@ -2003,13 +2013,13 @@ qs("#wordsWrapper")?.on("click", () => { }); window.addEventListener("blur", () => { - OutOfFocus.show("window"); + setTestFocusState("unfocusedWindow"); }); // little roadblock for basic cheating document.addEventListener("visibilitychange", () => { if (document.visibilityState !== "hidden") return; - OutOfFocus.show("window"); + setTestFocusState("unfocusedWindow"); }); configEvent.subscribe(({ key, newValue }) => { @@ -2023,7 +2033,7 @@ configEvent.subscribe(({ key, newValue }) => { updateLiveStatsColor(newValue); } if (key === "showOutOfFocusWarning" && !newValue) { - OutOfFocus.hide(); + setTestFocusState("focused"); } if (key === "compositionDisplay" && newValue === "below") { setCompositionText(" ");