Skip to content

Commit 16ad0e2

Browse files
authored
refactor: test word centering behavior, words input position update (@miodc) (monkeytypegame#6962)
brrr
1 parent 10130d7 commit 16ad0e2

File tree

5 files changed

+92
-79
lines changed

5 files changed

+92
-79
lines changed

frontend/src/ts/test/caret.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as TestInput from "./test-input";
44
import * as SlowTimer from "../states/slow-timer";
55
import * as TestState from "../test/test-state";
66
import * as TestWords from "./test-words";
7-
import { prefersReducedMotion } from "../utils/misc";
87
import { convertRemToPixels } from "../utils/numbers";
98
import { splitIntoCharacters, getWordDirection } from "../utils/strings";
109
import { safeNumber } from "@monkeytype/util/numbers";
@@ -262,25 +261,6 @@ export async function updatePosition(noAnim = false): Promise<void> {
262261
jqcaret
263262
.stop(true, false)
264263
.animate(animation, SlowTimer.get() || noAnim ? 0 : smoothCaretSpeed);
265-
266-
if (Config.showAllLines) {
267-
const browserHeight = window.innerHeight;
268-
const middlePos = browserHeight / 2 - (jqcaret.outerHeight() as number) / 2;
269-
const contentHeight = document.body.scrollHeight;
270-
271-
if (
272-
newTop >= middlePos &&
273-
contentHeight > browserHeight &&
274-
TestState.isActive
275-
) {
276-
const newscrolltop = newTop - middlePos / 2;
277-
window.scrollTo({
278-
left: 0,
279-
top: newscrolltop,
280-
behavior: prefersReducedMotion() ? "instant" : "smooth",
281-
});
282-
}
283-
}
284264
}
285265

286266
function updateStyle(): void {

frontend/src/ts/test/result.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,11 @@ export async function update(
10741074
$("#result"),
10751075
250,
10761076
async () => {
1077-
$("#result").trigger("focus");
1077+
const result = document.querySelector<HTMLElement>("#result");
1078+
result?.focus({
1079+
preventScroll: true,
1080+
});
1081+
Misc.scrollToCenterOrTop(result);
10781082
void AdController.renderResult();
10791083
TestUI.setResultCalculating(false);
10801084
$("#words").empty();

frontend/src/ts/test/test-ui.ts

Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,45 @@ export function reset(): void {
155155
}
156156

157157
export function focusWords(): void {
158-
$("#wordsInput").trigger("focus");
158+
const wordsInput = document.querySelector<HTMLElement>("#wordsInput");
159+
wordsInput?.blur();
160+
wordsInput?.focus({
161+
preventScroll: true,
162+
});
163+
if (TestState.isActive) {
164+
keepWordsInputInTheCenter(true);
165+
} else {
166+
const typingTest = document.querySelector<HTMLElement>("#typingTest");
167+
Misc.scrollToCenterOrTop(typingTest);
168+
}
159169
}
160170

161171
export function blurWords(): void {
162172
$("#wordsInput").trigger("blur");
163173
}
164174

175+
export function keepWordsInputInTheCenter(force = false): void {
176+
const wordsInput = document.querySelector<HTMLElement>("#wordsInput");
177+
const wordsWrapper = document.querySelector<HTMLElement>("#wordsWrapper");
178+
if (!wordsInput || !wordsWrapper) return;
179+
180+
const wordsWrapperHeight = wordsWrapper.offsetHeight;
181+
const windowHeight = window.innerHeight;
182+
183+
// dont do anything if the wrapper can fit on screen
184+
if (wordsWrapperHeight < windowHeight) return;
185+
186+
const wordsInputRect = wordsInput.getBoundingClientRect();
187+
const wordsInputBelowCenter = wordsInputRect.top > windowHeight / 2;
188+
189+
// dont do anything if its above or at the center unless forced
190+
if (!wordsInputBelowCenter && !force) return;
191+
192+
wordsInput.scrollIntoView({
193+
block: "center",
194+
});
195+
}
196+
165197
export function getWordElement(index: number): HTMLElement | null {
166198
const el = document.querySelector<HTMLElement>(
167199
`#words .word[data-wordindex='${index}']`
@@ -197,9 +229,8 @@ export function updateActiveElement(
197229

198230
activeWordTop = newActiveWord.offsetTop;
199231

200-
if (!initial && shouldUpdateWordsInputPosition()) {
201-
void updateWordsInputPosition();
202-
}
232+
void updateWordsInputPosition();
233+
203234
if (!initial && Config.tapeMode !== "off") {
204235
void scrollTape();
205236
}
@@ -448,7 +479,7 @@ function updateWordWrapperClasses(): void {
448479

449480
updateWordsWidth();
450481
updateWordsWrapperHeight(true);
451-
updateWordsMargin(updateWordsInputPosition, [true]);
482+
updateWordsMargin(updateWordsInputPosition, []);
452483
}
453484

454485
export function showWords(): void {
@@ -478,65 +509,54 @@ export function appendEmptyWordElement(
478509
`<div class='word' data-wordindex='${index}'><letter class='invisible'>_</letter></div>`
479510
);
480511
}
512+
let updateWordsInputPositionAnimationFrameId: null | number = null;
513+
export async function updateWordsInputPosition(): Promise<void> {
514+
if (updateWordsInputPositionAnimationFrameId !== null) {
515+
cancelAnimationFrame(updateWordsInputPositionAnimationFrameId);
516+
}
517+
updateWordsInputPositionAnimationFrameId = requestAnimationFrame(async () => {
518+
updateWordsInputPositionAnimationFrameId = null;
519+
if (ActivePage.get() !== "test") return;
520+
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
521+
const isLanguageRTL = currentLanguage.rightToLeft;
481522

482-
const posUpdateLangList = ["japanese", "chinese", "korean"];
483-
function shouldUpdateWordsInputPosition(): boolean {
484-
const language = posUpdateLangList.some((l) => Config.language.startsWith(l));
485-
return language || (Config.mode !== "time" && Config.showAllLines);
486-
}
487-
488-
export async function updateWordsInputPosition(initial = false): Promise<void> {
489-
if (ActivePage.get() !== "test") return;
490-
491-
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
492-
const isLanguageRTL = currentLanguage.rightToLeft;
493-
494-
const el = document.querySelector<HTMLElement>("#wordsInput");
523+
const el = document.querySelector<HTMLElement>("#wordsInput");
495524

496-
if (!el) return;
525+
if (!el) return;
497526

498-
const activeWord = getActiveWordElement();
499-
500-
if (!activeWord) {
501-
el.style.top = "0px";
502-
el.style.left = "0px";
503-
return;
504-
}
527+
const activeWord = getActiveWordElement();
505528

506-
const computed = window.getComputedStyle(activeWord);
507-
const activeWordMargin =
508-
parseInt(computed.marginTop) + parseInt(computed.marginBottom);
529+
if (!activeWord) {
530+
el.style.top = "0px";
531+
el.style.left = "0px";
532+
return;
533+
}
509534

510-
const letterHeight = convertRemToPixels(Config.fontSize);
511-
const targetTop =
512-
activeWord.offsetTop + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border
535+
const letterHeight = convertRemToPixels(Config.fontSize);
536+
const targetTop =
537+
activeWord.offsetTop + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border
513538

514-
if (Config.tapeMode !== "off") {
515-
el.style.maxWidth = `${100 - Config.tapeMargin}%`;
516-
} else {
517-
el.style.maxWidth = "";
518-
}
519-
if (activeWord.offsetWidth < letterHeight) {
520-
el.style.width = letterHeight + "px";
521-
} else {
522-
el.style.width = activeWord.offsetWidth + "px";
523-
}
539+
if (Config.tapeMode !== "off") {
540+
el.style.maxWidth = `${100 - Config.tapeMargin}%`;
541+
} else {
542+
el.style.maxWidth = "";
543+
}
544+
if (activeWord.offsetWidth < letterHeight) {
545+
el.style.width = letterHeight + "px";
546+
} else {
547+
el.style.width = activeWord.offsetWidth + "px";
548+
}
524549

525-
if (
526-
initial &&
527-
!shouldUpdateWordsInputPosition() &&
528-
Config.tapeMode === "off"
529-
) {
530-
el.style.top = targetTop + letterHeight + activeWordMargin + 4 + "px";
531-
} else {
532550
el.style.top = targetTop + "px";
533-
}
534551

535-
if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
536-
el.style.left = activeWord.offsetLeft - letterHeight + "px";
537-
} else {
538-
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
539-
}
552+
if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
553+
el.style.left = activeWord.offsetLeft - letterHeight + "px";
554+
} else {
555+
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
556+
}
557+
558+
keepWordsInputInTheCenter();
559+
});
540560
}
541561

542562
let centeringActiveLine: Promise<void> = Promise.resolve();

frontend/src/ts/ui.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,7 @@ const debouncedEvent = debounce(250, () => {
107107
}
108108
setTimeout(() => {
109109
void TestUI.updateWordsInputPosition();
110-
if ($("#wordsInput").is(":focus")) {
111-
Caret.show(true);
112-
}
110+
TestUI.focusWords();
113111
}, 250);
114112
}
115113
});

frontend/src/ts/utils/misc.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,4 +746,15 @@ export function isMacLike(): boolean {
746746
return isPlatform(/Mac|iPod|iPhone|iPad/);
747747
}
748748

749+
export function scrollToCenterOrTop(el: HTMLElement | null): void {
750+
if (!el) return;
751+
752+
const elementHeight = el.offsetHeight;
753+
const windowHeight = window.innerHeight;
754+
755+
el.scrollIntoView({
756+
block: elementHeight < windowHeight ? "center" : "start",
757+
});
758+
}
759+
749760
// DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES

0 commit comments

Comments
 (0)