@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/article-renderer.d.ts +2 -1
  3. package/dist/dependencies.d.ts +7 -1
  4. package/dist/es/index.js +66 -29
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.js +66 -29
  7. package/dist/index.js.map +1 -1
  8. package/dist/interaction-tracker.d.ts +3 -3
  9. package/dist/item-renderer.d.ts +2 -1
  10. package/dist/multi-items/multi-renderer.d.ts +2 -1
  11. package/dist/perseus-api.d.ts +1 -1
  12. package/dist/renderer.d.ts +1 -1
  13. package/dist/server-item-renderer.d.ts +3 -1
  14. package/dist/types.d.ts +29 -9
  15. package/dist/widgets/expression.d.ts +409 -12
  16. package/dist/widgets/graded-group.d.ts +2 -2
  17. package/dist/widgets/sequence.d.ts +2 -2
  18. package/package.json +3 -3
  19. package/src/__stories__/article-renderer.stories.tsx +35 -2
  20. package/src/__testdata__/article-renderer.testdata.ts +71 -0
  21. package/src/__tests__/item-renderer.test.tsx +12 -1
  22. package/src/__tests__/server-item-renderer.test.tsx +9 -1
  23. package/src/article-renderer.tsx +38 -30
  24. package/src/dependencies.ts +16 -1
  25. package/src/interaction-tracker.ts +3 -3
  26. package/src/item-renderer.tsx +62 -44
  27. package/src/multi-items/__tests__/multi-renderer.test.tsx +6 -1
  28. package/src/multi-items/multi-renderer.tsx +16 -4
  29. package/src/renderer.tsx +1 -1
  30. package/src/server-item-renderer.tsx +21 -16
  31. package/src/types.ts +49 -13
  32. package/src/widgets/__stories__/definition.stories.tsx +8 -1
  33. package/src/widgets/__stories__/expression.stories.tsx +2 -2
  34. package/src/widgets/__tests__/{expression.test.ts → expression.test.tsx} +73 -8
  35. package/src/widgets/__tests__/renderQuestion.tsx +26 -18
  36. package/src/widgets/expression.tsx +82 -48
  37. package/src/widgets/graded-group.tsx +11 -2
  38. package/src/widgets/sequence.tsx +10 -2
  39. package/tsconfig-build.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @khanacademy/perseus
2
2
 
3
+ ## 9.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - dd800c22: Rename analytics prop from onEvent to onAnalyticsEvent
8
+ - 2b99fbda: Remove 'analytics' key from PerseusDependencies
9
+
10
+ ### Minor Changes
11
+
12
+ - ec50a510: Switch `virtualKeypadVersion` on `perseus:expression-evaluated` event to be non-optional
13
+ - 673f61b3: Introduce `dependencies` on ArticleRenderer, ItemRenderer, MultiRenderer, and ServerItemRenderer.
14
+
15
+ ### Patch Changes
16
+
17
+ - 2c69b0dc: Fix APIOptions `trackInteraction` type for better Flow type generation
18
+ - Updated dependencies [dd800c22]
19
+ - Updated dependencies [5352d512]
20
+ - Updated dependencies [673f61b3]
21
+ - @khanacademy/math-input@10.0.0
22
+ - @khanacademy/perseus-core@0.2.0
23
+
3
24
  ## 8.0.0
4
25
 
5
26
  ### Major Changes
@@ -5,7 +5,7 @@
5
5
  import * as React from "react";
6
6
  import type { KeypadProps } from "./mixins/provide-keypad";
7
7
  import type { PerseusRenderer } from "./perseus-types";
8
- import type { APIOptions } from "./types";
8
+ import type { APIOptions, PerseusDependenciesV2 } from "./types";
9
9
  import type { LinterContextProps } from "@khanacademy/perseus-linter";
