@khanacademy/math-input 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/components/input/math-wrapper.d.ts +1 -1
- package/dist/components/input/math-wrapper.js.flow +1 -1
- package/dist/components/input/mathquill-instance.d.ts +14 -3
- package/dist/components/input/mathquill-instance.js.flow +18 -3
- package/dist/components/input/mathquill-types.d.ts +28 -6
- package/dist/components/input/mathquill-types.js.flow +31 -7
- package/dist/components/key-handlers/handle-arrow.d.ts +3 -0
- package/dist/components/{input/key-handlers → key-handlers}/handle-arrow.js.flow +2 -2
- package/dist/components/{input/key-handlers → key-handlers}/handle-backspace.d.ts +1 -1
- package/dist/components/{input/key-handlers → key-handlers}/handle-backspace.js.flow +1 -1
- package/dist/components/key-handlers/handle-exponent.d.ts +3 -0
- package/dist/components/{input/key-handlers → key-handlers}/handle-exponent.js.flow +2 -2
- package/dist/components/{input/key-handlers → key-handlers}/handle-jump-out.d.ts +2 -2
- package/dist/components/{input/key-handlers → key-handlers}/handle-jump-out.js.flow +2 -2
- package/dist/components/key-handlers/key-translator.d.ts +4 -0
- package/dist/components/{key-translator.js.flow → key-handlers/key-translator.js.flow} +3 -3
- package/dist/components/keypad/geometry-page/index.d.ts +2 -1
- package/dist/components/keypad/geometry-page/index.js.flow +2 -1
- package/dist/components/keypad/index.d.ts +2 -1
- package/dist/components/keypad/index.js.flow +2 -1
- package/dist/components/keypad/keypad-page-items.d.ts +8 -16
- package/dist/components/keypad/keypad-page-items.js.flow +11 -16
- package/dist/components/keypad/numbers-page/index.d.ts +2 -1
- package/dist/components/keypad/numbers-page/index.js.flow +2 -1
- package/dist/components/keypad/operators-page/advanced-relations-buttons.d.ts +2 -1
- package/dist/components/keypad/operators-page/advanced-relations-buttons.js.flow +2 -1
- package/dist/components/keypad/operators-page/basic-relations-buttons.d.ts +2 -1
- package/dist/components/keypad/operators-page/basic-relations-buttons.js.flow +2 -1
- package/dist/components/keypad/operators-page/index.d.ts +2 -1
- package/dist/components/keypad/operators-page/index.js.flow +2 -1
- package/dist/components/keypad/operators-page/logarithms-buttons.d.ts +2 -1
- package/dist/components/keypad/operators-page/logarithms-buttons.js.flow +2 -1
- package/dist/components/keypad/operators-page/pre-algebra-buttons.d.ts +2 -1
- package/dist/components/keypad/operators-page/pre-algebra-buttons.js.flow +2 -1
- package/dist/es/index.js +395 -366
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +398 -367
- package/dist/index.js.flow +6 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js.flow +1 -0
- package/package.json +1 -1
- package/src/components/input/math-input.tsx +10 -14
- package/src/components/input/math-wrapper.ts +23 -49
- package/src/components/input/mathquill-helpers.ts +11 -11
- package/src/components/input/mathquill-instance.ts +57 -2
- package/src/components/input/mathquill-types.ts +37 -7
- package/src/components/{input/key-handlers → key-handlers}/handle-arrow.ts +6 -6
- package/src/components/{input/key-handlers → key-handlers}/handle-backspace.ts +19 -17
- package/src/components/{input/key-handlers → key-handlers}/handle-exponent.ts +8 -5
- package/src/components/{input/key-handlers → key-handlers}/handle-jump-out.ts +15 -10
- package/src/components/{key-translator.ts → key-handlers/key-translator.ts} +43 -28
- package/src/components/keypad/__tests__/Button.test.tsx +51 -0
- package/src/components/keypad/__tests__/keypad-button.test.tsx +41 -0
- package/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx +231 -0
- package/src/components/keypad/__tests__/keypad-v2.cypress.ts +46 -0
- package/src/components/keypad/geometry-page/index.tsx +2 -1
- package/src/components/keypad/index.tsx +2 -1
- package/src/components/keypad/keypad-mathquill.stories.tsx +15 -23
- package/src/components/keypad/keypad-page-items.tsx +10 -19
- package/src/components/keypad/numbers-page/index.tsx +2 -1
- package/src/components/keypad/operators-page/advanced-relations-buttons.tsx +2 -1
- package/src/components/keypad/operators-page/basic-relations-buttons.tsx +2 -1
- package/src/components/keypad/operators-page/index.tsx +2 -1
- package/src/components/keypad/operators-page/logarithms-buttons.tsx +2 -1
- package/src/components/keypad/operators-page/pre-algebra-buttons.tsx +2 -1
- package/src/index.ts +6 -1
- package/src/types.ts +2 -0
- package/tsconfig-build.json +1 -2
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/components/input/__tests__/test-math-wrapper.d.ts +0 -8
- package/dist/components/input/__tests__/test-math-wrapper.js.flow +0 -14
- package/dist/components/input/key-handlers/handle-arrow.d.ts +0 -3
- package/dist/components/input/key-handlers/handle-exponent.d.ts +0 -3
- package/dist/components/key-translator.d.ts +0 -4
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import Key from "
|
|
2
|
-
import {DecimalSeparator} from "
|
|
3
|
-
import {decimalSeparator} from "
|
|
4
|
-
|
|
5
|
-
import MQ from "./input/mathquill-instance";
|
|
1
|
+
import Key from "../../data/keys";
|
|
2
|
+
import {DecimalSeparator} from "../../enums";
|
|
3
|
+
import {decimalSeparator} from "../../utils";
|
|
4
|
+
import {mathQuillInstance} from "../input/mathquill-instance";
|
|
6
5
|
import {
|
|
7
6
|
MathFieldInterface,
|
|
8
|
-
|
|
9
|
-
} from "
|
|
7
|
+
MathFieldUpdaterCallback,
|
|
8
|
+
} from "../input/mathquill-types";
|
|
9
|
+
|
|
10
|
+
import handleArrow from "./handle-arrow";
|
|
11
|
+
import handleExponent from "./handle-exponent";
|
|
12
|
+
import handleJumpOut from "./handle-jump-out";
|
|
10
13
|
|
|
11
14
|
enum ActionType {
|
|
12
15
|
WRITE = "write",
|
|
@@ -20,7 +23,7 @@ const decimalSymbol = decimalSeparator === DecimalSeparator.COMMA ? "," : ".";
|
|
|
20
23
|
function buildGenericCallback(
|
|
21
24
|
str: string,
|
|
22
25
|
type: ActionType = ActionType.WRITE,
|
|
23
|
-
):
|
|
26
|
+
): MathFieldUpdaterCallback {
|
|
24
27
|
return function (mathQuill: MathFieldInterface) {
|
|
25
28
|
switch (type) {
|
|
26
29
|
case ActionType.WRITE: {
|
|
@@ -39,20 +42,41 @@ function buildGenericCallback(
|
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
function buildNormalFunctionCallback(command: string) {
|
|
46
|
+
return function (mathField: MathFieldInterface) {
|
|
47
|
+
mathField.write(`\\${command}\\left(\\right)`);
|
|
48
|
+
mathField.keystroke("Left");
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const keyToMathquillMap: Record<Key, MathFieldUpdaterCallback> = {
|
|
53
|
+
EXP: handleExponent,
|
|
54
|
+
EXP_2: handleExponent,
|
|
55
|
+
EXP_3: handleExponent,
|
|
56
|
+
|
|
57
|
+
JUMP_OUT_PARENTHESES: handleJumpOut,
|
|
58
|
+
JUMP_OUT_EXPONENT: handleJumpOut,
|
|
59
|
+
JUMP_OUT_BASE: handleJumpOut,
|
|
60
|
+
JUMP_INTO_NUMERATOR: handleJumpOut,
|
|
61
|
+
JUMP_OUT_NUMERATOR: handleJumpOut,
|
|
62
|
+
JUMP_OUT_DENOMINATOR: handleJumpOut,
|
|
63
|
+
|
|
64
|
+
LEFT: handleArrow,
|
|
65
|
+
RIGHT: handleArrow,
|
|
66
|
+
|
|
67
|
+
LOG: buildNormalFunctionCallback("log"),
|
|
68
|
+
LN: buildNormalFunctionCallback("ln"),
|
|
69
|
+
SIN: buildNormalFunctionCallback("sin"),
|
|
70
|
+
COS: buildNormalFunctionCallback("cos"),
|
|
71
|
+
TAN: buildNormalFunctionCallback("tan"),
|
|
72
|
+
|
|
43
73
|
CDOT: buildGenericCallback("\\cdot"),
|
|
44
|
-
COS: buildGenericCallback("cos"),
|
|
45
74
|
DECIMAL: buildGenericCallback(decimalSymbol),
|
|
46
75
|
DIVIDE: buildGenericCallback("\\div"),
|
|
47
76
|
EQUAL: buildGenericCallback("="),
|
|
48
|
-
EXP: buildGenericCallback("^"),
|
|
49
|
-
EXP_2: buildGenericCallback("^2"),
|
|
50
|
-
EXP_3: buildGenericCallback("^3"),
|
|
51
77
|
GEQ: buildGenericCallback("\\geq"),
|
|
52
78
|
GT: buildGenericCallback(">"),
|
|
53
79
|
LEQ: buildGenericCallback("\\leq"),
|
|
54
|
-
LN: buildGenericCallback("\\ln"),
|
|
55
|
-
LOG: buildGenericCallback("\\log"),
|
|
56
80
|
LT: buildGenericCallback("<"),
|
|
57
81
|
MINUS: buildGenericCallback("-"),
|
|
58
82
|
NEGATIVE: buildGenericCallback("-"),
|
|
@@ -60,13 +84,12 @@ const keyToMathquillMap: Record<Key, MathQuillUpdaterCallback> = {
|
|
|
60
84
|
PERCENT: buildGenericCallback("%"),
|
|
61
85
|
PERIOD: buildGenericCallback("."),
|
|
62
86
|
PLUS: buildGenericCallback("+"),
|
|
63
|
-
SIN: buildGenericCallback("sin"),
|
|
64
|
-
TAN: buildGenericCallback("tan"),
|
|
65
87
|
TIMES: buildGenericCallback("\\times"),
|
|
66
88
|
|
|
67
89
|
// The `FRAC_EXCLUSIVE` variant is handled manually, since we may need to do
|
|
68
90
|
// some additional navigation depending on the cursor position.
|
|
69
91
|
FRAC_INCLUSIVE: buildGenericCallback("/", ActionType.CMD),
|
|
92
|
+
FRAC: buildGenericCallback("\\frac", ActionType.CMD),
|
|
70
93
|
LEFT_PAREN: buildGenericCallback("(", ActionType.CMD),
|
|
71
94
|
RIGHT_PAREN: buildGenericCallback(")", ActionType.CMD),
|
|
72
95
|
SQRT: buildGenericCallback("sqrt", ActionType.CMD),
|
|
@@ -75,6 +98,7 @@ const keyToMathquillMap: Record<Key, MathQuillUpdaterCallback> = {
|
|
|
75
98
|
THETA: buildGenericCallback("theta", ActionType.CMD),
|
|
76
99
|
RADICAL: buildGenericCallback("nthroot", ActionType.CMD),
|
|
77
100
|
|
|
101
|
+
BACKSPACE: buildGenericCallback("Backspace", ActionType.KEYSTROKE),
|
|
78
102
|
UP: buildGenericCallback("Up", ActionType.KEYSTROKE),
|
|
79
103
|
DOWN: buildGenericCallback("Down", ActionType.KEYSTROKE),
|
|
80
104
|
|
|
@@ -87,7 +111,8 @@ const keyToMathquillMap: Record<Key, MathQuillUpdaterCallback> = {
|
|
|
87
111
|
const cursor = mathQuill.__controller.cursor;
|
|
88
112
|
// If there's nothing to the left of the cursor, then we want to
|
|
89
113
|
// leave the cursor to the left of the fraction after creating it.
|
|
90
|
-
const shouldNavigateLeft =
|
|
114
|
+
const shouldNavigateLeft =
|
|
115
|
+
cursor[mathQuillInstance.L] === ActionType.MQ_END;
|
|
91
116
|
mathQuill.cmd("\\frac");
|
|
92
117
|
if (shouldNavigateLeft) {
|
|
93
118
|
mathQuill.keystroke("Left");
|
|
@@ -128,17 +153,7 @@ const keyToMathquillMap: Record<Key, MathQuillUpdaterCallback> = {
|
|
|
128
153
|
|
|
129
154
|
// These need to be overwritten by the consumer
|
|
130
155
|
// if they're going to be used
|
|
131
|
-
FRAC: () => {},
|
|
132
|
-
RIGHT: () => {},
|
|
133
|
-
LEFT: () => {},
|
|
134
|
-
BACKSPACE: () => {},
|
|
135
156
|
DISMISS: () => {},
|
|
136
|
-
JUMP_OUT_PARENTHESES: () => {},
|
|
137
|
-
JUMP_OUT_EXPONENT: () => {},
|
|
138
|
-
JUMP_OUT_BASE: () => {},
|
|
139
|
-
JUMP_INTO_NUMERATOR: () => {},
|
|
140
|
-
JUMP_OUT_NUMERATOR: () => {},
|
|
141
|
-
JUMP_OUT_DENOMINATOR: () => {},
|
|
142
157
|
NOOP: () => {},
|
|
143
158
|
MANY: () => {},
|
|
144
159
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {render, screen} from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
|
|
6
|
+
import Button from "../button";
|
|
7
|
+
|
|
8
|
+
describe("<Button />", () => {
|
|
9
|
+
it("uses the aria label", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
render(
|
|
12
|
+
<Button onPress={() => {}} ariaLabel="Oranges">
|
|
13
|
+
<p />
|
|
14
|
+
</Button>,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(
|
|
19
|
+
screen.getByRole("button", {name: "Oranges"}),
|
|
20
|
+
).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("handles callback on press", () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const mockPressCallback = jest.fn();
|
|
26
|
+
render(
|
|
27
|
+
<Button onPress={mockPressCallback} ariaLabel="Oranges">
|
|
28
|
+
<p />
|
|
29
|
+
</Button>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
userEvent.click(screen.getByRole("button", {name: "Oranges"}));
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(mockPressCallback).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders child", () => {
|
|
40
|
+
// Arrange
|
|
41
|
+
render(
|
|
42
|
+
<Button onPress={() => {}} ariaLabel="Test">
|
|
43
|
+
<img aria-label="child1" />
|
|
44
|
+
</Button>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(screen.getByRole("button", {name: "Test"})).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByRole("img", {name: "child1"})).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {render, screen} from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
|
|
6
|
+
import Keys from "../../../data/key-configs";
|
|
7
|
+
import {KeypadButton} from "../keypad-page-items";
|
|
8
|
+
|
|
9
|
+
describe("<KeypadButton />", () => {
|
|
10
|
+
it("uses the aria label from the key config", () => {
|
|
11
|
+
// Arrange
|
|
12
|
+
render(
|
|
13
|
+
<KeypadButton
|
|
14
|
+
onClickKey={(config) => {}}
|
|
15
|
+
keyConfig={Keys.LEFT_PAREN}
|
|
16
|
+
/>,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(
|
|
21
|
+
screen.getByRole("button", {name: "Left parenthesis"}),
|
|
22
|
+
).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("handles onClickKey callback", () => {
|
|
26
|
+
// Arrange
|
|
27
|
+
const mockClickKeyCallback = jest.fn();
|
|
28
|
+
render(
|
|
29
|
+
<KeypadButton
|
|
30
|
+
onClickKey={mockClickKeyCallback}
|
|
31
|
+
keyConfig={Keys.LEFT_PAREN}
|
|
32
|
+
/>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
userEvent.click(screen.getByRole("button", {name: "Left parenthesis"}));
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(mockClickKeyCallback).toHaveBeenCalledWith("LEFT_PAREN");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
2
|
+
import {Popover} from "@khanacademy/wonder-blocks-popover";
|
|
3
|
+
import {render, screen} from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import "@testing-library/jest-dom";
|
|
8
|
+
|
|
9
|
+
import Key from "../../../data/keys";
|
|
10
|
+
import {createMathField} from "../../input/mathquill-instance";
|
|
11
|
+
import {MathFieldInterface} from "../../input/mathquill-types";
|
|
12
|
+
import keyTranslator from "../../key-handlers/key-translator";
|
|
13
|
+
import Keypad from "../index";
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
onChangeMathInput: (mathInputTex: string) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function V2KeypadWithMathquill(props: Props) {
|
|
20
|
+
const mathFieldWrapperRef = React.useRef<HTMLDivElement>(null);
|
|
21
|
+
const [mathField, setMathField] = React.useState<MathFieldInterface>();
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
if (!mathField && mathFieldWrapperRef.current) {
|
|
25
|
+
const mathFieldInstance = createMathField(
|
|
26
|
+
mathFieldWrapperRef.current,
|
|
27
|
+
(baseConfig) => {
|
|
28
|
+
return {
|
|
29
|
+
...baseConfig,
|
|
30
|
+
handlers: {
|
|
31
|
+
edit: (mathField) => {
|
|
32
|
+
props.onChangeMathInput(mathField.latex());
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
setMathField(mathFieldInstance);
|
|
39
|
+
}
|
|
40
|
+
}, [mathField, props]);
|
|
41
|
+
|
|
42
|
+
function handleClickKey(key: Key) {
|
|
43
|
+
if (!mathField) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mathFieldCallback = keyTranslator[key];
|
|
48
|
+
if (mathFieldCallback) {
|
|
49
|
+
mathFieldCallback(mathField, key);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div style={{maxWidth: "400px", margin: "2em"}}>
|
|
55
|
+
<Popover
|
|
56
|
+
content={
|
|
57
|
+
<div>
|
|
58
|
+
<Keypad
|
|
59
|
+
extraKeys={["a", "b", "c"]}
|
|
60
|
+
onClickKey={handleClickKey}
|
|
61
|
+
advancedRelations
|
|
62
|
+
basicRelations
|
|
63
|
+
divisionKey
|
|
64
|
+
logarithms
|
|
65
|
+
multiplicationDot
|
|
66
|
+
preAlgebra
|
|
67
|
+
trigonometry
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
}
|
|
71
|
+
dismissEnabled
|
|
72
|
+
opened
|
|
73
|
+
>
|
|
74
|
+
<div
|
|
75
|
+
style={{
|
|
76
|
+
width: "100%",
|
|
77
|
+
marginBottom: "1em",
|
|
78
|
+
border: `1px solid ${Color.offBlack16}`,
|
|
79
|
+
}}
|
|
80
|
+
ref={mathFieldWrapperRef}
|
|
81
|
+
/>
|
|
82
|
+
</Popover>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("Keypad v2 with MathQuill", () => {
|
|
88
|
+
it("can write the Pythagorean theorem (simple)", () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const mockMathInputCallback = jest.fn();
|
|
91
|
+
render(
|
|
92
|
+
<V2KeypadWithMathquill onChangeMathInput={mockMathInputCallback} />,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Act
|
|
96
|
+
|
|
97
|
+
// a^2
|
|
98
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
99
|
+
userEvent.click(screen.getByRole("button", {name: "a"}));
|
|
100
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
101
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
102
|
+
|
|
103
|
+
// +
|
|
104
|
+
userEvent.click(screen.getByRole("button", {name: "Numbers"}));
|
|
105
|
+
userEvent.click(screen.getByRole("button", {name: "Plus"}));
|
|
106
|
+
|
|
107
|
+
// b^2 =
|
|
108
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
109
|
+
userEvent.click(screen.getByRole("button", {name: "b"}));
|
|
110
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
111
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
112
|
+
userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
|
|
113
|
+
|
|
114
|
+
// c^2
|
|
115
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
116
|
+
userEvent.click(screen.getByRole("button", {name: "c"}));
|
|
117
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
118
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
expect(mockMathInputCallback).toHaveBeenLastCalledWith("a^2+b^2=c^2");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("can write the Pythagorean theorem (complex)", () => {
|
|
125
|
+
// Arrange
|
|
126
|
+
const mockMathInputCallback = jest.fn();
|
|
127
|
+
render(
|
|
128
|
+
<V2KeypadWithMathquill onChangeMathInput={mockMathInputCallback} />,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Act
|
|
132
|
+
|
|
133
|
+
// c = /Square root
|
|
134
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
135
|
+
userEvent.click(screen.getByRole("button", {name: "c"}));
|
|
136
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
137
|
+
userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
|
|
138
|
+
userEvent.click(screen.getByRole("button", {name: "Square root"}));
|
|
139
|
+
|
|
140
|
+
// a^2
|
|
141
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
142
|
+
userEvent.click(screen.getByRole("button", {name: "a"}));
|
|
143
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
144
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
145
|
+
|
|
146
|
+
// +
|
|
147
|
+
userEvent.click(screen.getByRole("button", {name: "Numbers"}));
|
|
148
|
+
userEvent.click(screen.getByRole("button", {name: "Plus"}));
|
|
149
|
+
|
|
150
|
+
// b^2
|
|
151
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
152
|
+
userEvent.click(screen.getByRole("button", {name: "b"}));
|
|
153
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
154
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
expect(mockMathInputCallback).toHaveBeenLastCalledWith(
|
|
158
|
+
"c=\\sqrt{a^2+b^2}",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("writes the Pythagorean theorem using typing/clicking together", () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const mockMathInputCallback = jest.fn();
|
|
165
|
+
render(
|
|
166
|
+
<V2KeypadWithMathquill onChangeMathInput={mockMathInputCallback} />,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Act
|
|
170
|
+
|
|
171
|
+
// Argument is empty because mathquill generates textarea w/o label
|
|
172
|
+
userEvent.type(screen.getByRole("textbox"), "a");
|
|
173
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
174
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
175
|
+
|
|
176
|
+
userEvent.type(screen.getByRole("textbox"), "+");
|
|
177
|
+
|
|
178
|
+
// b^2
|
|
179
|
+
userEvent.click(screen.getByRole("button", {name: "Extras"}));
|
|
180
|
+
userEvent.click(screen.getByRole("button", {name: "b"}));
|
|
181
|
+
userEvent.click(screen.getByRole("button", {name: "Operators"}));
|
|
182
|
+
userEvent.click(screen.getByRole("button", {name: "Square"}));
|
|
183
|
+
userEvent.type(screen.getByRole("textbox"), "=c^2");
|
|
184
|
+
|
|
185
|
+
// Assert
|
|
186
|
+
expect(mockMathInputCallback).toHaveBeenLastCalledWith("a^2+b^2=c^2");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("deletes from the input using the backspace button", () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const mockMathInputCallback = jest.fn();
|
|
192
|
+
render(
|
|
193
|
+
<V2KeypadWithMathquill onChangeMathInput={mockMathInputCallback} />,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Act
|
|
197
|
+
// Write `a^2+b^2=c^2` using the keypad
|
|
198
|
+
const buttonPressesForFormula = [
|
|
199
|
+
"Extras",
|
|
200
|
+
"a",
|
|
201
|
+
"Operators",
|
|
202
|
+
"Square",
|
|
203
|
+
"Numbers",
|
|
204
|
+
"Plus",
|
|
205
|
+
"Extras",
|
|
206
|
+
"b",
|
|
207
|
+
"Operators",
|
|
208
|
+
"Square",
|
|
209
|
+
"Equals sign",
|
|
210
|
+
"Extras",
|
|
211
|
+
"c",
|
|
212
|
+
"Operators",
|
|
213
|
+
"Square",
|
|
214
|
+
];
|
|
215
|
+
buttonPressesForFormula.forEach((button) => {
|
|
216
|
+
userEvent.click(screen.getByRole("button", {name: button}));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Assert
|
|
220
|
+
// make sure the formula was typed correctly
|
|
221
|
+
expect(mockMathInputCallback).toHaveBeenLastCalledWith("a^2+b^2=c^2");
|
|
222
|
+
|
|
223
|
+
userEvent.click(screen.getByRole("button", {name: "Numbers"}));
|
|
224
|
+
// delete: need 14 backspaces in MathQuill to delete `a^2+b^2=c^2`
|
|
225
|
+
for (let i = 0; i < 14; i++) {
|
|
226
|
+
userEvent.click(screen.getByRole("button", {name: "Delete"}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
expect(mockMathInputCallback).toHaveBeenLastCalledWith("");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import renderSingleKeypad from "../../../../../../testing/render-keypad-with-cypress";
|
|
2
|
+
import KeyConfigs from "../../../data/key-configs";
|
|
3
|
+
|
|
4
|
+
const tabs = [
|
|
5
|
+
{
|
|
6
|
+
name: "Operators",
|
|
7
|
+
specialButton: "EXP_2",
|
|
8
|
+
label: KeyConfigs["EXP_2"].ariaLabel,
|
|
9
|
+
},
|
|
10
|
+
{name: "Extras", specialButton: "PI", label: KeyConfigs["PI"].ariaLabel},
|
|
11
|
+
|
|
12
|
+
{
|
|
13
|
+
name: "Geometry",
|
|
14
|
+
specialButton: "COS",
|
|
15
|
+
label: KeyConfigs["COS"].ariaLabel,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Numbers",
|
|
19
|
+
specialButton: "NUM_7",
|
|
20
|
+
label: KeyConfigs["NUM_7"].ariaLabel,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe("Keypad v2", () => {
|
|
25
|
+
tabs.forEach((tab) => {
|
|
26
|
+
it(`switches to the correct tab: ${tab.name}`, () => {
|
|
27
|
+
renderSingleKeypad((key) => {});
|
|
28
|
+
|
|
29
|
+
// currently clicking on the bottom left due to button re-rendering
|
|
30
|
+
// after mousedown but before mouseup (only in Cypress)
|
|
31
|
+
cy.get('[aria-label="' + tab.name + '"]').click("bottomLeft");
|
|
32
|
+
cy.get('[aria-label="' + tab.label + '"]').should("exist");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it(`calls ${tab.specialButton} key callback in ${tab.name} tab`, () => {
|
|
36
|
+
const onClickKeySpy = cy.spy().as("onClickKeySpy");
|
|
37
|
+
renderSingleKeypad(onClickKeySpy);
|
|
38
|
+
cy.get('[aria-label="' + tab.name + '"]').click();
|
|
39
|
+
cy.get('[aria-label="' + tab.label + '"]').click();
|
|
40
|
+
cy.get("@onClickKeySpy").should(
|
|
41
|
+
"have.been.calledOnceWithExactly",
|
|
42
|
+
tab.specialButton,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
import Keys from "../../../data/key-configs";
|
|
4
|
+
import {ClickKeyCallback} from "../../../types";
|
|
4
5
|
import {
|
|
5
6
|
KeypadPageContainer,
|
|
6
7
|
SecondaryKeypadButton,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
} from "../keypad-page-items";
|
|
9
10
|
|
|
10
11
|
type Props = {
|
|
11
|
-
onClickKey:
|
|
12
|
+
onClickKey: ClickKeyCallback;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export default class GeometryPage extends React.Component<Props> {
|
|
@@ -4,6 +4,7 @@ import {StyleSheet} from "aphrodite";
|
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
|
|
6
6
|
import Key from "../../data/keys";
|
|
7
|
+
import {ClickKeyCallback} from "../../types";
|
|
7
8
|
import Tabbar from "../tabbar/tabbar";
|
|
8
9
|
import {TabbarItemType} from "../tabbar/types";
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ import OperatorsPage from "./operators-page";
|
|
|
15
16
|
import {OperatorsButtonSets} from "./operators-page/types";
|
|
16
17
|
|
|
17
18
|
export type Props = {
|
|
18
|
-
onClickKey:
|
|
19
|
+
onClickKey: ClickKeyCallback;
|
|
19
20
|
trigonometry?: boolean;
|
|
20
21
|
extraKeys: ReadonlyArray<Key>;
|
|
21
22
|
} & OperatorsButtonSets &
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
2
2
|
import {Popover} from "@khanacademy/wonder-blocks-popover";
|
|
3
|
-
import MathQuill from "mathquill";
|
|
4
3
|
import * as React from "react";
|
|
5
4
|
|
|
6
5
|
import Key from "../../data/keys";
|
|
7
|
-
import
|
|
6
|
+
import {createMathField} from "../input/mathquill-instance";
|
|
7
|
+
import {MathFieldInterface} from "../input/mathquill-types";
|
|
8
|
+
import keyTranslator from "../key-handlers/key-translator";
|
|
8
9
|
|
|
9
10
|
import Keypad from "./index";
|
|
10
11
|
|
|
@@ -12,36 +13,27 @@ export default {
|
|
|
12
13
|
title: "v2 Keypad With Mathquill",
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
const mathQuillConfig = {
|
|
16
|
-
autoCommands: "pi theta phi sqrt nthroot",
|
|
17
|
-
charsThatBreakOutOfSupSub: "+-*/=<>≠≤≥",
|
|
18
|
-
supSubsRequireOperand: true,
|
|
19
|
-
spaceBehavesLikeTab: true,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
16
|
export function V2KeypadWithMathquill() {
|
|
23
|
-
const
|
|
24
|
-
const [
|
|
17
|
+
const mathFieldWrapperRef = React.useRef<HTMLDivElement>(null);
|
|
18
|
+
const [mathField, setMathField] = React.useState<MathFieldInterface>();
|
|
25
19
|
|
|
26
20
|
React.useEffect(() => {
|
|
27
|
-
if (!
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
mathquillWrapperRef.current,
|
|
31
|
-
mathQuillConfig,
|
|
21
|
+
if (!mathField && mathFieldWrapperRef.current) {
|
|
22
|
+
const mathFieldInstance = createMathField(
|
|
23
|
+
mathFieldWrapperRef.current,
|
|
32
24
|
);
|
|
33
|
-
|
|
25
|
+
setMathField(mathFieldInstance);
|
|
34
26
|
}
|
|
35
|
-
}, [
|
|
27
|
+
}, [mathField]);
|
|
36
28
|
|
|
37
29
|
function handleClickKey(key: Key) {
|
|
38
|
-
if (!
|
|
30
|
+
if (!mathField) {
|
|
39
31
|
return;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
34
|
+
const mathFieldCallback = keyTranslator[key];
|
|
35
|
+
if (mathFieldCallback) {
|
|
36
|
+
mathFieldCallback(mathField, key);
|
|
45
37
|
} else {
|
|
46
38
|
// eslint-disable-next-line no-console
|
|
47
39
|
console.warn(`No translation to Mathquill for: ${key}`);
|
|
@@ -74,7 +66,7 @@ export function V2KeypadWithMathquill() {
|
|
|
74
66
|
marginBottom: "1em",
|
|
75
67
|
border: `1px solid ${Color.offBlack16}`,
|
|
76
68
|
}}
|
|
77
|
-
ref={
|
|
69
|
+
ref={mathFieldWrapperRef}
|
|
78
70
|
/>
|
|
79
71
|
</Popover>
|
|
80
72
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {View} from "@khanacademy/wonder-blocks-core";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
import {KeyConfig} from "../../types";
|
|
4
|
+
import {KeyConfig, ClickKeyCallback} from "../../types";
|
|
5
5
|
|
|
6
6
|
import Button from "./button";
|
|
7
7
|
import ButtonAsset from "./button-assets";
|
|
@@ -30,11 +30,14 @@ export const KeypadPageContainer = ({
|
|
|
30
30
|
</View>
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
type SharedButtonProps = {
|
|
34
34
|
keyConfig: KeyConfig;
|
|
35
|
-
tintColor?: string;
|
|
36
35
|
style?: StyleType;
|
|
37
|
-
onClickKey:
|
|
36
|
+
onClickKey: ClickKeyCallback;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type KeypadButtonProps = SharedButtonProps & {
|
|
40
|
+
tintColor?: string;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
export const KeypadButton = ({
|
|
@@ -47,23 +50,17 @@ export const KeypadButton = ({
|
|
|
47
50
|
onPress={() => onClickKey(keyConfig.id)}
|
|
48
51
|
tintColor={tintColor}
|
|
49
52
|
style={style}
|
|
50
|
-
ariaLabel={keyConfig.
|
|
53
|
+
ariaLabel={keyConfig.ariaLabel}
|
|
51
54
|
>
|
|
52
55
|
<ButtonAsset id={keyConfig.id} />
|
|
53
56
|
</Button>
|
|
54
57
|
);
|
|
55
58
|
|
|
56
|
-
type SecondaryKeypadButtonProps = {
|
|
57
|
-
keyConfig: KeyConfig;
|
|
58
|
-
style?: StyleType;
|
|
59
|
-
onClickKey: (keyConfig: string) => void;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
59
|
export const SecondaryKeypadButton = ({
|
|
63
60
|
keyConfig,
|
|
64
61
|
onClickKey,
|
|
65
62
|
style,
|
|
66
|
-
}:
|
|
63
|
+
}: SharedButtonProps): React.ReactElement => (
|
|
67
64
|
<KeypadButton
|
|
68
65
|
keyConfig={keyConfig}
|
|
69
66
|
onClickKey={onClickKey}
|
|
@@ -72,17 +69,11 @@ export const SecondaryKeypadButton = ({
|
|
|
72
69
|
/>
|
|
73
70
|
);
|
|
74
71
|
|
|
75
|
-
type KeypadActionButtonProps = {
|
|
76
|
-
keyConfig: KeyConfig;
|
|
77
|
-
style?: StyleType;
|
|
78
|
-
onClickKey: (keyConfig: string) => void;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
72
|
export const KeypadActionButton = ({
|
|
82
73
|
keyConfig,
|
|
83
74
|
onClickKey,
|
|
84
75
|
style,
|
|
85
|
-
}:
|
|
76
|
+
}: SharedButtonProps): React.ReactElement => (
|
|
86
77
|
<KeypadButton
|
|
87
78
|
keyConfig={keyConfig}
|
|
88
79
|
onClickKey={onClickKey}
|