Skip to content

Commit 26f3703

Browse files
committed
Fixed AutoScroll
1 parent 0182239 commit 26f3703

File tree

2 files changed

+110
-17
lines changed

2 files changed

+110
-17
lines changed

packages/app/src/app/race/_components/race/code.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { cn } from "@/lib/utils";
2+
import React from "react";
23

34
type CodeProps = {
45
code: string;
56
input: string;
7+
preRef: React.MutableRefObject<HTMLPreElement | null>;
68
};
79

8-
export default function Code({
9-
code,
10-
input,
11-
}: CodeProps) {
10+
export default function Code({ code, input, preRef }: CodeProps) {
1211
const array: number[] = [];
1312
const currentCharacter = input.slice(-1);
1413
const expectedCharacter = code.charAt(input.length - 1);
@@ -20,8 +19,9 @@ export default function Code({
2019
return (
2120
<>
2221
<pre
23-
className="text-monochrome mb-4 overflow-auto font-medium px-2 w-full"
22+
className="text-monochrome mb-4 overflow-auto font-medium w-full"
2423
data-cy="code-snippet-preformatted"
24+
ref={preRef}
2525
>
2626
{code.split("").map((char, index) => (
2727
<span
@@ -31,8 +31,7 @@ export default function Code({
3131
code[index] !== " " && array.includes(index),
3232
"border-red-500 opacity-100":
3333
code[index] === " " && array.includes(index),
34-
"bg-yellow-200 opacity-80 text-black":
35-
input.length === index,
34+
"bg-yellow-200 opacity-80 text-black": input.length === index,
3635
"opacity-100": input.length !== index && input[index] === char,
3736
})}
3837
>

packages/app/src/app/race/_components/race/race-practice.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { ChartTimeStamp } from "./types";
2121
import type { ReplayTimeStamp } from "./types";
2222
import { catchError } from "@/lib/utils";
2323
import { useCheckForUserNavigator } from "@/lib/user-system";
24+
import { useEffectOnce } from "react-use";
2425

2526
type RacePracticeProps = {
2627
user?: User;
@@ -33,19 +34,37 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
3334
const [totalErrors, setTotalErrors] = useState(0);
3435
const [chartTimeStamp, setChartTimeStamp] = useState<ChartTimeStamp[]>([]);
3536
const [replayTimeStamp, setReplayTimeStamp] = useState<ReplayTimeStamp[]>([]);
36-
37+
const [windowStart, setWindowStart] = useState<number>(0);
38+
const [windowEnd, setWindowEnd] = useState<number>(0);
39+
const [scrollPosition, setScrollPosition] = useState(0);
3740
const inputElement = useRef<HTMLInputElement | null>(null);
3841
const code = snippet.code.trimEnd();
3942
const router = useRouter();
4043
const isRaceFinished = input === code;
4144

4245
const isUserOnAdroid = useCheckForUserNavigator("android");
43-
46+
//for auto scroll
47+
const preElement = useRef<HTMLPreElement | null>(null);
48+
const scrollUpperLimit = 7;
49+
const scrollLowerLimit = 7 - 1; //1 is deducted because when scrolling backwards, we count from the left side
50+
const spanElementWidth = 10.4;
4451
useEffect(() => {
4552
localStorage.removeItem("chartTimeStamp");
4653
if (!inputElement.current) return;
4754
inputElement.current.focus();
4855
});
56+
useEffectOnce(() => {
57+
if (preElement.current) {
58+
setWindowEnd(
59+
Math.floor(
60+
preElement.current?.getBoundingClientRect().width / spanElementWidth
61+
)
62+
);
63+
}
64+
});
65+
useEffect(() => {
66+
preElement.current?.scrollTo(scrollPosition, 0);
67+
}, [scrollPosition]);
4968

5069
useEffect(() => {
5170
if (isRaceFinished) {
@@ -63,7 +82,7 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
6382
cpm: calculateCPM(input.length, timeTaken),
6483
time: Date.now(),
6584
},
66-
]),
85+
])
6786
);
6887

6988
localStorage.setItem(
@@ -75,7 +94,7 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
7594
textIndicatorPosition: input.length,
7695
time: Date.now(),
7796
},
78-
]),
97+
])
7998
);
8099

81100
if (user) {
@@ -120,6 +139,8 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
120139
}
121140