10
10
  type Props = {
11
11
  apiOptions: APIOptions;
@@ -16,6 +16,7 @@ type Props = {
16
16
  useNewStyles: boolean;
17
17
  linterContext: LinterContextProps;
18
18
  legacyPerseusLint?: ReadonlyArray<string>;
19
+ dependencies: PerseusDependenciesV2;
19
20
  } & KeypadProps;
20
21
  type DefaultProps = {
21
22
  apiOptions: Props["apiOptions"];
@@ -1,3 +1,9 @@
1
- import type { PerseusDependencies } from "./types";
1
+ import * as React from "react";
2
+ import type { PerseusDependencies, PerseusDependenciesV2 } from "./types";
2
3
  export declare const setDependencies: (dependencies: PerseusDependencies) => void;
3
4
  export declare const getDependencies: () => PerseusDependencies;
5
+ export declare const DependenciesContext: React.Context<PerseusDependenciesV2>;
6
+ /**
7
+ * useDependencies provides easy access to the current Perseus dependencies.
8
+ */
9
+ export declare const useDependencies: () => PerseusDependenciesV2;
package/dist/es/index.js CHANGED
@@ -113,11 +113,26 @@ const getDependencies = () => {
113
113
  }
114
114
  throw new Error(["Perseus has not been provided required dependencies.", "setDependencies(dependencies) must be called first.", "Make sure Perseus is being imported from javascript/perseus/perseus.js."].join("\n"));
115
115
  };
116
+ const DependenciesContext = /*#__PURE__*/React.createContext({
117
+ analytics: {
118
+ onAnalyticsEvent: async () => {}
119
+ }
120
+ });
121
+
122
+ /**
123
+ * useDependencies provides easy access to the current Perseus dependencies.
124
+ */
125
+ const useDependencies = () => {
126
+ const deps = React.useContext(DependenciesContext);
127
+ return deps;
128
+ };
116
129
 
117
130
  var dependencies = /*#__PURE__*/Object.freeze({
118
131
  __proto__: null,
119
132
  setDependencies: setDependencies,
120
- getDependencies: getDependencies
133
+ getDependencies: getDependencies,
134
+ DependenciesContext: DependenciesContext,
135
+ useDependencies: useDependencies
121
136
  });
122
137
 
123
138
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -2430,14 +2445,6 @@ const KhanAnswerTypes = {
2430
2445
  }
2431
2446
  };
2432
2447
 
2433
- const sendExpressionEvaluatedEvent = result => {
2434
- getDependencies().analytics({
2435
- type: "perseus:expression-evaluated",
2436
- payload: {
2437
- result
2438
- }
2439
- });
2440
- };
2441
2448
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
2442
2449
  const insertBraces = value => {
2443
2450
  // HACK(alex): Make sure that all LaTeX super/subscripts are wrapped
@@ -2460,6 +2467,13 @@ const insertBraces = value => {
2460
2467
  // TODO(alex): Properly hack MathQuill to always use explicit braces.
2461
2468
  return value.replace(/([_^])([^{])/g, "$1{$2}");
2462
2469
  };
2470
+ const deriveKeypadVersion = apiOptions => {
2471
+ // We can derive which version of the keypad is in use. This is
2472
+ // a bit tricky, but this code will be relatively short-lived
2473
+ // as we coalesce onto the new, v2 Keypad, at which point we
2474
+ // can remove this `virtualKeypadVersion` field entirely.
2475
+ return apiOptions.nativeKeypadProxy != null ? "REACT_NATIVE_KEYPAD" : apiOptions.customKeypad === true ? apiOptions.useV2Keypad === true ? "MATH_INPUT_KEYPAD_V2" : "MATH_INPUT_KEYPAD_V1" : "PERSEUS_MATH_INPUT";
2476
+ };
2463
2477
  // The new, MathQuill input expression widget
2464
2478
  class Expression extends React.Component {
2465
2479
  constructor(..._args) {
@@ -2514,7 +2528,9 @@ class Expression extends React.Component {
2514
2528
  };
2515
2529
  this.simpleValidate = (rubric, onInputError) => {
2516
2530
  onInputError = onInputError || function () {};
2517
- return Expression.validate(this.getUserInput(), rubric, onInputError);
2531
+ const result = Expression.validate(this.getUserInput(), rubric, onInputError);
2532
+ this.sendExpressionEvaluatedEvent(result.type === "invalid" ? "invalid" : result.earned === result.total ? "correct" : "incorrect");
2533
+ return result;
2518
2534
  };
2519
2535
  this.getUserInput = () => {
2520
2536
  return Expression.getUserInputFromProps(this.props);
@@ -2670,8 +2686,6 @@ class Expression extends React.Component {
2670
2686
  // we did, whether it's considered correct, incorrect, or ungraded
2671
2687
  if (!matchingAnswerForm) {
2672
2688
  if (firstUngradedResult) {
2673
- sendExpressionEvaluatedEvent("invalid");
2674
-
2675
2689
  // While we didn't directly match with any answer form, we
2676
2690
  // did at some point get an "ungraded" validation result,
2677
2691
  // which might indicate e.g. a mismatch in variable casing.
@@ -2685,8 +2699,6 @@ class Expression extends React.Component {
2685
2699
  };
2686
2700
  }
2687
2701
  if (allEmpty) {
2688
- sendExpressionEvaluatedEvent("invalid");
2689
-
2690
2702
  // If everything graded as empty, it's invalid.
2691
2703
  return {
2692
2704
  type: "invalid",
@@ -2695,7 +2707,6 @@ class Expression extends React.Component {
2695
2707
  }
2696
2708
  // We fell through all the possibilities and we're not empty,
2697
2709
  // so the answer is considered incorrect.
2698
- sendExpressionEvaluatedEvent("incorrect");
2699
2710
  return {
2700
2711
  type: "points",
2701
2712
  earned: 0,
@@ -2708,7 +2719,6 @@ class Expression extends React.Component {
2708
2719
  const apiResult = onInputError(null,
2709
2720
  // Reserved for some widget identifier
2710
2721
  userInput, matchMessage);
2711
- sendExpressionEvaluatedEvent("invalid");
2712
2722
  return {
2713
2723
  type: "invalid",
2714
2724
  message: apiResult === false ? null : matchMessage
@@ -2720,7 +2730,6 @@ class Expression extends React.Component {
2720
2730
  // TODO(eater): Seems silly to translate result to this
2721
2731
  // invalid/points thing and immediately translate it back in
2722
2732
  // ItemRenderer.scoreInput()
2723
- sendExpressionEvaluatedEvent(matchingAnswerForm.considered === "correct" ? "correct" : "incorrect");
2724
2733
  return {
2725
2734
  type: "points",
2726
2735
  earned: matchingAnswerForm.considered === "correct" ? 1 : 0,
@@ -2746,6 +2755,15 @@ class Expression extends React.Component {
2746
2755
  // @ts-expect-error - TS2339 - Property 'insert' does not exist on type 'ReactInstance'.
2747
2756
  this.refs.input.insert(keyPressed);
2748
2757
  }
2758
+ sendExpressionEvaluatedEvent(result) {
2759
+ this.props.analytics.onAnalyticsEvent({
2760
+ type: "perseus:expression-evaluated",
2761
+ payload: {
2762
+ result,
2763
+ virtualKeypadVersion: deriveKeypadVersion(this.props.apiOptions)
2764
+ }
2765
+ });
2766
+ }
2749
2767
  render() {
2750
2768
  if (this.props.apiOptions.customKeypad) {
2751
2769
  return /*#__PURE__*/React.createElement(KeypadInput
@@ -2906,11 +2924,18 @@ const propUpgrades$2 = {
2906
2924
  }]
2907
2925
  })
2908
2926
  };
2927
+ const ExpressionWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) => {
2928
+ const deps = useDependencies();
2929
+ return /*#__PURE__*/React.createElement(Expression, _extends({
2930
+ ref: ref,
2931
+ analytics: deps.analytics
2932
+ }, props));
2933
+ });
2909
2934
  var Expression$1 = {
2910
2935
  name: "expression",
2911
2936
  displayName: "Expression / Equation",
2912
2937
  defaultAlignment: "inline-block",
2913
- widget: Expression,
2938
+ widget: ExpressionWithDependencies,
2914
2939
  transform: widgetOptions => {
2915
2940
  const {
2916
2941
  times,
@@ -44033,7 +44058,9 @@ class ArticleRenderer extends React.Component {
44033
44058
  // TODO(alex): Add mobile api functions and pass them down here
44034
44059
  const sections = this._sections().map((section, i) => {
44035
44060
  const refForSection = `section-${i}`;
44036
- return /*#__PURE__*/React.createElement("div", {
44061
+ return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
44062
+ value: this.props.dependencies
44063
+ }, /*#__PURE__*/React.createElement("div", {
44037
44064
  key: i,
44038
44065
  className: "clearfix"
44039
44066
  }, /*#__PURE__*/React.createElement(Renderer, _extends({}, section, {
@@ -44051,7 +44078,7 @@ class ArticleRenderer extends React.Component {
44051
44078
  }),
44052
44079
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "article"),
44053
44080
  legacyPerseusLint: this.props.legacyPerseusLint
44054
- })));
44081
+ }))));
44055
44082
  });
44056
44083
  return /*#__PURE__*/React.createElement("div", {
44057
44084
  className: classes
@@ -44508,7 +44535,9 @@ class ItemRenderer extends React.Component {
44508
44535
  // that have completely different places in the DOM, we have to do this
44509
44536
  // strangeness instead of relying on React's normal render() method.
44510
44537
  // TODO(alpert): Figure out how to clean this up somehow
44511
- render( /*#__PURE__*/React.createElement(Renderer, _extends({
44538
+ render( /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
44539
+ value: this.props.dependencies
44540
+ }, /*#__PURE__*/React.createElement(Renderer, _extends({
44512
44541
  ref: node => {
44513
44542
  if (!node) {
44514
44543
  return;
@@ -44536,10 +44565,12 @@ class ItemRenderer extends React.Component {
44536
44565
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "question")
44537
44566
  }, this.props.item.question, {
44538
44567
  legacyPerseusLint: this.props.legacyPerseusLint
44539
- })),
44568
+ }))),
44540
44569
  // @ts-expect-error - TS2345 - Argument of type 'Element' is not assignable to parameter of type 'HTMLElement'.
44541
44570
  workArea);
44542
- render( /*#__PURE__*/React.createElement(HintsRenderer, {
44571
+ render( /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
44572
+ value: this.props.dependencies
44573
+ }, /*#__PURE__*/React.createElement(HintsRenderer, {
44543
44574
  ref: node => this.hintsRenderer = node,
44544
44575
  hints: this.props.item.hints,
44545
44576
  hintsVisible: this.state.hintsVisible
@@ -44547,7 +44578,7 @@ class ItemRenderer extends React.Component {
44547
44578
  ,
44548
44579
  apiOptions: apiOptions,
44549
44580
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "hints")
44550
- }),
44581
+ })),
44551
44582
  // @ts-expect-error - TS2345 - Argument of type 'Element' is not assignable to parameter of type 'HTMLElement'.
44552
44583
  hintsArea);
44553
44584
  const answerArea = this.props.item.answerArea || {};
@@ -44764,7 +44795,9 @@ class ItemRenderer extends React.Component {
44764
44795
  this.questionRenderer.deselectIncorrectSelectedChoices();
44765
44796
  }
44766
44797
  render() {
44767
- return /*#__PURE__*/React.createElement("div", null);
44798
+ return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
44799
+ value: this.props.dependencies
44800
+ }, /*#__PURE__*/React.createElement("div", null));
44768
44801
  }
44769
44802
  }
44770
44803
  ItemRenderer.defaultProps = {
@@ -45084,14 +45117,16 @@ class ServerItemRenderer extends React.Component {
45084
45117
  apiOptions: apiOptions,
45085
45118
  ref: elem => this.hintsRenderer = elem
45086
45119
  });
45087
- return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", null, questionRenderer), /*#__PURE__*/React.createElement("div", {
45120
+ return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
45121
+ value: this.props.dependencies
45122
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", null, questionRenderer), /*#__PURE__*/React.createElement("div", {
45088
45123
  className:
45089
45124
  // Avoid adding any horizontal padding when applying the
45090
45125
  // mobile hint styles, which are flush to the left.
45091
45126
  // NOTE(charlie): We may still want to apply this
45092
45127
  // padding for desktop exercises.
45093
45128
  apiOptions.isMobile ? undefined : css(styles$3.hintsContainer)
45094
- }, hintsRenderer));
45129
+ }, hintsRenderer)));
45095
45130
  }
45096
45131
  }
45097
45132
  ServerItemRenderer.defaultProps = {
@@ -46492,9 +46527,11 @@ class MultiRenderer extends React.Component {
46492
46527
 
46493
46528
  // Pass the renderer tree to the `children` function, which will
46494
46529
  // determine the actual content of this component.
46495
- return this.props.children({
46530
+ return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
46531
+ value: this.props.dependencies
46532
+ }, this.props.children({
46496
46533
  renderers: this._getRenderers()
46497
- });
46534
+ }));
46498
46535
  }
46499
46536
  }
46500
46537
  const styles = StyleSheet.create({