Skip to content

Commit b84d5fb

Browse files
committed
feat: rework the typing test page
1 parent c71afb5 commit b84d5fb

15 files changed

+117
-100
lines changed

packages/page-typing-test/lib/TypingTestPage.test.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { KeyboardProvider } from "@keybr/keyboard";
44
import { FakePhoneticModel } from "@keybr/phonetic-model";
55
import { PhoneticModelLoader } from "@keybr/phonetic-model-loader";
66
import { FakeSettingsContext } from "@keybr/settings";
7-
import { fireEvent, render } from "@testing-library/react";
7+
import { render } from "@testing-library/react";
88
import { TypingTestPage } from "./TypingTestPage.tsx";
99

10-
test("render", async () => {
10+
test("render", () => {
1111
PhoneticModelLoader.loader = FakePhoneticModel.loader;
1212

1313
const r = render(
@@ -20,8 +20,5 @@ test("render", async () => {
2020
</FakeIntlProvider>,
2121
);
2222

23-
fireEvent.click(await r.findByTitle("Settings", { exact: false }));
24-
fireEvent.click(await r.findByTitle("Save settings", { exact: false }));
25-
2623
r.unmount();
2724
});
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
11
.line {
2-
display: flex;
3-
align-items: center;
42
border-block-end: var(--separator-border);
53
}
6-
7-
.text {
8-
flex: auto;
9-
}
10-
11-
.stats {
12-
text-align: end;
13-
}
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,12 @@
1-
import { formatDuration, Value } from "@keybr/widget";
21
import { memo, type ReactNode } from "react";
3-
import { type Progress, type SessionLine } from "../session/index.ts";
2+
import { type SessionLine } from "../session/index.ts";
43
import * as styles from "./LineTemplate.module.less";
54

65
export const LineTemplate = memo(function LineTemplate({
76
children,
87
progress,
98
}: {
109
readonly children: ReactNode;
11-
} & SessionLine): ReactNode {
12-
return (
13-
<div className={styles.line}>
14-
<div className={styles.text}>{children}</div>
15-
<div className={styles.stats}>{progress && <Stats {...progress} />}</div>
16-
</div>
17-
);
10+
} & SessionLine) {
11+
return <div className={styles.line}>{children}</div>;
1812
});
19-
20-
function Stats({ length, time, progress }: Progress): ReactNode {
21-
return (
22-
<>
23-
<Value
24-
value={formatDuration(time, { showMillis: true })}
25-
title="Time passed."
26-
/>
27-
{" / "}
28-
<Value value={`${length}`} title="Characters inputted." />
29-
{" / "}
30-
<Value value={`${Math.floor(progress * 100)}%`} title="Progress made." />
31-
</>
32-
);
33-
}

packages/page-typing-test/lib/components/Replay.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import { Box, useDocumentVisibility } from "@keybr/widget";
1212
import { type ReactNode, useEffect, useMemo, useState } from "react";
1313
import { ReplayState, Session, type TestResult } from "../session/index.ts";
1414
import { useCompositeSettings } from "../settings.ts";
15-
import { Progress } from "./Progress.tsx";
1615
import * as styles from "./Replay.module.less";
16+
import { ReplayProgress } from "./ReplayProgress.tsx";
1717

1818
export function Replay({
1919
result: { steps, events },
@@ -29,7 +29,7 @@ export function Replay({
2929
);
3030
return (
3131
<div className={styles.root}>
32-
<Progress stepper={stepper} />
32+
<ReplayProgress stepper={stepper} />
3333
<Box className={styles.text} alignItems="center" justifyContent="center">
3434
<StaticText settings={textDisplay} lines={lines} cursor={true} />
3535
</Box>

packages/page-typing-test/lib/components/Progress.tsx packages/page-typing-test/lib/components/ReplayProgress.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
import { useEffect, useState } from "react";
99
import { type ReplayState } from "../session/index.ts";
1010

11-
export function Progress({ stepper }: { readonly stepper: ReplayState }) {
11+
export function ReplayProgress({ stepper }: { readonly stepper: ReplayState }) {
1212
const { formatSpeed } = useFormatter();
13-
const { progress, time } = useProgress(stepper);
13+
const { progress, time } = useReplayProgress(stepper);
1414
return (
1515
<Para align="center">
1616
<NameValue
@@ -26,7 +26,7 @@ export function Progress({ stepper }: { readonly stepper: ReplayState }) {
2626
);
2727
}
2828

29-
function useProgress(stepper: ReplayState) {
29+
function useReplayProgress(stepper: ReplayState) {
3030
const { state, progress } = stepper;
3131
const [time, setTime] = useState(0);
3232
useEffect(() => {

packages/page-typing-test/lib/components/ReportScreen.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ export function ReportScreen({ result }: { readonly result: TestResult }) {
137137
<Button
138138
label="Next test"
139139
icon={<Icon shape={mdiSkipNext} />}
140-
title="Try another test."
141140
onClick={handleNext}
142141
/>
143142
</Field>
@@ -154,14 +153,12 @@ export function ReportScreen({ result }: { readonly result: TestResult }) {
154153
function Indicator({
155154
name,
156155
value,
157-
title,
158156
}: {
159157
readonly name: ReactNode;
160158
readonly value: ReactNode;
161-
readonly title?: string;
162159
}) {
163160
return (
164-
<div className={styles.indicator} title={title}>
161+
<div className={styles.indicator}>
165162
<div className={styles.indicatorValue}>
166163
<Value>{value}</Value>
167164
</div>

packages/page-typing-test/lib/components/SettingsScreen.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export function SettingsScreen() {
4343
<Button
4444
icon={<Icon shape={mdiCheckCircle} />}
4545
label="Done"
46-
title="Save settings and return to the test."
4746
onClick={() => {
4847
setView("test");
4948
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.root {
2+
position: relative;
3+
background-color: var(--primary-l1);
4+
}
5+
6+
.bar {
7+
position: absolute;
8+
inset-inline-start: 0;
9+
inset-block-start: 0;
10+
inset-block-end: 0;
11+
background-color: var(--primary-d1);
12+
}
13+
14+
.info {
15+
position: relative;
16+
inset: 0;
17+
text-align: center;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useIntlNumbers } from "@keybr/intl";
2+
import { useFormatter } from "@keybr/lesson-ui";
3+
import { formatDuration, Para, Value, withDeferred } from "@keybr/widget";
4+
import { memo } from "react";
5+
import { type Progress } from "../session/index.ts";
6+
import * as styles from "./TestProgress.module.less";
7+
8+
export const TestProgress0 = memo(function TestProgress({
9+
progress: { length, time, progress, speed },
10+
}: {
11+
readonly progress: Progress;
12+
}) {
13+
const { formatNumber, formatPercents } = useIntlNumbers();
14+
const { formatSpeed } = useFormatter();
15+
return (
16+
<Para className={styles.root}>
17+
<div
18+
className={styles.bar}
19+
style={{ inlineSize: `${Math.floor(progress * 100)}%` }}
20+
/>
21+
<div className={styles.info}>
22+
<Value value={formatDuration(time, { showMillis: true })} />
23+
{" / "}
24+
<Value value={formatNumber(length)} />
25+
{" / "}
26+
<Value value={formatPercents(progress)} />
27+
{" / "}
28+
<Value value={formatSpeed(speed)} />
29+
</div>
30+
</Para>
31+
);
32+
});
33+
34+
export const TestProgress = withDeferred(TestProgress0);
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.text {
2+
max-inline-size: 60rem;
23
margin: -1rem;
34
padding: 1rem;
5+
overflow: hidden;
46
backdrop-filter: blur(10px);
57
}

packages/page-typing-test/lib/components/TestScreen.tsx

+33-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Screen } from "@keybr/pages-shared";
22
import { type LineList, makeStats } from "@keybr/textinput";
33
import { TextArea } from "@keybr/textinput-ui";
4-
import { type Focusable, Spacer, useView } from "@keybr/widget";
4+
import { Box, type Focusable, Spacer, useView } from "@keybr/widget";
55
import { useEffect, useRef, useState } from "react";
66
import {
77
type TextGenerator,
@@ -11,6 +11,7 @@ import { Session, type TestResult } from "../session/index.ts";
1111
import { type CompositeSettings, useCompositeSettings } from "../settings.ts";
1212
import { views } from "../views.tsx";
1313
import { LineTemplate } from "./LineTemplate.tsx";
14+
import { TestProgress } from "./TestProgress.tsx";
1415
import * as styles from "./TestScreen.module.less";
1516
import { Toolbar } from "./Toolbar.tsx";
1617

@@ -34,10 +35,9 @@ function Controller({
3435
const { setView } = useView(views);
3536
const settings = useCompositeSettings();
3637
const focusRef = useRef<Focusable>(null);
37-
const [session, setSession] = useState<Session>(() =>
38-
nextTest(settings, generator),
39-
);
38+
const [session, setSession] = useState(() => nextTest(settings, generator));
4039
const [lines, setLines] = useState<LineList>(Session.emptyLines);
40+
const [progress, setProgress] = useState(Session.emptyProgress);
4141
useEffect(() => {
4242
generator.reset(mark);
4343
const session = nextTest(settings, generator);
@@ -55,36 +55,40 @@ function Controller({
5555
}}
5656
/>
5757
<Spacer size={10} />
58-
<div className={styles.text}>
59-
<TextArea
60-
focusRef={focusRef}
61-
settings={settings.textDisplay}
62-
lines={lines}
63-
wrap={false}
64-
onFocus={() => {
65-
generator.reset(mark);
66-
const session = nextTest(settings, generator);
67-
setSession(session);
68-
setLines(session.getLines());
69-
}}
70-
onKeyDown={session.handleKeyDown}
71-
onKeyUp={session.handleKeyUp}
72-
onInput={(event) => {
73-
const { completed } = session.handleInput(event);
74-
setLines(session.getLines());
75-
if (completed) {
76-
setView("report", { result: makeResult(session) });
77-
}
78-
}}
79-
lineTemplate={LineTemplate}
80-
/>
81-
</div>
58+
<Box alignItems="center" justifyContent="center">
59+
<div className={styles.text}>
60+
<TextArea
61+
focusRef={focusRef}
62+
settings={settings.textDisplay}
63+
lines={lines}
64+
wrap={false}
65+
onFocus={() => {
66+
generator.reset(mark);
67+
const session = nextTest(settings, generator);
68+
setSession(session);
69+
setLines(session.getLines());
70+
}}
71+
onKeyDown={session.handleKeyDown}
72+
onKeyUp={session.handleKeyUp}
73+
onInput={(event) => {
74+
const { progress, completed } = session.handleInput(event);
75+
setLines(session.getLines());
76+
setProgress(progress);
77+
if (completed) {
78+
setView("report", { result: makeResult(session) });
79+
}
80+
}}
81+
lineTemplate={LineTemplate}
82+
/>
83+
<TestProgress progress={progress} />
84+
</div>
85+
</Box>
8286
</Screen>
8387
);
8488
}
8589

8690
function nextTest(settings: CompositeSettings, generator: TextGenerator) {
87-
return new Session({ ...settings, numLines: 7, numCols: 55 }, generator);
91+
return new Session({ ...settings, numLines: 5, numCols: 55 }, generator);
8892
}
8993

9094
function makeResult(session: Session): TestResult {

packages/page-typing-test/lib/components/Toolbar.tsx

+8-14
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,14 @@ export const Toolbar = memo(function Toolbar({
1616
}): ReactNode {
1717
return (
1818
<FieldList>
19+
<Field.Filler />
1920
<Field>
2021
<DurationSwitcher onChange={onChange} />
2122
</Field>
22-
<Field.Filler />
2323
<Field>
24-
<IconButton
25-
icon={<Icon shape={mdiCog} />}
26-
title="Settings..."
27-
onClick={onConfigure}
28-
/>
24+
<IconButton icon={<Icon shape={mdiCog} />} onClick={onConfigure} />
2925
</Field>
26+
<Field.Filler />
3027
</FieldList>
3128
);
3229
});
@@ -35,11 +32,12 @@ export const DurationSwitcher = memo(function DurationSwitcher({
3532
onChange,
3633
}: {
3734
readonly onChange: () => void;
38-
}): ReactNode {
35+
}) {
3936
const { settings, updateSettings } = useSettings();
4037
const compositeSettings = toCompositeSettings(settings);
4138
const children: ReactNode[] = [];
42-
durations.forEach(({ duration, label }, index) => {
39+
for (let index = 0; index < durations.length; index++) {
40+
const { duration, label } = durations[index];
4341
if (index > 0) {
4442
children.push(<span key={children.length}>{" | "}</span>);
4543
}
@@ -66,10 +64,6 @@ export const DurationSwitcher = memo(function DurationSwitcher({
6664
{label}
6765
</Link>,
6866
);
69-
});
70-
return (
71-
<>
72-
<span>Test duration:</span> {children}
73-
</>
74-
);
67+
}
68+
return <>{children}</>;
7569
});

packages/page-typing-test/lib/components/settings/TypingSettings.module.less

-3
This file was deleted.

packages/page-typing-test/lib/components/settings/text/CommonWordsSettings.tsx

-4
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ function Content({ wordList }: { readonly wordList: WordList }): ReactNode {
8181
max={typingTestProps.wordList.wordListSize.max}
8282
step={1}
8383
value={settings.get(typingTestProps.wordList.wordListSize)}
84-
title={formatMessage({
85-
id: "settings.wordListSize.description",
86-
defaultMessage: "Chose how many common words to use.",
87-
})}
8884
onChange={(value) => {
8985
updateSettings(
9086
settings.set(typingTestProps.wordList.wordListSize, value),

packages/page-typing-test/lib/session/session.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ import {
1919
} from "./types.ts";
2020

2121
export class Session {
22-
static readonly emptyLines = { text: "", lines: [] } satisfies SessionLines;
22+
static readonly emptyLines = {
23+
text: "",
24+
lines: [],
25+
} satisfies SessionLines;
26+
static readonly emptyProgress = {
27+
time: 0,
28+
length: 0,
29+
progress: 0,
30+
speed: 0,
31+
} satisfies Progress;
32+
2333
/** A list of events to replay. */
2434
#events: AnyEvent[] = [];
2535
/** The currently visible lines. */

0 commit comments

Comments
 (0)