122141
function handleKeyboardDownEvent(e: React.KeyboardEvent<HTMLInputElement>) {
142+
//scroll to the cursor position
143+
preElement.current?.scrollTo(scrollPosition, 0);
123144
// For ANDROID.
124145
// since the enter button on a mobile keyboard/keypad actually
125146
// returns a e.key of "Enter" onkeydown, we just set a condition for that.
@@ -131,11 +152,11 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
131152
}
132153
if (input !== code.slice(0, input.length)) return;
133154
Enter();
134-
break;
155+
break;
135156
// this is to delete the characters when "Enter" is pressed;
136157
case "Backspace":
137158
Backspace();
138-
break;
159+
break;
139160
}
140161
return;
141162
}
@@ -244,9 +265,40 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
244265
if (input.length === 0) {
245266
return;
246267
}
247-
setInput((prevInput) => prevInput.slice(0, -1));
248-
249268
const character = input.slice(-1);
269+
setInput((prevInput) => {
270+
const updatedInput = prevInput.slice(0, -1);
271+
272+
if (character === "\n") {
273+
const lineNumber = updatedInput.split("\n").length;
274+
const totalCharactersInput = Number(
275+
updatedInput.split("\n")[lineNumber - 1]?.length
276+
);
277+
console.log({
278+
totalCharactersInput,
279+
scrollUpperLimit,
280+
spanElementWidth,
281+
});
282+
setScrollPosition(
283+
(totalCharactersInput -
284+
Math.floor(
285+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
286+
(preElement.current?.getBoundingClientRect().width as any) /
287+
spanElementWidth
288+
) +
289+
scrollUpperLimit +
290+
1) *
291+
spanElementWidth
292+
// spanElement?.current?.getBoundingClientRect().width
293+
);
294+
setWindowEnd(totalCharactersInput + scrollUpperLimit + 1);
295+
setWindowStart(totalCharactersInput + scrollUpperLimit + 1 - 67);
296+
} else {
297+
handleScrollNegative(updatedInput);
298+
}
299+
return updatedInput;
300+
});
301+
250302
if (character !== " " && character !== "\n") {
251303
setChartTimeStamp((prevArray) => prevArray.slice(0, -1));
252304
}
@@ -268,6 +320,18 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
268320
indent += " ";
269321
i++;
270322
}
323+
preElement.current?.scrollTo(0, 0);
324+
325+
setWindowEnd(
326+
Math.floor(
327+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
328+
(preElement?.current?.getBoundingClientRect().width as any) /
329+
spanElementWidth
330+
)
331+
);
332+
333+
setWindowStart(0);
334+
setScrollPosition(0);
271335
setInput((prevInput) => prevInput + "\n" + indent);
272336
}
273337
}
@@ -276,7 +340,13 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
276340
if (e.key !== code.slice(input.length, input.length + 1)) {
277341
setTotalErrors((prevTotalErrors) => prevTotalErrors + 1);
278342
}
279-
setInput((prevInput) => prevInput + e.key);
343+
setInput((prevInput) => {
344+
const updated = prevInput + e.key;
345+
346+
handleScrollPositive(updated);
347+
console.log({ TotalChar: updated.length });
348+
return updated;
349+
});
280350

281351
if (e.key !== " ") {
282352
const currTime = Date.now();
@@ -309,6 +379,30 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
309379
setChartTimeStamp([]);
310380
}
311381

382+
function handleScrollPositive(updatedInput: string) {
383+
const lineNumber = input.split("\n").length;
384+
const totalCharactersInput = Number(
385+
updatedInput.split("\n")[lineNumber - 1]?.length
386+
);
387+
if (windowEnd - totalCharactersInput <= scrollUpperLimit) {
388+
setWindowEnd((previousValue) => previousValue + 1);
389+
setWindowStart((prev) => prev + 1);
390+
setScrollPosition((prev) => prev + spanElementWidth);
391+
}
392+
}
393+
function handleScrollNegative(updatedInput: string) {
394+
const lineNumber = input.split("\n").length;
395+
const totalCharactersInput = Number(
396+
updatedInput.split("\n")[lineNumber - 1]?.length
397+
);
398+
399+
if (totalCharactersInput - windowStart <= scrollLowerLimit) {
400+
setWindowStart((previousValue) => previousValue - 1);
401+
setWindowEnd((previousValue) => previousValue - 1);
402+
setScrollPosition((prev) => prev - spanElementWidth);
403+
}
404+
}
405+
312406
return (
313407
<div
314408
className="relative flex flex-col w-[clamp(10rem,95%,50rem)] gap-2 p-4 mx-auto rounded-md lg:p-8 bg-accent"
@@ -325,7 +419,7 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) {
325419
<Header user={user} snippet={snippet} handleRestart={handleRestart} />
326420
<section className="flex">
327421
<LineNumbers code={code} currentLineNumber={input.split("\n").length} />
328-
<Code code={code} input={input} />
422+
<Code code={code} input={input} preRef={preElement} />
329423
<input
330424
type="text"
331425
ref={inputElement}

0 commit comments

Comments
 (0)