@khanacademy/perseus 8.0.0 → 10.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/article-renderer.d.ts +2 -1
  3. package/dist/components/info-tip.d.ts +2 -1
  4. package/dist/components/input-with-examples.d.ts +1 -1
  5. package/dist/components/sortable.d.ts +1 -1
  6. package/dist/dependencies.d.ts +7 -1
  7. package/dist/es/index.css +3 -5
  8. package/dist/es/index.js +72 -45
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/hint-renderer.d.ts +2 -1
  11. package/dist/hints-renderer.d.ts +2 -1
  12. package/dist/index.css +3 -5
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +72 -46
  15. package/dist/index.js.map +1 -1
  16. package/dist/interaction-tracker.d.ts +3 -3
  17. package/dist/item-renderer.d.ts +3 -1
  18. package/dist/multi-items/multi-renderer.d.ts +4 -2
  19. package/dist/perseus-api.d.ts +1 -1
  20. package/dist/renderer.d.ts +4 -9
  21. package/dist/server-item-renderer.d.ts +3 -1
  22. package/dist/types.d.ts +30 -15
  23. package/dist/widget-container.d.ts +2 -1
  24. package/dist/widgets/expression.d.ts +409 -12
  25. package/dist/widgets/graded-group.d.ts +2 -2
  26. package/dist/widgets/grapher.d.ts +2 -1
  27. package/dist/widgets/passage-ref.d.ts +2 -1
  28. package/dist/widgets/sequence.d.ts +2 -2
  29. package/package.json +3 -3
  30. package/src/__stories__/article-renderer.stories.tsx +35 -2
  31. package/src/__testdata__/article-renderer.testdata.ts +71 -0
  32. package/src/__tests__/item-renderer.test.tsx +12 -1
  33. package/src/__tests__/renderer.test.tsx +0 -2
  34. package/src/__tests__/server-item-renderer.test.tsx +11 -5
  35. package/src/article-renderer.tsx +38 -30
  36. package/src/components/info-tip.tsx +3 -4
  37. package/src/components/input-with-examples.tsx +1 -1
  38. package/src/components/sortable.tsx +1 -1
  39. package/src/dependencies.ts +16 -1
  40. package/src/hint-renderer.tsx +2 -1
  41. package/src/hints-renderer.tsx +2 -9
  42. package/src/index.ts +0 -1
  43. package/src/interaction-tracker.ts +3 -3
  44. package/src/item-renderer.tsx +57 -44
  45. package/src/multi-items/__tests__/multi-renderer.test.tsx +6 -1
  46. package/src/multi-items/multi-renderer.tsx +18 -11
  47. package/src/renderer.tsx +4 -4
  48. package/src/server-item-renderer.tsx +21 -17
  49. package/src/types.ts +50 -21
  50. package/src/widget-container.tsx +2 -1
  51. package/src/widgets/__stories__/definition.stories.tsx +8 -1
  52. package/src/widgets/__stories__/expression.stories.tsx +2 -2
  53. package/src/widgets/__tests__/{expression.test.ts → expression.test.tsx} +73 -8
  54. package/src/widgets/__tests__/renderQuestion.tsx +29 -29
  55. package/src/widgets/expression.tsx +77 -48
  56. package/src/widgets/graded-group.tsx +11 -2
  57. package/src/widgets/grapher.tsx +4 -12
  58. package/src/widgets/passage-ref.tsx +1 -1
  59. package/src/widgets/sequence.tsx +10 -2
  60. package/tsconfig-build.tsbuildinfo +1 -1
@@ -12,7 +12,7 @@ import Tooltip, {
12
12
  HorizontalDirection,
13
13
  VerticalDirection,
14
14
  } from "../components/tooltip";
15
- import {getDependencies} from "../dependencies";
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,19 +23,14 @@ import type {
23
23
  PerseusExpressionWidgetOptions,
24
24
  PerseusExpressionAnswerForm,
25
25
  } from "../perseus-types";
26
- import type {PerseusScore, WidgetExports, WidgetProps} from "../types";
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
-
29
- const sendExpressionEvaluatedEvent = (
30
- result: "correct" | "incorrect" | "invalid",
31
- ) => {
32
- getDependencies().analytics({
33
- type: "perseus:expression-evaluated",
34
- payload: {
35
- result,
36
- },
37
- });
38
- };
33
+ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
39
34
 
40
35
  type InputPath = ReadonlyArray<string>;
41
36
 
@@ -63,20 +58,43 @@ const insertBraces = (value) => {
63
58
  return value.replace(/([_^])([^{])/g, "$1{$2}");
64
59
  };
65
60
 
61
+ const deriveKeypadVersion = (apiOptions: APIOptions) => {
62
+ // We can derive which version of the keypad is in use. This is
63
+ // a bit tricky, but this code will be relatively short-lived
64
+ // as we coalesce onto the new, v2 Keypad, at which point we
65
+ // can remove this `virtualKeypadVersion` field entirely.
66
+ return apiOptions.nativeKeypadProxy != null
67
+ ? "REACT_NATIVE_KEYPAD"
68
+ : apiOptions.customKeypad === true
69
+ ? apiOptions.useV2Keypad === true
70
+ ? "MATH_INPUT_KEYPAD_V2"
71
+ : "MATH_INPUT_KEYPAD_V1"
72
+ : "PERSEUS_MATH_INPUT";
73
+ };
74
+
66
75
  type Rubric = PerseusExpressionWidgetOptions;
67
76
 
77
+ type RenderProps = {
78
+ buttonSets: PerseusExpressionWidgetOptions["buttonSets"];
79
+ buttonsVisible?: PerseusExpressionWidgetOptions["buttonsVisible"];
80
+ functions: PerseusExpressionWidgetOptions["functions"];
81
+ times: PerseusExpressionWidgetOptions["times"];
82
+ keypadConfiguration: ReturnType<typeof keypadConfigurationForProps>;
83
+ };
84
+
68
85
  type ExternalProps = WidgetProps<RenderProps, Rubric>;
69
86
 
70
- export type Props = ExternalProps & {
71
- apiOptions: NonNullable<ExternalProps["apiOptions"]>;
72
- buttonSets: NonNullable<ExternalProps["buttonSets"]>;
73
- functions: NonNullable<ExternalProps["functions"]>;
74
- linterContext: NonNullable<ExternalProps["linterContext"]>;
75
- onBlur: NonNullable<ExternalProps["onBlur"]>;
76
- onFocus: NonNullable<ExternalProps["onFocus"]>;
77
- times: NonNullable<ExternalProps["times"]>;
78
- value: string;
79
- };
87
+ export type Props = ExternalProps &
88
+ ReturnType<typeof useDependencies> & {
89
+ apiOptions: NonNullable<ExternalProps["apiOptions"]>;
90
+ buttonSets: NonNullable<ExternalProps["buttonSets"]>;
91
+ functions: NonNullable<ExternalProps["functions"]>;
92
+ linterContext: NonNullable<ExternalProps["linterContext"]>;
93
+ onBlur: NonNullable<ExternalProps["onBlur"]>;
94
+ onFocus: NonNullable<ExternalProps["onFocus"]>;
95
+ times: NonNullable<ExternalProps["times"]>;
96
+ value: string;
97
+ };
80
98
 
81
99
  export type ExpressionState = {
82
100
  showErrorTooltip: boolean;
@@ -211,8 +229,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
211
229
  // we did, whether it's considered correct, incorrect, or ungraded
212
230
  if (!matchingAnswerForm) {
213
231
  if (firstUngradedResult) {
214
- sendExpressionEvaluatedEvent("invalid");
215
-
216
232
  // While we didn't directly match with any answer form, we
217
233
  // did at some point get an "ungraded" validation result,
218
234
  // which might indicate e.g. a mismatch in variable casing.
@@ -227,8 +243,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
227
243
  };
228
244
  }
229
245
  if (allEmpty) {
230
- sendExpressionEvaluatedEvent("invalid");
231
-
232
246
  // If everything graded as empty, it's invalid.
233
247
  return {
234
248
  type: "invalid",
@@ -237,7 +251,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
237
251
  }
238
252
  // We fell through all the possibilities and we're not empty,
239
253
  // so the answer is considered incorrect.
240
- sendExpressionEvaluatedEvent("incorrect");
241
254
  return {
242
255
  type: "points",
243
256
  earned: 0,
@@ -252,7 +265,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
252
265
  userInput,
253
266
  matchMessage,
254
267
  );
255
- sendExpressionEvaluatedEvent("invalid");
256
268
  return {
257
269
  type: "invalid",
258
270
  message: apiResult === false ? null : matchMessage,
@@ -264,12 +276,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
264
276
  // TODO(eater): Seems silly to translate result to this
265
277
  // invalid/points thing and immediately translate it back in
266
278
  // ItemRenderer.scoreInput()
267
- sendExpressionEvaluatedEvent(
268
- matchingAnswerForm.considered === "correct"
269
- ? "correct"
270
- : "incorrect",
271
- );
272
-
273
279
  return {
274
280
  type: "points",
275
281
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -369,7 +375,21 @@ export class Expression extends React.Component<Props, ExpressionState> {
369
375
  onInputError: OnInputErrorFunctionType,
370
376
  ) => PerseusScore = (rubric, onInputError) => {
371
377
  onInputError = onInputError || function () {};
372
- return Expression.validate(this.getUserInput(), rubric, onInputError);
378
+ const result = Expression.validate(
379
+ this.getUserInput(),
380
+ rubric,
381
+ onInputError,
382
+ );
383
+
384
+ this.sendExpressionEvaluatedEvent(
385
+ result.type === "invalid"
386
+ ? "invalid"
387
+ : result.earned === result.total
388
+ ? "correct"
389
+ : "incorrect",
390
+ );
391
+
392
+ return result;
373
393
  };
374
394
 
375
395
  getUserInput: () => string = () => {
@@ -471,6 +491,18 @@ export class Expression extends React.Component<Props, ExpressionState> {
471
491
  );
472
492
  };
473
493
 
494
+ sendExpressionEvaluatedEvent(result: "correct" | "incorrect" | "invalid") {
495
+ this.props.analytics.onAnalyticsEvent({
496
+ type: "perseus:expression-evaluated",
497
+ payload: {
498
+ result,
499
+ virtualKeypadVersion: deriveKeypadVersion(
500
+ this.props.apiOptions,
501
+ ),
502
+ },
503
+ });
504
+ }
505
+
474
506
  render():
475
507
  | React.ReactNode
476
508
  | React.ReactElement<React.ComponentProps<"div">> {
@@ -640,22 +672,19 @@ const propUpgrades = {
640
672
  }),
641
673
  } as const;
642
674
 
643
- type RenderProps = {
644
- buttonSets: any;
645
- buttonsVisible?: "always" | "focused" | "never";
646
- functions: ReadonlyArray<string>;
647
- keypadConfiguration: {
648
- extraKeys: ReadonlyArray<any | string>;
649
- keypadType: any;
650
- };
651
- times: boolean;
652
- };
675
+ const ExpressionWithDependencies = React.forwardRef<
676
+ Expression,
677
+ Omit<PropsFor<typeof Expression>, keyof ReturnType<typeof useDependencies>>
678
+ >((props, ref) => {
679
+ const deps = useDependencies();
680
+ return <Expression ref={ref} analytics={deps.analytics} {...props} />;
681
+ });
653
682
 
654
683
  export default {
655
684
  name: "expression",
656
685
  displayName: "Expression / Equation",
657
686
  defaultAlignment: "inline-block",
658
- widget: Expression,
687
+ widget: ExpressionWithDependencies,
659
688
  transform: (widgetOptions: PerseusExpressionWidgetOptions): RenderProps => {
660
689
  const {times, functions, buttonSets, buttonsVisible} = widgetOptions;
661
690
  return {
@@ -671,4 +700,4 @@ export default {
671
700
 
672
701
  // For use by the editor
673
702
  isLintable: true,
674
- } as WidgetExports<typeof Expression>;
703
+ } as WidgetExports<typeof ExpressionWithDependencies>;
@@ -25,7 +25,12 @@ import a11y from "../util/a11y";
25
25
  import GradedGroupAnswerBar from "./graded-group-answer-bar";
26
26
 
27
27
  import type {PerseusGradedGroupWidgetOptions} from "../perseus-types";
28
- import type {PerseusScore, WidgetExports, WidgetProps} from "../types";
28
+ import type {
29
+ PerseusScore,
30
+ TrackingGradedGroupExtraArguments,
31
+ WidgetExports,
32
+ WidgetProps,
33
+ } from "../types";
29
34
  import type {ANSWER_BAR_STATES} from "./graded-group-answer-bar";
30
35
 
31
36
  const GRADING_STATUSES = {
@@ -64,7 +69,11 @@ const DEFAULT_INVALID_MESSAGE =
64
69
  type Rubric = PerseusGradedGroupWidgetOptions;
65
70
  type RenderProps = PerseusGradedGroupWidgetOptions; // exports has no 'transform'
66
71
 
67
- type Props = WidgetProps<RenderProps, Rubric> & {
72
+ type Props = WidgetProps<
73
+ RenderProps,
74
+ Rubric,
75
+ TrackingGradedGroupExtraArguments
76
+ > & {
68
77
  inGradedGroupSet?: boolean; // Set by graded-group-set.jsx,
69
78
  onNextQuestion?: () => unknown; // Set by graded-group-set.jsx
70
79
  };
@@ -36,6 +36,7 @@ import type {ChangeableProps} from "../mixins/changeable";
36
36
  import type {PerseusGrapherWidgetOptions} from "../perseus-types";
37
37
  import type {PerseusScore, WidgetExports, WidgetProps} from "../types";
38
38
  import type {GridDimensions} from "../util";
39
+ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
39
40
 
40
41
  // @ts-expect-error - TS2339 - Property 'MovablePoint' does not exist on type 'typeof Graphie'.
41
42
  const MovablePoint = Graphie.MovablePoint;
@@ -473,18 +474,9 @@ class Grapher extends React.Component<Props> {
473
474
  }
474
475
 
475
476
  _calculateMobileTickStep(
476
- gridStep: JSX.LibraryManagedAttributes<
477
- typeof Graphie,
478
- Required<React.ComponentProps<typeof Graphie>>
479
- >["gridStep"],
480
- step: JSX.LibraryManagedAttributes<
481
- typeof Graphie,
482
- Required<React.ComponentProps<typeof Graphie>>
483
- >["step"],
484
- ranges: JSX.LibraryManagedAttributes<
485
- typeof Graphie,
486
- Required<React.ComponentProps<typeof Graphie>>
487
- >["ranges"],
477
+ gridStep: NonNullable<PropsFor<typeof Graphie>["gridStep"]>,
478
+ step: NonNullable<PropsFor<typeof Graphie>["step"]>,
479
+ ranges: NonNullable<PropsFor<typeof Graphie>["ranges"]>,
488
480
  ): any {
489
481
  const tickStep = Util.constrainedTickStepsFromTickSteps(step, ranges);
490
482
 
@@ -13,9 +13,9 @@ import type {
13
13
  PerseusScore,
14
14
  WidgetExports,
15
15
  WidgetProps,
16
- LinterContextProps,
17
16
  } from "../types";
18
17
  import type {Passage, Reference} from "./passage";
18
+ import type {LinterContextProps} from "@khanacademy/perseus-linter";
19
19
 
20
20
  const EN_DASH = "\u2013";
21
21
 
@@ -9,11 +9,19 @@ import Renderer from "../renderer";
9
9
  import Util from "../util";
10
10
 
11
11
  import type {PerseusSequenceWidgetOptions} from "../perseus-types";
12
- import type {WidgetExports, WidgetProps} from "../types";
12
+ import type {
13
+ TrackingSequenceExtraArguments,
14
+ WidgetExports,
15
+ WidgetProps,
16
+ } from "../types";
13
17
 
14
18
  type Rubric = PerseusSequenceWidgetOptions;
15
19
 
16
- type ExternalProps = WidgetProps<PerseusSequenceWidgetOptions, Rubric>;
20
+ type ExternalProps = WidgetProps<
21
+ PerseusSequenceWidgetOptions,
22
+ Rubric,
23
+ TrackingSequenceExtraArguments
24
+ >;
17
25
 
18
26
  type Props = ExternalProps;
19
27