@khanacademy/perseus 11.6.1 → 12.0.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/math-input.d.ts +38 -11
- package/dist/components/tex-buttons.d.ts +2 -1
- package/dist/es/index.css +5 -6
- package/dist/es/index.js +999 -919
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +5 -6
- package/dist/index.js +976 -895
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +2 -1
- package/dist/strings.js +3 -0
- package/dist/widgets/expression.d.ts +15 -14
- package/package.json +2 -2
- package/src/__tests__/mock-asset-loading-widget.tsx +1 -2
- package/src/components/__stories__/math-input.stories.tsx +17 -4
- package/src/components/__tests__/math-input.test.tsx +95 -8
- package/src/components/math-input.tsx +274 -96
- package/src/components/tex-buttons.tsx +2 -2
- package/src/perseus-types.ts +11 -1
- package/src/styles/perseus-renderer.less +5 -6
- package/src/styles/widgets/expression.less +0 -6
- package/src/widgets/__stories__/expression.stories.tsx +3 -3
- package/src/widgets/__tests__/expression.test.tsx +4 -77
- package/src/widgets/expression.tsx +99 -94
- package/tsconfig-build.tsbuildinfo +1 -1
package/dist/perseus-types.d.ts
CHANGED
|
@@ -133,9 +133,10 @@ export type PerseusExplanationWidgetOptions = {
|
|
|
133
133
|
};
|
|
134
134
|
static: boolean;
|
|
135
135
|
};
|
|
136
|
+
export type LegacyButtonSets = ReadonlyArray<"basic" | "basic+div" | "trig" | "prealgebra" | "logarithms" | "basic relations" | "advanced relations">;
|
|
136
137
|
export type PerseusExpressionWidgetOptions = {
|
|
137
138
|
answerForms: ReadonlyArray<PerseusExpressionAnswerForm>;
|
|
138
|
-
buttonSets:
|
|
139
|
+
buttonSets: LegacyButtonSets;
|
|
139
140
|
functions: ReadonlyArray<string>;
|
|
140
141
|
times: boolean;
|
|
141
142
|
buttonsVisible?: "always" | "never" | "focused";
|
package/dist/strings.js
CHANGED
|
@@ -48,6 +48,7 @@ module.exports = [
|
|
|
48
48
|
i18n._("Click on the tiles to change the lights.", {}),
|
|
49
49
|
i18n._("Click to add points", {}),
|
|
50
50
|
i18n._("Click to add vertices", {}),
|
|
51
|
+
i18n._("close math keypad", {}),
|
|
51
52
|
i18n._("Correct", {}),
|
|
52
53
|
i18n._("Correct (but you crossed it out)", {}),
|
|
53
54
|
i18n._("Correct (selected)", {}),
|
|
@@ -109,6 +110,8 @@ module.exports = [
|
|
|
109
110
|
i18n._("Note: For this question, the shape will not move.", {}),
|
|
110
111
|
i18n._("Number of divisions:", {}),
|
|
111
112
|
i18n._("Number of trials:", {}),
|
|
113
|
+
i18n._("Oops!", {}),
|
|
114
|
+
i18n._("open math keypad", {}),
|
|
112
115
|
i18n._("Parameter", {}),
|
|
113
116
|
i18n._("Phi", {}),
|
|
114
117
|
i18n._("Pi", {}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import type { DependenciesContext } from "../dependencies";
|
|
3
3
|
import type { PerseusExpressionWidgetOptions } from "../perseus-types";
|
|
4
4
|
import type { PerseusScore, WidgetProps } from "../types";
|
|
5
5
|
import type { Keys as Key, KeypadConfiguration } from "@khanacademy/math-input";
|
|
@@ -13,7 +13,7 @@ type RenderProps = {
|
|
|
13
13
|
keypadConfiguration: ReturnType<typeof keypadConfigurationForProps>;
|
|
14
14
|
};
|
|
15
15
|
type ExternalProps = WidgetProps<RenderProps, Rubric>;
|
|
16
|
-
export type Props = ExternalProps &
|
|
16
|
+
export type Props = ExternalProps & Partial<React.ContextType<typeof DependenciesContext>> & {
|
|
17
17
|
apiOptions: NonNullable<ExternalProps["apiOptions"]>;
|
|
18
18
|
buttonSets: NonNullable<ExternalProps["buttonSets"]>;
|
|
19
19
|
functions: NonNullable<ExternalProps["functions"]>;
|
|
@@ -24,8 +24,9 @@ export type Props = ExternalProps & ReturnType<typeof useDependencies> & {
|
|
|
24
24
|
value: string;
|
|
25
25
|
};
|
|
26
26
|
export type ExpressionState = {
|
|
27
|
+
invalid: boolean;
|
|
27
28
|
showErrorTooltip: boolean;
|
|
28
|
-
|
|
29
|
+
showErrorStyle: boolean;
|
|
29
30
|
};
|
|
30
31
|
type DefaultProps = {
|
|
31
32
|
apiOptions: Props["apiOptions"];
|
|
@@ -40,7 +41,6 @@ type DefaultProps = {
|
|
|
40
41
|
type OnInputErrorFunctionType = (arg1?: any, arg2?: any, arg3?: any) => boolean | null | undefined;
|
|
41
42
|
export declare class Expression extends React.Component<Props, ExpressionState> {
|
|
42
43
|
_isMounted: boolean;
|
|
43
|
-
errorTimeout: null | number;
|
|
44
44
|
static validate(userInput: string, rubric: Rubric, onInputError?: OnInputErrorFunctionType): PerseusScore;
|
|
45
45
|
static getUserInputFromProps(props: Props): string;
|
|
46
46
|
static getOneCorrectAnswerFromRubric(rubric: Rubric): string | null | undefined;
|
|
@@ -50,6 +50,7 @@ export declare class Expression extends React.Component<Props, ExpressionState>
|
|
|
50
50
|
componentDidMount: () => void;
|
|
51
51
|
componentDidUpdate: (prevProps: Props) => void;
|
|
52
52
|
componentWillUnmount: () => void;
|
|
53
|
+
_handleMouseDown: () => void;
|
|
53
54
|
simpleValidate: (rubric: Rubric, onInputError: OnInputErrorFunctionType) => PerseusScore;
|
|
54
55
|
getUserInput: () => string;
|
|
55
56
|
change: (...args: any) => any | undefined;
|
|
@@ -151,7 +152,7 @@ declare const _default: Readonly<{
|
|
|
151
152
|
isLastUsedWidget: boolean;
|
|
152
153
|
linterContext: import("@khanacademy/perseus-linter").LinterContextProps;
|
|
153
154
|
containerSizeClass: import("../util/sizing-utils").SizeClass;
|
|
154
|
-
} & import("../types").PerseusDependenciesV2 & {
|
|
155
|
+
} & Partial<import("../types").PerseusDependenciesV2> & {
|
|
155
156
|
apiOptions: Readonly<Readonly<{
|
|
156
157
|
isArticle?: boolean | undefined;
|
|
157
158
|
onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
|
|
@@ -205,14 +206,14 @@ declare const _default: Readonly<{
|
|
|
205
206
|
showAlignmentOptions: NonNullable<boolean | undefined>;
|
|
206
207
|
useDraftEditor: NonNullable<boolean | undefined>;
|
|
207
208
|
}>;
|
|
208
|
-
buttonSets:
|
|
209
|
+
buttonSets: import("../perseus-types").LegacyButtonSets;
|
|
209
210
|
functions: readonly string[];
|
|
210
211
|
linterContext: import("@khanacademy/perseus-linter").LinterContextProps;
|
|
211
212
|
onBlur: (blurPath: import("../types").FocusPath) => void;
|
|
212
213
|
onFocus: (blurPath: import("../types").FocusPath) => void;
|
|
213
214
|
times: boolean;
|
|
214
215
|
value: string;
|
|
215
|
-
}, "static" | "trackInteraction" | "onChange" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"
|
|
216
|
+
}, "static" | "trackInteraction" | "onChange" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "analytics" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
|
|
216
217
|
value?: string | undefined;
|
|
217
218
|
onFocus?: ((blurPath: import("../types").FocusPath) => void) | undefined;
|
|
218
219
|
onBlur?: ((blurPath: import("../types").FocusPath) => void) | undefined;
|
|
@@ -270,9 +271,9 @@ declare const _default: Readonly<{
|
|
|
270
271
|
showAlignmentOptions: NonNullable<boolean | undefined>;
|
|
271
272
|
useDraftEditor: NonNullable<boolean | undefined>;
|
|
272
273
|
}> | undefined;
|
|
273
|
-
|
|
274
|
-
buttonSets?: readonly string[] | undefined;
|
|
274
|
+
buttonSets?: import("../perseus-types").LegacyButtonSets | undefined;
|
|
275
275
|
functions?: readonly string[] | undefined;
|
|
276
|
+
times?: boolean | undefined;
|
|
276
277
|
} & {}, "analytics"> & React.RefAttributes<Expression>>) | undefined;
|
|
277
278
|
accessible?: boolean | ((props: any) => boolean) | undefined;
|
|
278
279
|
hidden?: boolean | undefined;
|
|
@@ -357,7 +358,7 @@ declare const _default: Readonly<{
|
|
|
357
358
|
isLastUsedWidget: boolean;
|
|
358
359
|
linterContext: import("@khanacademy/perseus-linter").LinterContextProps;
|
|
359
360
|
containerSizeClass: import("../util/sizing-utils").SizeClass;
|
|
360
|
-
} & import("../types").PerseusDependenciesV2 & {
|
|
361
|
+
} & Partial<import("../types").PerseusDependenciesV2> & {
|
|
361
362
|
apiOptions: Readonly<Readonly<{
|
|
362
363
|
isArticle?: boolean | undefined;
|
|
363
364
|
onInputError?: ((widgetId: any, value: string, message?: string | null | undefined) => unknown) | undefined;
|
|
@@ -411,14 +412,14 @@ declare const _default: Readonly<{
|
|
|
411
412
|
showAlignmentOptions: NonNullable<boolean | undefined>;
|
|
412
413
|
useDraftEditor: NonNullable<boolean | undefined>;
|
|
413
414
|
}>;
|
|
414
|
-
buttonSets:
|
|
415
|
+
buttonSets: import("../perseus-types").LegacyButtonSets;
|
|
415
416
|
functions: readonly string[];
|
|
416
417
|
linterContext: import("@khanacademy/perseus-linter").LinterContextProps;
|
|
417
418
|
onBlur: (blurPath: import("../types").FocusPath) => void;
|
|
418
419
|
onFocus: (blurPath: import("../types").FocusPath) => void;
|
|
419
420
|
times: boolean;
|
|
420
421
|
value: string;
|
|
421
|
-
}, "static" | "trackInteraction" | "onChange" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"
|
|
422
|
+
}, "static" | "trackInteraction" | "onChange" | "problemNum" | "questionCompleted" | "keypadElement" | "alignment" | "analytics" | "buttonsVisible" | "containerSizeClass" | "widgetId" | "findWidgets" | "keypadConfiguration" | "reviewModeRubric" | "isLastUsedWidget"> & {
|
|
422
423
|
value?: string | undefined;
|
|
423
424
|
onFocus?: ((blurPath: import("../types").FocusPath) => void) | undefined;
|
|
424
425
|
onBlur?: ((blurPath: import("../types").FocusPath) => void) | undefined;
|
|
@@ -476,9 +477,9 @@ declare const _default: Readonly<{
|
|
|
476
477
|
showAlignmentOptions: NonNullable<boolean | undefined>;
|
|
477
478
|
useDraftEditor: NonNullable<boolean | undefined>;
|
|
478
479
|
}> | undefined;
|
|
479
|
-
|
|
480
|
-
buttonSets?: readonly string[] | undefined;
|
|
480
|
+
buttonSets?: import("../perseus-types").LegacyButtonSets | undefined;
|
|
481
481
|
functions?: readonly string[] | undefined;
|
|
482
|
+
times?: boolean | undefined;
|
|
482
483
|
} & {}, "analytics"> & React.RefAttributes<Expression>>;
|
|
483
484
|
}>;
|
|
484
485
|
export default _default;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Core Perseus API (includes renderers and widgets)",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "12.0.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@khanacademy/kas": "^0.3.2",
|
|
26
26
|
"@khanacademy/kmath": "^0.1.3",
|
|
27
|
-
"@khanacademy/math-input": "^14.0
|
|
27
|
+
"@khanacademy/math-input": "^14.1.0",
|
|
28
28
|
"@khanacademy/perseus-core": "1.1.1",
|
|
29
29
|
"@khanacademy/perseus-linter": "^0.3.5",
|
|
30
30
|
"@khanacademy/pure-markdown": "^0.2.7",
|
|
@@ -5,14 +5,13 @@ import AssetContext from "../asset-context";
|
|
|
5
5
|
import type {PerseusItem} from "../perseus-types";
|
|
6
6
|
import type {WidgetExports} from "../types";
|
|
7
7
|
|
|
8
|
-
// @ts-expect-error - TS2322 - Type '"mocked-asset-widget"' is not assignable to type '"categorizer" | "cs-program" | "definition" | "dropdown" | "example-graphie-widget" | "example-widget" | "explanation" | "expression" | "graded-group-set" | "graded-group" | ... 29 more ... | "video"'.
|
|
9
8
|
export const mockedAssetItem: PerseusItem = {
|
|
10
9
|
question: {
|
|
11
10
|
content: "[[\u2603 mocked-asset-widget 1]]",
|
|
12
11
|
images: Object.freeze({}),
|
|
13
12
|
widgets: {
|
|
14
13
|
"mocked-asset-widget 1": {
|
|
15
|
-
type: "mocked-asset-widget",
|
|
14
|
+
type: "mocked-asset-widget" as any,
|
|
16
15
|
alignment: "default",
|
|
17
16
|
static: false,
|
|
18
17
|
graded: true,
|
|
@@ -13,8 +13,16 @@ export default {
|
|
|
13
13
|
} as Story;
|
|
14
14
|
|
|
15
15
|
const defaultObject = {
|
|
16
|
-
|
|
16
|
+
keypadButtonSets: {
|
|
17
|
+
advancedRelations: true,
|
|
18
|
+
basicRelations: true,
|
|
19
|
+
divisionKey: true,
|
|
20
|
+
logarithms: true,
|
|
21
|
+
preAlgebra: true,
|
|
22
|
+
trigonometry: true,
|
|
23
|
+
},
|
|
17
24
|
onChange: () => {},
|
|
25
|
+
analytics: {onAnalyticsEvent: () => Promise.resolve()},
|
|
18
26
|
} as const;
|
|
19
27
|
|
|
20
28
|
export const DefaultWithBasicButtonSet = (
|
|
@@ -22,9 +30,14 @@ export const DefaultWithBasicButtonSet = (
|
|
|
22
30
|
): React.ReactElement => {
|
|
23
31
|
return <MathInput {...defaultObject} />;
|
|
24
32
|
};
|
|
25
|
-
export const AlwaysVisibleButtonSet = (args: StoryArgs): React.ReactElement => {
|
|
26
|
-
return <MathInput {...defaultObject} buttonsVisible="always" />;
|
|
27
|
-
};
|
|
28
33
|
export const DefaultWithAriaLabel = (args: StoryArgs): React.ReactElement => {
|
|
29
34
|
return <MathInput {...defaultObject} labelText="Sample label" />;
|
|
30
35
|
};
|
|
36
|
+
|
|
37
|
+
export const KeypadOpenByDefault = (args: StoryArgs): React.ReactElement => {
|
|
38
|
+
return <MathInput {...defaultObject} buttonsVisible="always" />;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const KeypadNeverVisible = (args: StoryArgs): React.ReactElement => {
|
|
42
|
+
return <MathInput {...defaultObject} buttonsVisible="never" />;
|
|
43
|
+
};
|
|
@@ -7,6 +7,15 @@ import {testDependencies} from "../../../../../testing/test-dependencies";
|
|
|
7
7
|
import * as Dependencies from "../../dependencies";
|
|
8
8
|
import MathInput from "../math-input";
|
|
9
9
|
|
|
10
|
+
const allButtonSets = {
|
|
11
|
+
advancedRelations: true,
|
|
12
|
+
basicRelations: true,
|
|
13
|
+
divisionKey: true,
|
|
14
|
+
logarithms: true,
|
|
15
|
+
preAlgebra: true,
|
|
16
|
+
trigonometry: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
10
19
|
describe("Perseus' MathInput", () => {
|
|
11
20
|
beforeEach(() => {
|
|
12
21
|
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|
|
@@ -19,8 +28,9 @@ describe("Perseus' MathInput", () => {
|
|
|
19
28
|
render(
|
|
20
29
|
<MathInput
|
|
21
30
|
onChange={() => {}}
|
|
22
|
-
|
|
31
|
+
keypadButtonSets={allButtonSets}
|
|
23
32
|
labelText="test"
|
|
33
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
24
34
|
/>,
|
|
25
35
|
);
|
|
26
36
|
|
|
@@ -31,7 +41,13 @@ describe("Perseus' MathInput", () => {
|
|
|
31
41
|
it("is possible to type in the input", () => {
|
|
32
42
|
// Assemble
|
|
33
43
|
const mockOnChange = jest.fn();
|
|
34
|
-
render(
|
|
44
|
+
render(
|
|
45
|
+
<MathInput
|
|
46
|
+
onChange={mockOnChange}
|
|
47
|
+
keypadButtonSets={allButtonSets}
|
|
48
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
35
51
|
|
|
36
52
|
// Act
|
|
37
53
|
userEvent.type(screen.getByRole("textbox"), "12345");
|
|
@@ -43,18 +59,89 @@ describe("Perseus' MathInput", () => {
|
|
|
43
59
|
it("is possible to use buttons", () => {
|
|
44
60
|
// Assemble
|
|
45
61
|
const mockOnChange = jest.fn();
|
|
46
|
-
render(
|
|
62
|
+
render(
|
|
63
|
+
<MathInput
|
|
64
|
+
onChange={mockOnChange}
|
|
65
|
+
keypadButtonSets={allButtonSets}
|
|
66
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
47
69
|
|
|
48
70
|
// Act
|
|
49
|
-
|
|
50
|
-
screen.getByRole("
|
|
51
|
-
userEvent.type(screen.getByRole("textbox"), "1");
|
|
71
|
+
screen.getByRole("switch").click();
|
|
72
|
+
userEvent.click(screen.getByRole("button", {name: "1"}));
|
|
52
73
|
userEvent.click(screen.getByRole("button", {name: "Plus"}));
|
|
53
|
-
userEvent.
|
|
74
|
+
userEvent.click(screen.getByRole("button", {name: "2"}));
|
|
54
75
|
userEvent.click(screen.getByRole("button", {name: "Minus"}));
|
|
55
|
-
userEvent.
|
|
76
|
+
userEvent.click(screen.getByRole("button", {name: "3"}));
|
|
56
77
|
|
|
57
78
|
// Assert
|
|
58
79
|
expect(mockOnChange).toHaveBeenLastCalledWith("1+2-3");
|
|
59
80
|
});
|
|
81
|
+
|
|
82
|
+
it("is possible to use buttons with legacy props", () => {
|
|
83
|
+
// Assemble
|
|
84
|
+
const mockOnChange = jest.fn();
|
|
85
|
+
render(
|
|
86
|
+
<MathInput
|
|
87
|
+
onChange={mockOnChange}
|
|
88
|
+
buttonSets={["basic+div"]}
|
|
89
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
90
|
+
/>,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Act
|
|
94
|
+
// focusing the input triggers the popover
|
|
95
|
+
screen.getByRole("switch").click();
|
|
96
|
+
userEvent.click(screen.getByRole("button", {name: "1"}));
|
|
97
|
+
userEvent.click(screen.getByRole("button", {name: "Plus"}));
|
|
98
|
+
userEvent.click(screen.getByRole("button", {name: "2"}));
|
|
99
|
+
userEvent.click(screen.getByRole("button", {name: "Divide"}));
|
|
100
|
+
userEvent.click(screen.getByRole("button", {name: "3"}));
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
expect(mockOnChange).toHaveBeenLastCalledWith("1+2\\div3");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns focus to input after button click", () => {
|
|
107
|
+
// Assemble
|
|
108
|
+
render(
|
|
109
|
+
<MathInput
|
|
110
|
+
onChange={() => {}}
|
|
111
|
+
keypadButtonSets={allButtonSets}
|
|
112
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
// focusing the input triggers the popover
|
|
118
|
+
screen.getByRole("switch").click();
|
|
119
|
+
userEvent.click(screen.getByRole("button", {name: "1"}));
|
|
120
|
+
|
|
121
|
+
// Assert
|
|
122
|
+
expect(screen.getByRole("textbox")).toHaveFocus();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not return focus to input after button press via keyboard", () => {
|
|
126
|
+
// Assemble
|
|
127
|
+
render(
|
|
128
|
+
<MathInput
|
|
129
|
+
onChange={() => {}}
|
|
130
|
+
keypadButtonSets={allButtonSets}
|
|
131
|
+
analytics={{onAnalyticsEvent: () => Promise.resolve()}}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Act
|
|
136
|
+
// focusing the input triggers the popover
|
|
137
|
+
screen.getByRole("switch").click();
|
|
138
|
+
userEvent.tab(); // to "123" tab
|
|
139
|
+
userEvent.tab(); // to extra keys tab
|
|
140
|
+
userEvent.tab(); // to whole keypad
|
|
141
|
+
userEvent.tab(); // to "1" button
|
|
142
|
+
userEvent.keyboard("{enter}");
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
expect(screen.getByRole("textbox")).not.toHaveFocus();
|
|
146
|
+
});
|
|
60
147
|
});
|