refactor: test word centering behavior, words input position update (@miodc) (#6962)

brrr
This commit is contained in:
Jack 2025-09-17 19:03:01 +02:00 committed by GitHub
parent 10130d7348
commit 16ad0e285a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 94 additions and 81 deletions

View file

@ -4,7 +4,6 @@ import * as TestInput from "./test-input";
import * as SlowTimer from "../states/slow-timer";
import * as TestState from "../test/test-state";
import * as TestWords from "./test-words";
import { prefersReducedMotion } from "../utils/misc";
import { convertRemToPixels } from "../utils/numbers";
import { splitIntoCharacters, getWordDirection } from "../utils/strings";
import { safeNumber } from "@monkeytype/util/numbers";
@ -262,25 +261,6 @@ export async function updatePosition(noAnim = false): Promise<void> {
jqcaret
.stop(true, false)
.animate(animation, SlowTimer.get() || noAnim ? 0 : smoothCaretSpeed);
if (Config.showAllLines) {
const browserHeight = window.innerHeight;
const middlePos = browserHeight / 2 - (jqcaret.outerHeight() as number) / 2;
const contentHeight = document.body.scrollHeight;
if (
newTop >= middlePos &&
contentHeight > browserHeight &&
TestState.isActive
) {
const newscrolltop = newTop - middlePos / 2;
window.scrollTo({
left: 0,
top: newscrolltop,
behavior: prefersReducedMotion() ? "instant" : "smooth",
});
}
}
}
function updateStyle(): void {

View file

@ -1074,7 +1074,11 @@ export async function update(
$("#result"),
250,
async () => {
$("#result").trigger("focus");
const result = document.querySelector<HTMLElement>("#result");
result?.focus({
preventScroll: true,
});
Misc.scrollToCenterOrTop(result);
void AdController.renderResult();
TestUI.setResultCalculating(false);
$("#words").empty();

View file

@ -155,13 +155,45 @@ export function reset(): void {
}
export function focusWords(): void {
$("#wordsInput").trigger("focus");
const wordsInput = document.querySelector<HTMLElement>("#wordsInput");
wordsInput?.blur();
wordsInput?.focus({
preventScroll: true,
});
if (TestState.isActive) {
keepWordsInputInTheCenter(true);
} else {
const typingTest = document.querySelector<HTMLElement>("#typingTest");
Misc.scrollToCenterOrTop(typingTest);
}
}
export function blurWords(): void {
$("#wordsInput").trigger("blur");
}
export function keepWordsInputInTheCenter(force = false): void {
const wordsInput = document.querySelector<HTMLElement>("#wordsInput");
const wordsWrapper = document.querySelector<HTMLElement>("#wordsWrapper");
if (!wordsInput || !wordsWrapper) return;
const wordsWrapperHeight = wordsWrapper.offsetHeight;
const windowHeight = window.innerHeight;
// dont do anything if the wrapper can fit on screen
if (wordsWrapperHeight < windowHeight) return;
const wordsInputRect = wordsInput.getBoundingClientRect();
const wordsInputBelowCenter = wordsInputRect.top > windowHeight / 2;
// dont do anything if its above or at the center unless forced
if (!wordsInputBelowCenter && !force) return;
wordsInput.scrollIntoView({
block: "center",
});
}
export function getWordElement(index: number): HTMLElement | null {
const el = document.querySelector<HTMLElement>(
`#words .word[data-wordindex='${index}']`
@ -197,9 +229,8 @@ export function updateActiveElement(
activeWordTop = newActiveWord.offsetTop;
if (!initial && shouldUpdateWordsInputPosition()) {
void updateWordsInputPosition();
}
void updateWordsInputPosition();
if (!initial && Config.tapeMode !== "off") {
void scrollTape();
}
@ -448,7 +479,7 @@ function updateWordWrapperClasses(): void {
updateWordsWidth();
updateWordsWrapperHeight(true);
updateWordsMargin(updateWordsInputPosition, [true]);
updateWordsMargin(updateWordsInputPosition, []);
}
export function showWords(): void {
@ -478,65 +509,54 @@ export function appendEmptyWordElement(
`<div class='word' data-wordindex='${index}'><letter class='invisible'>_</letter></div>`
);
}
const posUpdateLangList = ["japanese", "chinese", "korean"];
function shouldUpdateWordsInputPosition(): boolean {
const language = posUpdateLangList.some((l) => Config.language.startsWith(l));
return language || (Config.mode !== "time" && Config.showAllLines);
}
export async function updateWordsInputPosition(initial = false): Promise<void> {
if (ActivePage.get() !== "test") return;
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
const isLanguageRTL = currentLanguage.rightToLeft;
const el = document.querySelector<HTMLElement>("#wordsInput");
if (!el) return;
const activeWord = getActiveWordElement();
if (!activeWord) {
el.style.top = "0px";
el.style.left = "0px";
return;
let updateWordsInputPositionAnimationFrameId: null | number = null;
export async function updateWordsInputPosition(): Promise<void> {
if (updateWordsInputPositionAnimationFrameId !== null) {
cancelAnimationFrame(updateWordsInputPositionAnimationFrameId);
}
updateWordsInputPositionAnimationFrameId = requestAnimationFrame(async () => {
updateWordsInputPositionAnimationFrameId = null;
if (ActivePage.get() !== "test") return;
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
const isLanguageRTL = currentLanguage.rightToLeft;
const computed = window.getComputedStyle(activeWord);
const activeWordMargin =
parseInt(computed.marginTop) + parseInt(computed.marginBottom);
const el = document.querySelector<HTMLElement>("#wordsInput");
const letterHeight = convertRemToPixels(Config.fontSize);
const targetTop =
activeWord.offsetTop + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border
if (!el) return;
if (Config.tapeMode !== "off") {
el.style.maxWidth = `${100 - Config.tapeMargin}%`;
} else {
el.style.maxWidth = "";
}
if (activeWord.offsetWidth < letterHeight) {
el.style.width = letterHeight + "px";
} else {
el.style.width = activeWord.offsetWidth + "px";
}
const activeWord = getActiveWordElement();
if (!activeWord) {
el.style.top = "0px";
el.style.left = "0px";
return;
}
const letterHeight = convertRemToPixels(Config.fontSize);
const targetTop =
activeWord.offsetTop + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border
if (Config.tapeMode !== "off") {
el.style.maxWidth = `${100 - Config.tapeMargin}%`;
} else {
el.style.maxWidth = "";
}
if (activeWord.offsetWidth < letterHeight) {
el.style.width = letterHeight + "px";
} else {
el.style.width = activeWord.offsetWidth + "px";
}
if (
initial &&
!shouldUpdateWordsInputPosition() &&
Config.tapeMode === "off"
) {
el.style.top = targetTop + letterHeight + activeWordMargin + 4 + "px";
} else {
el.style.top = targetTop + "px";
}
if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
el.style.left = activeWord.offsetLeft - letterHeight + "px";
} else {
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
}
if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
el.style.left = activeWord.offsetLeft - letterHeight + "px";
} else {
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
}
keepWordsInputInTheCenter();
});
}
let centeringActiveLine: Promise<void> = Promise.resolve();

View file

@ -107,9 +107,7 @@ const debouncedEvent = debounce(250, () => {
}
setTimeout(() => {
void TestUI.updateWordsInputPosition();
if ($("#wordsInput").is(":focus")) {
Caret.show(true);
}
TestUI.focusWords();
}, 250);
}
});

View file

@ -746,4 +746,15 @@ export function isMacLike(): boolean {
return isPlatform(/Mac|iPod|iPhone|iPad/);
}
export function scrollToCenterOrTop(el: HTMLElement | null): void {
if (!el) return;
const elementHeight = el.offsetHeight;
const windowHeight = window.innerHeight;
el.scrollIntoView({
block: elementHeight < windowHeight ? "center" : "start",
});
}
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES