Skip to content

Commit de4e436

Browse files
committed
feat: improve the typing test page
1 parent 81b206e commit de4e436

File tree

14 files changed

+144
-185
lines changed

14 files changed

+144
-185
lines changed

packages/keybr-textinput-events/lib/TextEvents.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
memo,
55
type ReactNode,
66
type RefObject,
7+
useEffect,
78
useImperativeHandle,
89
useRef,
910
} from "react";
@@ -19,13 +20,20 @@ export const TextEvents = memo(function TextEvents({
1920
}: Callbacks & {
2021
readonly focusRef?: RefObject<Focusable>;
2122
}): ReactNode {
23+
const inputRef = useRef<HTMLTextAreaElement>(null);
2224
const handler = useInputHandler();
2325
useImperativeHandle(focusRef, () => handler);
26+
useEffect(() => {
27+
handler.setInput(inputRef.current);
28+
return () => {
29+
handler.setInput(null);
30+
};
31+
}, [handler]);
2432
handler.setCallbacks({ onFocus, onBlur, onKeyDown, onKeyUp, onInput });
2533
return (
2634
<div style={divStyle}>
2735
<textarea
28-
ref={handler.setInput.bind(handler)}
36+
ref={inputRef}
2937
autoCapitalize="off"
3038
autoCorrect="off"
3139
spellCheck="false"
@@ -35,7 +43,7 @@ export const TextEvents = memo(function TextEvents({
3543
);
3644
});
3745

38-
function useInputHandler(): InputHandler {
46+
function useInputHandler() {
3947
const handlerRef = useRef<InputHandler | null>(null);
4048
let handler = handlerRef.current;
4149
if (handler == null) {

packages/keybr-widget/lib/components/explainer/ExplainerBoundary.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { type ReactNode, useState } from "react";
22
import { ExplainerStateContext } from "./context.ts";
33

44
export function ExplainerBoundary({
5+
defaultVisible = true,
56
children,
67
}: {
8+
readonly defaultVisible?: boolean;
79
readonly children: ReactNode;
810
}): ReactNode {
9-
const [explainersVisible, setExplainersVisible] = useState(true);
11+
const [explainersVisible, setExplainersVisible] = useState(defaultVisible);
1012
return (
1113
<ExplainerStateContext.Provider
1214
value={{

packages/keybr-widget/lib/components/text/Spacer.module.less

+4
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@
2121
.size5 {
2222
margin-block-start: 5rem;
2323
}
24+
25+
.size10 {
26+
margin-block-start: 10rem;
27+
}

packages/keybr-widget/lib/components/text/Spacer.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function Spacer({ size }: SpacerProps): ReactNode {
1212
[styles.size3]: size === 3,
1313
[styles.size4]: size === 4,
1414
[styles.size5]: size === 5,
15+
[styles.size10]: size === 10,
1516
})}
1617
/>
1718
);
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export type SpacerProps = {
2-
readonly size: 1 | 2 | 3 | 4 | 5;
2+
readonly size: 1 | 2 | 3 | 4 | 5 | 10;
33
};

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ test("render", async () => {
1717
</FakeIntlProvider>,
1818
);
1919

20-
fireEvent.click(await r.findByText("Settings..."));
21-
fireEvent.click(await r.findByText("Done"));
20+
fireEvent.click(await r.findByTitle("Settings", { exact: false }));
21+
fireEvent.click(await r.findByTitle("Save settings", { exact: false }));
2222

2323
r.unmount();
2424
});

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useSettings } from "@keybr/settings";
2-
import { makeStats, type Stats } from "@keybr/textinput";
2+
import { makeStats } from "@keybr/textinput";
33
import { type ReactNode, useMemo, useState } from "react";
44
import { Report } from "./components/Report.tsx";
55
import { SettingsScreen } from "./components/SettingsScreen.tsx";
@@ -20,17 +20,17 @@ export function TypingTestPage(): ReactNode {
2020
[settings],
2121
);
2222
const [view, setView] = useState(View.Test);
23-
const [stats, setStats] = useState<Stats>(makeStats([]));
23+
const [stats, setStats] = useState(makeStats([]));
2424

2525
switch (view) {
2626
case View.Test:
2727
return (
2828
<TextGeneratorLoader textSource={compositeSettings.textSource}>
29-
{(textGenerator) => {
29+
{(generator) => {
3030
return (
3131
<TestScreen
3232
settings={compositeSettings}
33-
textGenerator={textGenerator}
33+
generator={generator}
3434
onComplete={(stats) => {
3535
setView(View.Report);
3636
setStats(stats);
@@ -46,7 +46,7 @@ export function TypingTestPage(): ReactNode {
4646
case View.Report:
4747
return (
4848
<Report
49-
stats={stats!}
49+
stats={stats}
5050
onNext={() => {
5151
setView(View.Test);
5252
}}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
.line {
22
display: flex;
33
align-items: center;
4+
border-block-end: var(--separator-border);
45
}
56

67
.text {
78
flex: auto;
89
}
910

1011
.stats {
11-
min-inline-size: 10rem;
12+
text-align: end;
1213
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function Stats({ length, time, progress }: Progress): ReactNode {
2525
title="Time passed."
2626
/>
2727
{" / "}
28-
<Value value={`${length}`} title="Chars inputted." />
28+
<Value value={`${length}`} title="Characters inputted." />
2929
{" / "}
3030
<Value value={`${Math.floor(progress * 100)}%`} title="Progress made." />
3131
</>

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

+11-27
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import {
1414
Icon,
1515
Name,
1616
NameValue,
17+
useHotkeys,
1718
Value,
1819
} from "@keybr/widget";
19-
import { mdiFileImage, mdiSkipNext } from "@mdi/js";
20-
import { captureElementToImage } from "@sosimple/dom-to-image";
20+
import { mdiSkipNext } from "@mdi/js";
2121
import { memo, type ReactNode } from "react";
2222
import * as styles from "./Report.module.less";
2323

@@ -29,27 +29,14 @@ export const Report = memo(function Report({
2929
readonly onNext: () => void;
3030
}): ReactNode {
3131
const { formatNumber, formatPercents } = useIntlNumbers();
32+
33+
useHotkeys(["Enter", onNext]);
34+
3235
const dSpeed = makeSpeedDistribution();
3336
const dAccuracy = makeAccuracyDistribution();
3437
const pSpeed = dSpeed.cdf(speed);
3538
const pAccuracy = dAccuracy.cdf(dAccuracy.scale(accuracy));
3639

37-
const handleClickNext = () => {
38-
onNext();
39-
};
40-
41-
const handleClickScreenshot = () => {
42-
const selector = `.${styles.printable}`;
43-
const { backgroundColor } = getComputedStyle(document.body);
44-
captureElementToImage(selector, { backgroundColor })
45-
.then((blob) => {
46-
window.open(URL.createObjectURL(blob), "_blank");
47-
})
48-
.catch((error) => {
49-
console.error(error);
50-
});
51-
};
52-
5340
return (
5441
<div className={styles.report}>
5542
<div className={styles.printable}>
@@ -121,23 +108,20 @@ export const Report = memo(function Report({
121108

122109
<div className={styles.controlsLine}>
123110
<FieldList>
111+
<Field.Filler />
124112
<Field>
125113
<Button
126114
label="Next test"
127115
icon={<Icon shape={mdiSkipNext} />}
128116
title="Try another test."
129-
onClick={handleClickNext}
130-
/>
131-
</Field>
132-
<Field>
133-
<Button
134-
label="Screenshot"
135-
icon={<Icon shape={mdiFileImage} />}
136-
title="Take a screenshot."
137-
onClick={handleClickScreenshot}
117+
onClick={onNext}
138118
/>
139119
</Field>
120+
<Field.Filler />
140121
</FieldList>
122+
<p>
123+
Press the <kbd>Enter</kbd> key to start a new test.
124+
</p>
141125
</div>
142126
</div>
143127
);
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { Screen } from "@keybr/pages-shared";
22
import { TypingSettings } from "@keybr/textinput-ui";
3-
import { Button, Field, FieldList, Icon, Tab, TabList } from "@keybr/widget";
3+
import {
4+
Button,
5+
ExplainerBoundary,
6+
Field,
7+
FieldList,
8+
Icon,
9+
Tab,
10+
TabList,
11+
} from "@keybr/widget";
412
import { mdiCheckCircle } from "@mdi/js";
513
import { type ReactNode, useState } from "react";
614
import { TextGeneratorSettings } from "./settings/TextGeneratorSettings.tsx";
@@ -14,34 +22,36 @@ export function SettingsScreen({
1422

1523
return (
1624
<Screen>
17-
<TabList
18-
selectedIndex={tabIndex}
19-
onSelect={(tabIndex) => {
20-
setTabIndex(tabIndex);
21-
}}
22-
>
23-
<Tab label="Text">
24-
<TextGeneratorSettings />
25-
</Tab>
25+
<ExplainerBoundary defaultVisible={false}>
26+
<TabList
27+
selectedIndex={tabIndex}
28+
onSelect={(tabIndex) => {
29+
setTabIndex(tabIndex);
30+
}}
31+
>
32+
<Tab label="Text">
33+
<TextGeneratorSettings />
34+
</Tab>
2635

27-
<Tab label="Typing">
28-
<TypingSettings />
29-
</Tab>
30-
</TabList>
36+
<Tab label="Typing">
37+
<TypingSettings />
38+
</Tab>
39+
</TabList>
3140

32-
<FieldList>
33-
<Field.Filler />
34-
<Field>
35-
<Button
36-
icon={<Icon shape={mdiCheckCircle} />}
37-
label="Done"
38-
title="Save settings and return to the test."
39-
onClick={() => {
40-
onSubmit();
41-
}}
42-
/>
43-
</Field>
44-
</FieldList>
41+
<FieldList>
42+
<Field.Filler />
43+
<Field>
44+
<Button
45+
icon={<Icon shape={mdiCheckCircle} />}
46+
label="Done"
47+
title="Save settings and return to the test."
48+
onClick={() => {
49+
onSubmit();
50+
}}
51+
/>
52+
</Field>
53+
</FieldList>
54+
</ExplainerBoundary>
4555
</Screen>
4656
);
4757
}

0 commit comments

Comments
 (0)