@khanacademy/perseus 13.2.0 → 14.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/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": "13.2.0",
6
+ "version": "14.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.5",
26
26
  "@khanacademy/kmath": "^0.1.6",
27
- "@khanacademy/math-input": "^15.1.0",
27
+ "@khanacademy/math-input": "^16.0.0",
28
28
  "@khanacademy/perseus-core": "1.3.0",
29
29
  "@khanacademy/perseus-linter": "^0.3.8",
30
30
  "@khanacademy/pure-markdown": "^0.2.11",
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  StatefulKeypadContextProvider,
3
3
  KeypadContext,
4
+ MobileKeypad,
4
5
  } from "@khanacademy/math-input";
5
6
  import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
6
7
  import {screen, render, fireEvent, waitFor} from "@testing-library/react";
@@ -11,7 +12,6 @@ import {
11
12
  testDependencies,
12
13
  testDependenciesV2,
13
14
  } from "../../../../testing/test-dependencies";
14
- import KeypadSwitch from "../../../math-input/src/components/keypad-switch";
15
15
  import {articleWithExpression} from "../__testdata__/article-renderer.testdata";
16
16
  import ArticleRenderer from "../article-renderer";
17
17
  import * as Dependencies from "../dependencies";
@@ -24,11 +24,10 @@ function KeypadWithContext() {
24
24
  <KeypadContext.Consumer>
25
25
  {({setKeypadElement}) => {
26
26
  return (
27
- <KeypadSwitch
27
+ <MobileKeypad
28
28
  onElementMounted={setKeypadElement}
29
29
  onDismiss={() => {}}
30
30
  onAnalyticsEvent={async () => {}}
31
- useV2Keypad
32
31
  />
33
32
  );
34
33
  }}
@@ -5,6 +5,7 @@ import * as React from "react";
5
5
  import ReactDOM from "react-dom";
6
6
  import _ from "underscore";
7
7
 
8
+ import AssetContext from "../asset-context";
8
9
  import {interactiveSizes} from "../styles/constants";
9
10
  import Util from "../util";
10
11
  import GraphUtils from "../util/graph-utils";
@@ -395,14 +396,19 @@ class Graph extends React.Component<Props> {
395
396
  if (imageData.url) {
396
397
  const scale = this.props.box[0] / interactiveSizes.defaultBoxSize;
397
398
  image = (
398
- // @ts-expect-error - TS2741 - Property 'alt' is missing in type '{ src: any; width: any; height: any; scale: number; responsive: false; }' but required in type 'Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | "height" | "width" | "title" | "alt" | "trackInteraction" | "preloader" | "allowFullBleed" | "extraGraphie" | "overrideAriaHidden">'.
399
- <SvgImage
400
- src={imageData.url}
401
- width={imageData.width}
402
- height={imageData.height}
403
- scale={scale}
404
- responsive={false}
405
- />
399
+ <AssetContext.Consumer>
400
+ {({setAssetStatus}) => (
401
+ <SvgImage
402
+ // @ts-expect-error - TS2741 - Property 'alt' is missing in type '{ src: any; width: any; height: any; scale: number; responsive: false; }' but required in type 'Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | "height" | "width" | "title" | "alt" | "trackInteraction" | "preloader" | "allowFullBleed" | "extraGraphie" | "overrideAriaHidden">'.
403
+ src={imageData.url}
404
+ width={imageData.width}
405
+ height={imageData.height}
406
+ scale={scale}
407
+ responsive={false}
408
+ setAssetStatus={setAssetStatus}
409
+ />
410
+ )}
411
+ </AssetContext.Consumer>
406
412
  );
407
413
  } else {
408
414
  image = null;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
2
- /* eslint-disable no-useless-escape, react/no-unsafe */
2
+ /* eslint-disable react/no-unsafe */
3
3
  import {CircularSpinner} from "@khanacademy/wonder-blocks-progress-spinner";
4
4
  import classNames from "classnames";
5
5
  import $ from "jquery";
@@ -79,7 +79,7 @@ const doJSONP = function (url: string, options) {
79
79
  // system (with file://). We replace urls that start with `web+graphie`
80
80
  // in the perseus json with this `file+graphie` prefix to indicate that
81
81
  // they should have the `file://` protocol instead of `https://`.
82
- const svgLocalLabelsRegex = /^file\+graphie\:/;
82
+ const svgLocalLabelsRegex = /^file\+graphie:/;
83
83
  const hashRegex = /\/([^/]+)$/;
84
84
 
85
85
  function isImageProbablyPhotograph(imageUrl) {
@@ -180,6 +180,16 @@ type Props = {
180
180
  setAssetStatus: (assetKey: string, loaded: boolean) => void;
181
181
  };
182
182
 
183
+ type DefaultProps = {
184
+ constrainHeight: NonNullable<Props["constrainHeight"]>;
185
+ onUpdate: NonNullable<Props["onUpdate"]>;
186
+ responsive: NonNullable<Props["responsive"]>;
187
+ scale: NonNullable<Props["scale"]>;
188
+ setAssetStatus: NonNullable<Props["setAssetStatus"]>;
189
+ src: NonNullable<Props["src"]>;
190
+ zoomToFullSizeOnMobile: NonNullable<Props["zoomToFullSizeOnMobile"]>;
191
+ };
192
+
183
193
  type Label = {
184
194
  coordinates: ReadonlyArray<any>;
185
195
  content: string;
@@ -212,15 +222,7 @@ type State = {
212
222
  class SvgImage extends React.Component<Props, State> {
213
223
  _isMounted: boolean;
214
224
 
215
- static defaultProps: {
216
- constrainHeight: boolean;
217
- onUpdate: () => void;
218
- responsive: boolean;
219
- scale: number;
220
- setAssetStatus: (src: string, status: boolean) => void;
221
- src: string;
222
- zoomToFullSizeOnMobile: boolean;
223
- } = {
225
+ static defaultProps: DefaultProps = {
224
226
  constrainHeight: false,
225
227
  onUpdate: () => {},
226
228
  responsive: true,
@@ -102,7 +102,6 @@ const ProvideKeypad = {
102
102
  }}
103
103
  // @ts-expect-error - TS2339 - Property 'props' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable<any>; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'.
104
104
  style={_this.props.keypadStyle}
105
- useV2Keypad={apiOptions.useV2Keypad}
106
105
  onAnalyticsEvent={async () => {
107
106
  // Intentionally left empty. This component is
108
107
  // deprecated and so we don't want/need to instrument
@@ -534,7 +534,7 @@ export type PerseusInteractiveGraphWidgetOptions = {
534
534
  showTooltips?: boolean;
535
535
  // The unit to show on the ruler. e.g. "mm", "cm", "m", "km", "in", "ft", "yd", "mi"
536
536
  rulerLabel: string;
537
- // How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16
537
+ // How many ticks to show on the ruler. e.g. 1, 2, 4, 8, 10, 16. Must be an integer.
538
538
  rulerTicks: number;
539
539
  // The X and Y coordinate ranges for the view of the graph. default: [[-10, 10], [-10, 10]]
540
540
  // NOTE(kevinb): perseus_data.go defines this as Array<Array<number>>
@@ -577,6 +577,7 @@ export type PerseusGraphTypeAngle = {
577
577
  snapDegrees?: number;
578
578
  // How to match the answer. If missing, defaults to exact matching.
579
579
  match?: "congruent";
580
+ // must have 3 coords - ie [Coord, Coord, Coord]
580
581
  coords?: ReadonlyArray<Coord>;
581
582
  };
582
583
 
@@ -588,11 +589,13 @@ export type PerseusGraphTypeCircle = {
588
589
 
589
590
  export type PerseusGraphTypeLinear = {
590
591
  type: "linear";
592
+ // expects 2 coords
591
593
  coords?: ReadonlyArray<Coord>;
592
594
  } & PerseusGraphTypeCommon;
593
595
 
594
596
  export type PerseusGraphTypeLinearSystem = {
595
597
  type: "linear-system";
598
+ // expects 2 sets of 2 coords
596
599
  coords?: ReadonlyArray<ReadonlyArray<Coord>>;
597
600
  } & PerseusGraphTypeCommon;
598
601
 
@@ -620,6 +623,7 @@ export type PerseusGraphTypePolygon = {
620
623
 
621
624
  export type PerseusGraphTypeQuadratic = {
622
625
  type: "quadratic";
626
+ // expects a list of 3 coords
623
627
  coords?: ReadonlyArray<Coord>;
624
628
  } & PerseusGraphTypeCommon;
625
629
 
@@ -627,18 +631,19 @@ export type PerseusGraphTypeSegment = {
627
631
  type: "segment";
628
632
  // The number of segments if a "segment" type. default: 1. Max: 6
629
633
  numSegments?: number;
630
- // A list of segments (each segment is a list of coordinates).
631
- // Length should match the `numSegments` value.
634
+ // Expects a list of Coord tuples. Length should match the `numSegments` value.
632
635
  coords?: ReadonlyArray<ReadonlyArray<Coord>>;
633
636
  } & PerseusGraphTypeCommon;
634
637
 
635
638
  export type PerseusGraphTypeSinusoid = {
636
639
  type: "sinusoid";
640
+ // Expects a list of 2 Coords
637
641
  coords?: ReadonlyArray<Coord>;
638
642
  } & PerseusGraphTypeCommon;
639
643
 
640
644
  export type PerseusGraphTypeRay = {
641
645
  type: "ray";
646
+ // Expects a list of 2 Coords
642
647
  coords?: ReadonlyArray<Coord>;
643
648
  } & PerseusGraphTypeCommon;
644
649
 
@@ -0,0 +1,59 @@
1
+ import Button from "@khanacademy/wonder-blocks-button";
2
+ import * as React from "react";
3
+
4
+ import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
5
+ import {randomCategorizerGenerator} from "../__testdata__/categorizer.testdata";
6
+ import {randomDropdownGenerator} from "../__testdata__/dropdown.testdata";
7
+ import {randomExplanationGenerator} from "../__testdata__/explanation.testdata";
8
+ import {randomExpressionGenerator} from "../__testdata__/expression.testdata";
9
+ import {randomInteractiveGraphGenerator} from "../__testdata__/interactive-graph-random.testdata";
10
+ import {randomRadioGenerator} from "../__testdata__/radio.testdata";
11
+
12
+ import type {PerseusRenderer} from "@khanacademy/perseus";
13
+
14
+ export default {
15
+ title: "Perseus/Randomized Widgets",
16
+ };
17
+
18
+ const RandomizedWidgetContainer = (
19
+ randomizer: () => PerseusRenderer,
20
+ ): React.ReactElement => {
21
+ const [question, setQuestion] = React.useState(randomizer());
22
+ const randomize = () => {
23
+ setQuestion(randomizer());
24
+ };
25
+
26
+ return (
27
+ <div>
28
+ <Button size="small" style={{marginBottom: 24}} onClick={randomize}>
29
+ Randomize
30
+ </Button>
31
+
32
+ <RendererWithDebugUI question={question} />
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export const RandomCategorizer = (): React.ReactElement => {
38
+ return RandomizedWidgetContainer(randomCategorizerGenerator);
39
+ };
40
+
41
+ export const RandomDropdown = (): React.ReactElement => {
42
+ return RandomizedWidgetContainer(randomDropdownGenerator);
43
+ };
44
+
45
+ export const RandomExplanation = (): React.ReactElement => {
46
+ return RandomizedWidgetContainer(randomExplanationGenerator);
47
+ };
48
+
49
+ export const RandomExpression = (): React.ReactElement => {
50
+ return RandomizedWidgetContainer(randomExpressionGenerator);
51
+ };
52
+
53
+ export const RandomInteractiveGraph = (): React.ReactElement => {
54
+ return RandomizedWidgetContainer(randomInteractiveGraphGenerator);
55
+ };
56
+
57
+ export const RandomRadio = (): React.ReactElement => {
58
+ return RandomizedWidgetContainer(randomRadioGenerator);
59
+ };
@@ -17,7 +17,6 @@ const Footer = (): React.ReactElement => {
17
17
  onElementMounted={setKeypadElement}
18
18
  onDismiss={() => renderer && renderer.blur()}
19
19
  style={styles.keypad}
20
- useV2Keypad={true}
21
20
  onAnalyticsEvent={async (e) => {
22
21
  action("onAnalyticsEvent")(e);
23
22
  }}
@@ -1,3 +1,10 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomInteger,
5
+ randomSentence,
6
+ } from "./randomizers";
7
+
1
8
  import type {PerseusRenderer} from "../../perseus-types";
2
9
 
3
10
  export const question1: PerseusRenderer = {
@@ -47,3 +54,36 @@ export const question1: PerseusRenderer = {
47
54
  },
48
55
  },
49
56
  };
57
+
58
+ export const randomCategorizerGenerator = (): PerseusRenderer => {
59
+ const questionText = randomSentence(35);
60
+ const numItems = randomInteger(2, 7);
61
+ const numCategories = randomInteger(2, 6);
62
+
63
+ return {
64
+ content: `${questionText}\n\n\n[[\u2603 categorizer 1]]`,
65
+ images: {},
66
+ widgets: {
67
+ "categorizer 1": {
68
+ version: {major: 0, minor: 0},
69
+ type: "categorizer",
70
+ graded: randomBoolean(),
71
+ alignment: "default",
72
+ options: {
73
+ items: arrayOfLength(numItems).map(() =>
74
+ randomSentence(12),
75
+ ),
76
+ values: arrayOfLength(numItems).map(() =>
77
+ randomInteger(0, numCategories - 1),
78
+ ),
79
+ randomizeItems: randomBoolean(),
80
+ categories: arrayOfLength(numCategories).map(() =>
81
+ randomSentence(7),
82
+ ),
83
+ highlightLint: randomBoolean(),
84
+ static: randomBoolean(0.05),
85
+ },
86
+ },
87
+ },
88
+ };
89
+ };
@@ -1,3 +1,10 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomInteger,
5
+ randomSentence,
6
+ } from "./randomizers";
7
+
1
8
  import type {PerseusRenderer} from "../../perseus-types";
2
9
 
3
10
  export const question1: PerseusRenderer = {
@@ -31,3 +38,34 @@ export const question1: PerseusRenderer = {
31
38
  },
32
39
  },
33
40
  };
41
+
42
+ export const randomDropdownGenerator = (): PerseusRenderer => {
43
+ const numChoices = randomInteger(2, 16);
44
+ const correctIndex = randomInteger(0, numChoices - 1);
45
+ return {
46
+ content: `${randomSentence(20)} [[☃ dropdown 1]] ${randomSentence(10)}`,
47
+ images: {},
48
+ widgets: {
49
+ "dropdown 1": {
50
+ type: "dropdown",
51
+ alignment: "default",
52
+ static: randomBoolean(0.05),
53
+ graded: randomBoolean(),
54
+ options: {
55
+ static: randomBoolean(0.05),
56
+ placeholder: randomSentence(10),
57
+ choices: arrayOfLength(numChoices).map((_, i) => {
58
+ return {
59
+ content: randomSentence(10),
60
+ correct: i === correctIndex,
61
+ };
62
+ }),
63
+ },
64
+ version: {
65
+ major: 0,
66
+ minor: 0,
67
+ },
68
+ },
69
+ },
70
+ };
71
+ };
@@ -1,3 +1,5 @@
1
+ import {randomBoolean, randomSentence} from "./randomizers";
2
+
1
3
  import type {PerseusRenderer} from "../../perseus-types";
2
4
 
3
5
  export const question1: PerseusRenderer = {
@@ -53,3 +55,31 @@ export const question2: PerseusRenderer = {
53
55
  },
54
56
  },
55
57
  };
58
+
59
+ export const randomExplanationGenerator = (): PerseusRenderer => {
60
+ return {
61
+ content: `${randomSentence(
62
+ 50,
63
+ )}\n[[\u2603 explanation 1]]\n${randomSentence(50)}`,
64
+ images: {},
65
+ widgets: {
66
+ "explanation 1": {
67
+ graded: randomBoolean(),
68
+ version: {
69
+ major: 0,
70
+ minor: 0,
71
+ },
72
+ static: randomBoolean(0.05),
73
+ type: "explanation",
74
+ options: {
75
+ hidePrompt: `${randomSentence(20)}`,
76
+ widgets: {},
77
+ explanation: `${randomSentence(50)}`,
78
+ static: randomBoolean(0.05),
79
+ showPrompt: `${randomSentence(7)}`,
80
+ },
81
+ alignment: "default",
82
+ },
83
+ },
84
+ };
85
+ };
@@ -1,7 +1,19 @@
1
+ import {
2
+ arrayOfLength,
3
+ randomBoolean,
4
+ randomElement,
5
+ randomInteger,
6
+ randomLetter,
7
+ randomSentence,
8
+ randomWord,
9
+ } from "./randomizers";
10
+
1
11
  import type {
2
12
  PerseusExpressionWidgetOptions,
3
13
  Version,
4
14
  PerseusItem,
15
+ PerseusRenderer,
16
+ ExpressionWidget,
5
17
  } from "../../perseus-types";
6
18
 
7
19
  const createItemJson = (
@@ -96,3 +108,73 @@ export const expressionItem3: PerseusItem = createItemJson(
96
108
  minor: 0,
97
109
  },
98
110
  );
111
+
112
+ export const randomExpressionGenerator = (): PerseusRenderer => {
113
+ const randomButtonSet = [
114
+ "basic",
115
+ "basic+div",
116
+ "trig",
117
+ "prealgebra",
118
+ "logarithms",
119
+ "basic relations",
120
+ "advanced relations",
121
+ ]
122
+ .sort(() => {
123
+ return randomBoolean() ? 1 : -1;
124
+ })
125
+ .slice(0, randomInteger(1, 6));
126
+
127
+ const randomFunctionSet = arrayOfLength(randomInteger(0, 6)).map(
128
+ randomLetter,
129
+ );
130
+
131
+ return {
132
+ content: `${randomSentence(20)} [[☃ expression 1]]`,
133
+ images: {},
134
+ widgets: {
135
+ "expression 1": {
136
+ type: "expression",
137
+ graded: randomBoolean(),
138
+ version: {
139
+ major: 1,
140
+ minor: 0,
141
+ },
142
+ static: randomBoolean(0.05),
143
+ options: {
144
+ answerForms: [
145
+ {
146
+ considered: randomElement([
147
+ "correct",
148
+ "wrong",
149
+ "ungraded",
150
+ ]),
151
+ form: randomBoolean(),
152
+ simplify: randomBoolean(),
153
+ value: randomWord(),
154
+ },
155
+ {
156
+ considered: randomElement([
157
+ "correct",
158
+ "wrong",
159
+ "ungraded",
160
+ ]),
161
+ form: randomBoolean(),
162
+ simplify: randomBoolean(),
163
+ value: randomWord(),
164
+ },
165
+ ],
166
+ times: randomBoolean(),
167
+ buttonSets: randomButtonSet,
168
+ functions: randomFunctionSet,
169
+ buttonsVisible: randomElement([
170
+ "always",
171
+ "never",
172
+ "focused",
173
+ undefined,
174
+ ]),
175
+ alignment: "default",
176
+ },
177
+ } as ExpressionWidget,
178
+ },
179
+ };
180
+ };