@khanacademy/perseus 8.0.0 → 9.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 +21 -0
- package/dist/article-renderer.d.ts +2 -1
- package/dist/dependencies.d.ts +7 -1
- package/dist/es/index.js +66 -29
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +66 -29
- package/dist/index.js.map +1 -1
- package/dist/interaction-tracker.d.ts +3 -3
- package/dist/item-renderer.d.ts +2 -1
- package/dist/multi-items/multi-renderer.d.ts +2 -1
- package/dist/perseus-api.d.ts +1 -1
- package/dist/renderer.d.ts +1 -1
- package/dist/server-item-renderer.d.ts +3 -1
- package/dist/types.d.ts +29 -9
- package/dist/widgets/expression.d.ts +409 -12
- package/dist/widgets/graded-group.d.ts +2 -2
- package/dist/widgets/sequence.d.ts +2 -2
- package/package.json +3 -3
- package/src/__stories__/article-renderer.stories.tsx +35 -2
- package/src/__testdata__/article-renderer.testdata.ts +71 -0
- package/src/__tests__/item-renderer.test.tsx +12 -1
- package/src/__tests__/server-item-renderer.test.tsx +9 -1
- package/src/article-renderer.tsx +38 -30
- package/src/dependencies.ts +16 -1
- package/src/interaction-tracker.ts +3 -3
- package/src/item-renderer.tsx +62 -44
- package/src/multi-items/__tests__/multi-renderer.test.tsx +6 -1
- package/src/multi-items/multi-renderer.tsx +16 -4
- package/src/renderer.tsx +1 -1
- package/src/server-item-renderer.tsx +21 -16
- package/src/types.ts +49 -13
- package/src/widgets/__stories__/definition.stories.tsx +8 -1
- package/src/widgets/__stories__/expression.stories.tsx +2 -2
- package/src/widgets/__tests__/{expression.test.ts → expression.test.tsx} +73 -8
- package/src/widgets/__tests__/renderQuestion.tsx +26 -18
- package/src/widgets/expression.tsx +82 -48
- package/src/widgets/graded-group.tsx +11 -2
- package/src/widgets/sequence.tsx +10 -2
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -12,6 +12,7 @@ import * as React from "react";
|
|
|
12
12
|
import _ from "underscore";
|
|
13
13
|
|
|
14
14
|
import AssetContext from "./asset-context";
|
|
15
|
+
import {DependenciesContext} from "./dependencies";
|
|
15
16
|
import HintsRenderer from "./hints-renderer";
|
|
16
17
|
import Objective from "./interactive2/objective_";
|
|
17
18
|
import LoadingContext from "./loading-context";
|
|
@@ -20,7 +21,7 @@ import Renderer from "./renderer";
|
|
|
20
21
|
import Util from "./util";
|
|
21
22
|
|
|
22
23
|
import type {KeypadProps} from "./mixins/provide-keypad";
|
|
23
|
-
import type {APIOptions, FocusPath} from "./types";
|
|
24
|
+
import type {APIOptions, FocusPath, PerseusDependenciesV2} from "./types";
|
|
24
25
|
import type {RendererInterface, KEScore} from "@khanacademy/perseus-core";
|
|
25
26
|
|
|
26
27
|
const {mapObject} = Objective;
|
|
@@ -37,6 +38,8 @@ type OwnProps = // These props are used by the ProvideKeypad mixin.
|
|
|
37
38
|
reviewMode?: boolean;
|
|
38
39
|
// from KeypadContext
|
|
39
40
|
keypadElement?: any | null | undefined;
|
|
41
|
+
|
|
42
|
+
dependencies: PerseusDependenciesV2;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
type HOCProps = {
|
|
@@ -425,22 +428,24 @@ export class ServerItemRenderer
|
|
|
425
428
|
);
|
|
426
429
|
|
|
427
430
|
return (
|
|
428
|
-
<
|
|
429
|
-
<div>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
431
|
+
<DependenciesContext.Provider value={this.props.dependencies}>
|
|
432
|
+
<div>
|
|
433
|
+
<div>{questionRenderer}</div>
|
|
434
|
+
<div
|
|
435
|
+
className={
|
|
436
|
+
// Avoid adding any horizontal padding when applying the
|
|
437
|
+
// mobile hint styles, which are flush to the left.
|
|
438
|
+
// NOTE(charlie): We may still want to apply this
|
|
439
|
+
// padding for desktop exercises.
|
|
440
|
+
apiOptions.isMobile
|
|
441
|
+
? undefined
|
|
442
|
+
: css(styles.hintsContainer)
|
|
443
|
+
}
|
|
444
|
+
>
|
|
445
|
+
{hintsRenderer}
|
|
446
|
+
</div>
|
|
442
447
|
</div>
|
|
443
|
-
</
|
|
448
|
+
</DependenciesContext.Provider>
|
|
444
449
|
);
|
|
445
450
|
}
|
|
446
451
|
}
|
package/src/types.ts
CHANGED
|
@@ -4,12 +4,15 @@ import type {Item} from "./multi-items/item-types";
|
|
|
4
4
|
import type {PerseusWidget} from "./perseus-types";
|
|
5
5
|
import type {SizeClass} from "./util/sizing-utils";
|
|
6
6
|
import type {KeypadAPI} from "@khanacademy/math-input";
|
|
7
|
-
import type {
|
|
7
|
+
import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
|
|
8
8
|
import type {Result} from "@khanacademy/wonder-blocks-data";
|
|
9
9
|
import type * as React from "react";
|
|
10
10
|
|
|
11
11
|
export type FocusPath = ReadonlyArray<string> | null | undefined;
|
|
12
12
|
|
|
13
|
+
// TODO(FEI-5054): Figure out how to get global .d.ts files working with monorepos
|
|
14
|
+
type Empty = Record<never, never>;
|
|
15
|
+
|
|
13
16
|
export type Dimensions = {
|
|
14
17
|
width?: number;
|
|
15
18
|
height?: number;
|
|
@@ -109,6 +112,20 @@ export type Path = ReadonlyArray<string>;
|
|
|
109
112
|
|
|
110
113
|
type StubTagEditorType = any; // from "./components/stub-tag-editor";
|
|
111
114
|
|
|
115
|
+
type TrackInteractionArgs = {
|
|
116
|
+
// The widget type that this interaction originates from
|
|
117
|
+
type: string;
|
|
118
|
+
// The widget id that this interaction originates from
|
|
119
|
+
id: string;
|
|
120
|
+
|
|
121
|
+
correct?: boolean;
|
|
122
|
+
|
|
123
|
+
// Tracking args are all optional here because we don't know which
|
|
124
|
+
// widgets originated the call, and thus can't know what extra
|
|
125
|
+
// arguments will be included!
|
|
126
|
+
} & Partial<TrackingGradedGroupExtraArguments> &
|
|
127
|
+
Partial<TrackingSequenceExtraArguments>;
|
|
128
|
+
|
|
112
129
|
// APIOptions provides different ways to customize the behaviour of Perseus.
|
|
113
130
|
export type APIOptions = Readonly<{
|
|
114
131
|
isArticle?: boolean;
|
|
@@ -162,15 +179,7 @@ export type APIOptions = Readonly<{
|
|
|
162
179
|
// A function that is called when the user has interacted with a widget. It
|
|
163
180
|
// also includes any extra parameters that the originating widget provided.
|
|
164
181
|
// This is used for keeping track of widget interactions.
|
|
165
|
-
trackInteraction?: (
|
|
166
|
-
args: {
|
|
167
|
-
// The widget type that this interaction originates from
|
|
168
|
-
type: string;
|
|
169
|
-
// The widget id that this interaction originates from
|
|
170
|
-
id: string;
|
|
171
|
-
correct?: boolean;
|
|
172
|
-
} & Record<string, unknown>,
|
|
173
|
-
) => void;
|
|
182
|
+
trackInteraction?: (args: TrackInteractionArgs) => void;
|
|
174
183
|
// A boolean that indicates whether or not a custom keypad is
|
|
175
184
|
// being used. For mobile web this will be the ProvidedKeypad
|
|
176
185
|
// component. In this situation we use the MathInput component
|
|
@@ -310,7 +319,6 @@ export type PerseusDependencies = {
|
|
|
310
319
|
//misc
|
|
311
320
|
staticUrl: StaticUrlFn;
|
|
312
321
|
InitialRequestUrl: InitialRequestUrlInterface;
|
|
313
|
-
analytics: SendEventFn;
|
|
314
322
|
|
|
315
323
|
// video widget
|
|
316
324
|
// This is used as a hook to fetch data about a video which is used to
|
|
@@ -332,6 +340,18 @@ export type PerseusDependencies = {
|
|
|
332
340
|
isMobile: boolean;
|
|
333
341
|
};
|
|
334
342
|
|
|
343
|
+
/**
|
|
344
|
+
* The modern iteration of Perseus Depedndencies. These dependencies are
|
|
345
|
+
* provided to Perseus through its entrypoints (for example:
|
|
346
|
+
* ServerItemRenderer) and then attached to the DependenciesContext so they are
|
|
347
|
+
* available anywhere down the React render tree.
|
|
348
|
+
*
|
|
349
|
+
* Prefer using this type over `PerseusDependencies` when possible.
|
|
350
|
+
*/
|
|
351
|
+
export type PerseusDependenciesV2 = {
|
|
352
|
+
analytics: {onAnalyticsEvent: AnalyticsEventHandlerFn};
|
|
353
|
+
};
|
|
354
|
+
|
|
335
355
|
export type APIOptionsWithDefaults = Readonly<
|
|
336
356
|
APIOptions & {
|
|
337
357
|
GroupMetadataEditor: NonNullable<APIOptions["GroupMetadataEditor"]>;
|
|
@@ -369,6 +389,16 @@ export type Tracking =
|
|
|
369
389
|
// Track all interactions
|
|
370
390
|
| "all";
|
|
371
391
|
|
|
392
|
+
// See graded-group widget
|
|
393
|
+
export type TrackingGradedGroupExtraArguments = {
|
|
394
|
+
status: "correct" | "incorrect" | "invalid";
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// See sequence widget
|
|
398
|
+
export type TrackingSequenceExtraArguments = {
|
|
399
|
+
visible: number;
|
|
400
|
+
};
|
|
401
|
+
|
|
372
402
|
export type Alignment =
|
|
373
403
|
| "default"
|
|
374
404
|
| "block"
|
|
@@ -443,7 +473,13 @@ export type FilterCriterion =
|
|
|
443
473
|
) => boolean);
|
|
444
474
|
|
|
445
475
|
// NOTE: Rubric should always be the corresponding widget options type for the component.
|
|
446
|
-
export type WidgetProps<
|
|
476
|
+
export type WidgetProps<
|
|
477
|
+
RenderProps,
|
|
478
|
+
Rubric,
|
|
479
|
+
// Defines the arguments that can be passed to the `trackInteraction`
|
|
480
|
+
// function from APIOptions for this widget.
|
|
481
|
+
TrackingExtraArgs = Empty,
|
|
482
|
+
> = RenderProps & {
|
|
447
483
|
// provided by renderer.jsx#getWidgetProps()
|
|
448
484
|
widgetId: string;
|
|
449
485
|
alignment: string | null | undefined;
|
|
@@ -463,7 +499,7 @@ export type WidgetProps<RenderProps, Rubric> = RenderProps & {
|
|
|
463
499
|
// APIOptions. This provides the widget an easy way to notify the renderer
|
|
464
500
|
// of an interaction. The Renderer then enriches the data provided with the
|
|
465
501
|
// widget's id and type before calling APIOptions.trackInteraction.
|
|
466
|
-
trackInteraction: (extraData?:
|
|
502
|
+
trackInteraction: (extraData?: TrackingExtraArgs) => void;
|
|
467
503
|
isLastUsedWidget: boolean;
|
|
468
504
|
// provided by widget-container.jsx#render()
|
|
469
505
|
linterContext: LinterContextProps;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
|
|
4
|
+
import {storybookDependenciesV2} from "../../../../../testing/test-dependencies";
|
|
4
5
|
import ArticleRenderer from "../../article-renderer";
|
|
5
6
|
|
|
6
7
|
export default {
|
|
@@ -103,5 +104,11 @@ export const MultipleDefinitions = (args: StoryArgs): React.ReactElement => {
|
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
export const ArticleDefintion = (args: StoryArgs): React.ReactElement => {
|
|
106
|
-
return
|
|
107
|
+
return (
|
|
108
|
+
<ArticleRenderer
|
|
109
|
+
json={article}
|
|
110
|
+
useNewStyles
|
|
111
|
+
dependencies={storybookDependenciesV2}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
107
114
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {KeypadContext} from "@khanacademy/math-input";
|
|
1
|
+
import {KeypadContext, KeypadType} from "@khanacademy/math-input";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
|
|
4
4
|
import {ItemRendererWithDebugUI} from "../../../../../testing/item-renderer-with-debug-ui";
|
|
@@ -62,7 +62,7 @@ export const DesktopKitchenSink = (args: StoryArgs): React.ReactElement => {
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
const keypadConfiguration = {
|
|
65
|
-
keypadType:
|
|
65
|
+
keypadType: KeypadType.EXPRESSION,
|
|
66
66
|
extraKeys: ["x", "y", "z"],
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -3,7 +3,10 @@ import {screen, fireEvent} from "@testing-library/react";
|
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import "@testing-library/jest-dom";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
testDependencies,
|
|
8
|
+
testDependenciesV2,
|
|
9
|
+
} from "../../../../../testing/test-dependencies";
|
|
7
10
|
import * as Dependencies from "../../dependencies";
|
|
8
11
|
import {
|
|
9
12
|
expressionItem2,
|
|
@@ -15,8 +18,13 @@ import {Expression} from "../expression";
|
|
|
15
18
|
import {renderQuestion} from "./renderQuestion";
|
|
16
19
|
|
|
17
20
|
import type {PerseusItem} from "../../perseus-types";
|
|
21
|
+
import type {APIOptions} from "../../types";
|
|
18
22
|
|
|
19
|
-
const assertComplete = (
|
|
23
|
+
const assertComplete = (
|
|
24
|
+
itemData: PerseusItem,
|
|
25
|
+
input: string,
|
|
26
|
+
isCorrect: boolean,
|
|
27
|
+
) => {
|
|
20
28
|
const {renderer} = renderQuestion(itemData.question);
|
|
21
29
|
userEvent.type(screen.getByRole("textbox"), input);
|
|
22
30
|
const [_, score] = renderer.guessAndScore();
|
|
@@ -27,13 +35,14 @@ const assertComplete = (itemData: PerseusItem, input, isCorrect: boolean) => {
|
|
|
27
35
|
});
|
|
28
36
|
};
|
|
29
37
|
|
|
30
|
-
const assertCorrect = (itemData: PerseusItem, input) => {
|
|
38
|
+
const assertCorrect = (itemData: PerseusItem, input: string) => {
|
|
31
39
|
assertComplete(itemData, input, true);
|
|
32
40
|
|
|
33
|
-
expect(
|
|
41
|
+
expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
|
|
34
42
|
type: "perseus:expression-evaluated",
|
|
35
43
|
payload: {
|
|
36
44
|
result: "correct",
|
|
45
|
+
virtualKeypadVersion: "PERSEUS_MATH_INPUT",
|
|
37
46
|
},
|
|
38
47
|
});
|
|
39
48
|
};
|
|
@@ -41,16 +50,21 @@ const assertCorrect = (itemData: PerseusItem, input) => {
|
|
|
41
50
|
const assertIncorrect = (itemData: PerseusItem, input: string) => {
|
|
42
51
|
assertComplete(itemData, input, false);
|
|
43
52
|
|
|
44
|
-
expect(
|
|
53
|
+
expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
|
|
45
54
|
type: "perseus:expression-evaluated",
|
|
46
55
|
payload: {
|
|
47
56
|
result: "incorrect",
|
|
57
|
+
virtualKeypadVersion: "PERSEUS_MATH_INPUT",
|
|
48
58
|
},
|
|
49
59
|
});
|
|
50
60
|
};
|
|
51
61
|
|
|
52
62
|
// TODO: actually Assert that message is being set on the score object.
|
|
53
|
-
const assertInvalid = (
|
|
63
|
+
const assertInvalid = (
|
|
64
|
+
itemData: PerseusItem,
|
|
65
|
+
input: string,
|
|
66
|
+
message?: string,
|
|
67
|
+
) => {
|
|
54
68
|
const {renderer} = renderQuestion(itemData.question);
|
|
55
69
|
if (input.length) {
|
|
56
70
|
userEvent.type(screen.getByRole("textbox"), input);
|
|
@@ -58,17 +72,18 @@ const assertInvalid = (itemData: PerseusItem, input, message?: string) => {
|
|
|
58
72
|
const [_, score] = renderer.guessAndScore();
|
|
59
73
|
expect(score).toMatchObject({type: "invalid"});
|
|
60
74
|
|
|
61
|
-
expect(
|
|
75
|
+
expect(testDependenciesV2.analytics.onAnalyticsEvent).toHaveBeenCalledWith({
|
|
62
76
|
type: "perseus:expression-evaluated",
|
|
63
77
|
payload: {
|
|
64
78
|
result: "invalid",
|
|
79
|
+
virtualKeypadVersion: "PERSEUS_MATH_INPUT",
|
|
65
80
|
},
|
|
66
81
|
});
|
|
67
82
|
};
|
|
68
83
|
|
|
69
84
|
describe("Expression Widget", function () {
|
|
70
85
|
beforeEach(() => {
|
|
71
|
-
jest.spyOn(
|
|
86
|
+
jest.spyOn(testDependenciesV2.analytics, "onAnalyticsEvent");
|
|
72
87
|
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|
|
73
88
|
testDependencies,
|
|
74
89
|
);
|
|
@@ -171,6 +186,56 @@ describe("Expression Widget", function () {
|
|
|
171
186
|
expect(err).toStrictEqual({message: null, type: "invalid"});
|
|
172
187
|
});
|
|
173
188
|
});
|
|
189
|
+
|
|
190
|
+
describe("analytics", () => {
|
|
191
|
+
const assertKeypadVersion = (
|
|
192
|
+
apiOptions: APIOptions,
|
|
193
|
+
virtualKeypadVersion: string,
|
|
194
|
+
) => {
|
|
195
|
+
const {renderer} = renderQuestion(
|
|
196
|
+
expressionItem2.question,
|
|
197
|
+
apiOptions,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
renderer.guessAndScore();
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
testDependenciesV2.analytics.onAnalyticsEvent,
|
|
204
|
+
).toHaveBeenCalledWith({
|
|
205
|
+
type: "perseus:expression-evaluated",
|
|
206
|
+
payload: {
|
|
207
|
+
// We're not interested in validating that the expression
|
|
208
|
+
// widget did anything useful or that the keypad worked. We
|
|
209
|
+
// just want to make sure the code that derives which
|
|
210
|
+
// keypad version it detected is correct.
|
|
211
|
+
result: "invalid",
|
|
212
|
+
virtualKeypadVersion,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
it("should set the virtual keypad version to REACT_NATIVE_KEYPAD when nativeKeypadProxy is provided", () => {
|
|
218
|
+
assertKeypadVersion(
|
|
219
|
+
{nativeKeypadProxy: jest.fn()},
|
|
220
|
+
"REACT_NATIVE_KEYPAD",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should set the virtual keypad version to MATH_INPUT_KEYPAD_V1 when customKeypad is set and useV2Keypad is unset", () => {
|
|
225
|
+
assertKeypadVersion({customKeypad: true}, "MATH_INPUT_KEYPAD_V1");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should set the virtual keypad version to MATH_INPUT_KEYPAD_V2 when customKeypad is set and useV2Keypad is true", () => {
|
|
229
|
+
assertKeypadVersion(
|
|
230
|
+
{customKeypad: true, useV2Keypad: true},
|
|
231
|
+
"MATH_INPUT_KEYPAD_V2",
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should default the virtual keypad version to PERSEUS_MATH_INPUT", () => {
|
|
236
|
+
assertKeypadVersion(Object.freeze({}), "PERSEUS_MATH_INPUT");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
174
239
|
});
|
|
175
240
|
|
|
176
241
|
describe("getOneCorrectAnswerFromRubric", () => {
|
|
@@ -2,6 +2,9 @@ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
|
|
|
2
2
|
import {render} from "@testing-library/react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
5
|
+
// eslint-disable-next-line import/no-relative-packages
|
|
6
|
+
import {testDependenciesV2} from "../../../../../testing/test-dependencies";
|
|
7
|
+
import {DependenciesContext} from "../../dependencies";
|
|
5
8
|
import * as Perseus from "../../index";
|
|
6
9
|
import {registerAllWidgetsForTesting} from "../../util/register-all-widgets-for-testing";
|
|
7
10
|
|
|
@@ -32,18 +35,21 @@ export const renderQuestion = (
|
|
|
32
35
|
unmount: RenderResult["unmount"];
|
|
33
36
|
} => {
|
|
34
37
|
registerAllWidgetsForTesting();
|
|
38
|
+
|
|
35
39
|
let renderer: Perseus.Renderer | null = null;
|
|
36
40
|
const {container, rerender, unmount} = render(
|
|
37
41
|
<RenderStateRoot>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
<DependenciesContext.Provider value={testDependenciesV2}>
|
|
43
|
+
<Perseus.Renderer
|
|
44
|
+
ref={(node) => (renderer = node)}
|
|
45
|
+
content={question.content}
|
|
46
|
+
images={question.images}
|
|
47
|
+
widgets={question.widgets}
|
|
48
|
+
problemNum={0}
|
|
49
|
+
apiOptions={apiOptions}
|
|
50
|
+
{...extraProps}
|
|
51
|
+
/>
|
|
52
|
+
</DependenciesContext.Provider>
|
|
47
53
|
</RenderStateRoot>,
|
|
48
54
|
);
|
|
49
55
|
if (!renderer) {
|
|
@@ -56,15 +62,17 @@ export const renderQuestion = (
|
|
|
56
62
|
) => {
|
|
57
63
|
rerender(
|
|
58
64
|
<RenderStateRoot>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
<DependenciesContext.Provider value={testDependenciesV2}>
|
|
66
|
+
<Perseus.Renderer
|
|
67
|
+
ref={(node) => (renderer = node)}
|
|
68
|
+
content={question.content}
|
|
69
|
+
images={question.images}
|
|
70
|
+
widgets={question.widgets}
|
|
71
|
+
problemNum={0}
|
|
72
|
+
apiOptions={apiOptions}
|
|
73
|
+
{...extraProps}
|
|
74
|
+
/>
|
|
75
|
+
</DependenciesContext.Provider>
|
|
68
76
|
</RenderStateRoot>,
|
|
69
77
|
);
|
|
70
78
|
if (!renderer) {
|
|
@@ -12,7 +12,7 @@ import Tooltip, {
|
|
|
12
12
|
HorizontalDirection,
|
|
13
13
|
VerticalDirection,
|
|
14
14
|
} from "../components/tooltip";
|
|
15
|
-
import {
|
|
15
|
+
import {useDependencies} from "../dependencies";
|
|
16
16
|
import {iconExclamationSign} from "../icon-paths";
|
|
17
17
|
import {Errors as PerseusErrors, Log} from "../logging/log";
|
|
18
18
|
import * as Changeable from "../mixins/changeable";
|
|
@@ -23,20 +23,14 @@ import type {
|
|
|
23
23
|
PerseusExpressionWidgetOptions,
|
|
24
24
|
PerseusExpressionAnswerForm,
|
|
25
25
|
} from "../perseus-types";
|
|
26
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
APIOptions,
|
|
28
|
+
PerseusScore,
|
|
29
|
+
WidgetExports,
|
|
30
|
+
WidgetProps,
|
|
31
|
+
} from "../types";
|
|
27
32
|
import type {Keys as Key} from "@khanacademy/math-input";
|
|
28
33
|
|
|
29
|
-
const sendExpressionEvaluatedEvent = (
|
|
30
|
-
result: "correct" | "incorrect" | "invalid",
|
|
31
|
-
) => {
|
|
32
|
-
getDependencies().analytics({
|
|
33
|
-
type: "perseus:expression-evaluated",
|
|
34
|
-
payload: {
|
|
35
|
-
result,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
|
|
40
34
|
type InputPath = ReadonlyArray<string>;
|
|
41
35
|
|
|
42
36
|
const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
|
|
@@ -63,20 +57,43 @@ const insertBraces = (value) => {
|
|
|
63
57
|
return value.replace(/([_^])([^{])/g, "$1{$2}");
|
|
64
58
|
};
|
|
65
59
|
|
|
60
|
+
const deriveKeypadVersion = (apiOptions: APIOptions) => {
|
|
61
|
+
// We can derive which version of the keypad is in use. This is
|
|
62
|
+
// a bit tricky, but this code will be relatively short-lived
|
|
63
|
+
// as we coalesce onto the new, v2 Keypad, at which point we
|
|
64
|
+
// can remove this `virtualKeypadVersion` field entirely.
|
|
65
|
+
return apiOptions.nativeKeypadProxy != null
|
|
66
|
+
? "REACT_NATIVE_KEYPAD"
|
|
67
|
+
: apiOptions.customKeypad === true
|
|
68
|
+
? apiOptions.useV2Keypad === true
|
|
69
|
+
? "MATH_INPUT_KEYPAD_V2"
|
|
70
|
+
: "MATH_INPUT_KEYPAD_V1"
|
|
71
|
+
: "PERSEUS_MATH_INPUT";
|
|
72
|
+
};
|
|
73
|
+
|
|
66
74
|
type Rubric = PerseusExpressionWidgetOptions;
|
|
67
75
|
|
|
76
|
+
type RenderProps = {
|
|
77
|
+
buttonSets: PerseusExpressionWidgetOptions["buttonSets"];
|
|
78
|
+
buttonsVisible?: PerseusExpressionWidgetOptions["buttonsVisible"];
|
|
79
|
+
functions: PerseusExpressionWidgetOptions["functions"];
|
|
80
|
+
times: PerseusExpressionWidgetOptions["times"];
|
|
81
|
+
keypadConfiguration: ReturnType<typeof keypadConfigurationForProps>;
|
|
82
|
+
};
|
|
83
|
+
|
|
68
84
|
type ExternalProps = WidgetProps<RenderProps, Rubric>;
|
|
69
85
|
|
|
70
|
-
export type Props = ExternalProps &
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
export type Props = ExternalProps &
|
|
87
|
+
ReturnType<typeof useDependencies> & {
|
|
88
|
+
apiOptions: NonNullable<ExternalProps["apiOptions"]>;
|
|
89
|
+
buttonSets: NonNullable<ExternalProps["buttonSets"]>;
|
|
90
|
+
functions: NonNullable<ExternalProps["functions"]>;
|
|
91
|
+
linterContext: NonNullable<ExternalProps["linterContext"]>;
|
|
92
|
+
onBlur: NonNullable<ExternalProps["onBlur"]>;
|
|
93
|
+
onFocus: NonNullable<ExternalProps["onFocus"]>;
|
|
94
|
+
times: NonNullable<ExternalProps["times"]>;
|
|
95
|
+
value: string;
|
|
96
|
+
};
|
|
80
97
|
|
|
81
98
|
export type ExpressionState = {
|
|
82
99
|
showErrorTooltip: boolean;
|
|
@@ -211,8 +228,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
211
228
|
// we did, whether it's considered correct, incorrect, or ungraded
|
|
212
229
|
if (!matchingAnswerForm) {
|
|
213
230
|
if (firstUngradedResult) {
|
|
214
|
-
sendExpressionEvaluatedEvent("invalid");
|
|
215
|
-
|
|
216
231
|
// While we didn't directly match with any answer form, we
|
|
217
232
|
// did at some point get an "ungraded" validation result,
|
|
218
233
|
// which might indicate e.g. a mismatch in variable casing.
|
|
@@ -227,8 +242,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
227
242
|
};
|
|
228
243
|
}
|
|
229
244
|
if (allEmpty) {
|
|
230
|
-
sendExpressionEvaluatedEvent("invalid");
|
|
231
|
-
|
|
232
245
|
// If everything graded as empty, it's invalid.
|
|
233
246
|
return {
|
|
234
247
|
type: "invalid",
|
|
@@ -237,7 +250,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
237
250
|
}
|
|
238
251
|
// We fell through all the possibilities and we're not empty,
|
|
239
252
|
// so the answer is considered incorrect.
|
|
240
|
-
sendExpressionEvaluatedEvent("incorrect");
|
|
241
253
|
return {
|
|
242
254
|
type: "points",
|
|
243
255
|
earned: 0,
|
|
@@ -252,7 +264,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
252
264
|
userInput,
|
|
253
265
|
matchMessage,
|
|
254
266
|
);
|
|
255
|
-
sendExpressionEvaluatedEvent("invalid");
|
|
256
267
|
return {
|
|
257
268
|
type: "invalid",
|
|
258
269
|
message: apiResult === false ? null : matchMessage,
|
|
@@ -264,12 +275,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
264
275
|
// TODO(eater): Seems silly to translate result to this
|
|
265
276
|
// invalid/points thing and immediately translate it back in
|
|
266
277
|
// ItemRenderer.scoreInput()
|
|
267
|
-
sendExpressionEvaluatedEvent(
|
|
268
|
-
matchingAnswerForm.considered === "correct"
|
|
269
|
-
? "correct"
|
|
270
|
-
: "incorrect",
|
|
271
|
-
);
|
|
272
|
-
|
|
273
278
|
return {
|
|
274
279
|
type: "points",
|
|
275
280
|
earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
|
|
@@ -369,7 +374,21 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
369
374
|
onInputError: OnInputErrorFunctionType,
|
|
370
375
|
) => PerseusScore = (rubric, onInputError) => {
|
|
371
376
|
onInputError = onInputError || function () {};
|
|
372
|
-
|
|
377
|
+
const result = Expression.validate(
|
|
378
|
+
this.getUserInput(),
|
|
379
|
+
rubric,
|
|
380
|
+
onInputError,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
this.sendExpressionEvaluatedEvent(
|
|
384
|
+
result.type === "invalid"
|
|
385
|
+
? "invalid"
|
|
386
|
+
: result.earned === result.total
|
|
387
|
+
? "correct"
|
|
388
|
+
: "incorrect",
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return result;
|
|
373
392
|
};
|
|
374
393
|
|
|
375
394
|
getUserInput: () => string = () => {
|
|
@@ -471,6 +490,18 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
471
490
|
);
|
|
472
491
|
};
|
|
473
492
|
|
|
493
|
+
sendExpressionEvaluatedEvent(result: "correct" | "incorrect" | "invalid") {
|
|
494
|
+
this.props.analytics.onAnalyticsEvent({
|
|
495
|
+
type: "perseus:expression-evaluated",
|
|
496
|
+
payload: {
|
|
497
|
+
result,
|
|
498
|
+
virtualKeypadVersion: deriveKeypadVersion(
|
|
499
|
+
this.props.apiOptions,
|
|
500
|
+
),
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
474
505
|
render():
|
|
475
506
|
| React.ReactNode
|
|
476
507
|
| React.ReactElement<React.ComponentProps<"div">> {
|
|
@@ -640,22 +671,25 @@ const propUpgrades = {
|
|
|
640
671
|
}),
|
|
641
672
|
} as const;
|
|
642
673
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
674
|
+
const ExpressionWithDependencies = React.forwardRef<
|
|
675
|
+
Expression,
|
|
676
|
+
Omit<
|
|
677
|
+
JSX.LibraryManagedAttributes<
|
|
678
|
+
typeof Expression,
|
|
679
|
+
React.ComponentProps<typeof Expression>
|
|
680
|
+
>,
|
|
681
|
+
keyof ReturnType<typeof useDependencies>
|
|
682
|
+
>
|
|
683
|
+
>((props, ref) => {
|
|
684
|
+
const deps = useDependencies();
|
|
685
|
+
return <Expression ref={ref} analytics={deps.analytics} {...props} />;
|
|
686
|
+
});
|
|
653
687
|
|
|
654
688
|
export default {
|
|
655
689
|
name: "expression",
|
|
656
690
|
displayName: "Expression / Equation",
|
|
657
691
|
defaultAlignment: "inline-block",
|
|
658
|
-
widget:
|
|
692
|
+
widget: ExpressionWithDependencies,
|
|
659
693
|
transform: (widgetOptions: PerseusExpressionWidgetOptions): RenderProps => {
|
|
660
694
|
const {times, functions, buttonSets, buttonsVisible} = widgetOptions;
|
|
661
695
|
return {
|
|
@@ -671,4 +705,4 @@ export default {
|
|
|
671
705
|
|
|
672
706
|
// For use by the editor
|
|
673
707
|
isLintable: true,
|
|
674
|
-
} as WidgetExports<typeof
|
|
708
|
+
} as WidgetExports<typeof ExpressionWithDependencies>;
|