@khanacademy/perseus 17.2.0 → 17.3.1

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.
@@ -30,14 +30,8 @@ export type PerseusImageDetail = {
30
30
  width: number;
31
31
  height: number;
32
32
  };
33
- export type PerseusAnswerArea = {
34
- zTable: boolean;
35
- chi2Table: boolean;
36
- tTable: boolean;
37
- calculator: boolean;
38
- periodicTable: boolean;
39
- periodicTableWithKey?: boolean;
40
- };
33
+ export declare const ItemExtras: readonly ["calculator", "chi2Table", "financialCalculatorMonthlyPayment", "financialCalculatorTotalAmount", "financialCalculatorTimeToPayOff", "periodicTable", "periodicTableWithKey", "tTable", "zTable"];
34
+ export type PerseusAnswerArea = Record<typeof ItemExtras[number], boolean>;
41
35
  type Widget<Type extends string, Options> = {
42
36
  type: Type;
43
37
  static?: boolean;
@@ -4,6 +4,7 @@ type Props = {
4
4
  translationIndex: number;
5
5
  paragraphIndex?: number;
6
6
  children?: React.ReactNode;
7
+ inline?: boolean;
7
8
  };
8
9
  declare class QuestionParagraph extends React.Component<Props> {
9
10
  render(): React.ReactNode;
@@ -59,6 +59,7 @@ type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
59
59
  linterContext: LinterContextProps;
60
60
  legacyPerseusLint?: ReadonlyArray<string>;
61
61
  widgets: PerseusRenderer["widgets"];
62
+ inline?: boolean;
62
63
  };
63
64
  type State = {
64
65
  translationLintErrors: ReadonlyArray<string>;
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": "17.2.0",
6
+ "version": "17.3.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.7",
26
26
  "@khanacademy/kmath": "^0.1.8",
27
- "@khanacademy/math-input": "^16.3.0",
27
+ "@khanacademy/math-input": "^16.4.0",
28
28
  "@khanacademy/perseus-core": "1.4.1",
29
29
  "@khanacademy/perseus-linter": "^0.3.10",
30
30
  "@khanacademy/pure-markdown": "^0.2.13",
@@ -1,13 +1,13 @@
1
- import type {PerseusItem} from "../perseus-types";
1
+ import {
2
+ ItemExtras,
3
+ type PerseusAnswerArea,
4
+ type PerseusItem,
5
+ } from "../perseus-types";
2
6
 
3
7
  export const itemWithPieChart: PerseusItem = {
4
- answerArea: {
5
- calculator: false,
6
- chi2Table: false,
7
- periodicTable: false,
8
- tTable: false,
9
- zTable: false,
10
- },
8
+ answerArea: Object.fromEntries(
9
+ ItemExtras.map((extra) => [extra, false]),
10
+ ) as PerseusAnswerArea,
11
11
  hints: [],
12
12
  itemDataVersion: {
13
13
  major: 0,
@@ -1,8 +1,10 @@
1
- import type {
2
- InputNumberWidget,
3
- LabelImageWidget,
4
- PerseusItem,
5
- PerseusRenderer,
1
+ import {
2
+ ItemExtras,
3
+ type InputNumberWidget,
4
+ type LabelImageWidget,
5
+ type PerseusItem,
6
+ type PerseusRenderer,
7
+ type PerseusAnswerArea,
6
8
  } from "../perseus-types";
7
9
 
8
10
  export const itemWithInput: PerseusItem = {
@@ -37,13 +39,9 @@ export const itemWithInput: PerseusItem = {
37
39
  };
38
40
 
39
41
  export const labelImageItem: PerseusItem = {
40
- answerArea: {
41
- calculator: false,
42
- chi2Table: false,
43
- periodicTable: false,
44
- tTable: false,
45
- zTable: false,
46
- },
42
+ answerArea: Object.fromEntries(
43
+ ItemExtras.map((extra) => [extra, false]),
44
+ ) as PerseusAnswerArea,
47
45
  _multi: null,
48
46
  answer: null,
49
47
  hints: [],
@@ -1,8 +1,9 @@
1
1
  import * as React from "react";
2
2
 
3
3
  import AssetContext from "../asset-context";
4
+ import {ItemExtras} from "../perseus-types";
4
5
 
5
- import type {PerseusItem} from "../perseus-types";
6
+ import type {PerseusAnswerArea, PerseusItem} from "../perseus-types";
6
7
  import type {WidgetExports} from "../types";
7
8
 
8
9
  export const mockedAssetItem: PerseusItem = {
@@ -20,13 +21,9 @@ export const mockedAssetItem: PerseusItem = {
20
21
  },
21
22
  },
22
23
  },
23
- answerArea: {
24
- calculator: false,
25
- chi2Table: false,
26
- periodicTable: false,
27
- tTable: false,
28
- zTable: false,
29
- },
24
+ answerArea: Object.fromEntries(
25
+ ItemExtras.map((extra) => [extra, false]),
26
+ ) as PerseusAnswerArea,
30
27
  itemDataVersion: {major: 0, minor: 1},
31
28
  hints: [],
32
29
  _multi: null,
package/src/index.ts CHANGED
@@ -64,6 +64,7 @@ export {default as PerseusMarkdown} from "./perseus-markdown";
64
64
  export {
65
65
  PerseusExpressionAnswerFormConsidered,
66
66
  plotterPlotTypes,
67
+ ItemExtras,
67
68
  } from "./perseus-types";
68
69
  export {traverse} from "./traversal";
69
70
  export {isItemRenderableByVersion} from "./renderability";
@@ -127,6 +128,7 @@ export type {
127
128
  PerseusExpressionWidgetOptions,
128
129
  PerseusPlotterWidgetOptions,
129
130
  PerseusRenderer,
131
+ PerseusAnswerArea,
130
132
  } from "./perseus-types";
131
133
  export type {Coord} from "./interactive2/types";
132
134
  export type {MarkerType} from "./widgets/label-image/types";
@@ -62,20 +62,27 @@ export type PerseusImageDetail = {
62
62
  height: number;
63
63
  };
64
64
 
65
- export type PerseusAnswerArea = {
66
- // The user might benefit from using a statistics Z Table like https://www.ztable.net/
67
- zTable: boolean;
68
- // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
69
- chi2Table: boolean;
70
- // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
71
- tTable: boolean;
65
+ export const ItemExtras = [
72
66
  // The user might benefit from using a Scientific Calculator. Provided on Khan Academy when true
73
- calculator: boolean;
67
+ "calculator",
68
+ // The user might benefit from using a statistics Chi Squared Table like https://people.richland.edu/james/lecture/m170/tbl-chi.html
69
+ "chi2Table",
70
+ // The user might benefit from a monthly payments calculator. Provided on Khan Academy when true
71
+ "financialCalculatorMonthlyPayment",
72
+ // The user might benefit from a total amount calculator. Provided on Khan Academy when true
73
+ "financialCalculatorTotalAmount",
74
+ // The user might benefit from a time to pay off calculator. Provided on Khan Academy when true
75
+ "financialCalculatorTimeToPayOff",
74
76
  // The user might benefit from using a Periodic Table of Elements. Provided on Khan Academy when true
75
- periodicTable: boolean;
77
+ "periodicTable",
76
78
  // The user might benefit from using a Periodic Table of Elements with key. Provided on Khan Academy when true
77
- periodicTableWithKey?: boolean;
78
- };
79
+ "periodicTableWithKey",
80
+ // The user might benefit from using a statistics T Table like https://www.statisticshowto.com/tables/t-distribution-table/
81
+ "tTable",
82
+ // The user might benefit from using a statistics Z Table like https://www.ztable.net/
83
+ "zTable",
84
+ ] as const;
85
+ export type PerseusAnswerArea = Record<typeof ItemExtras[number], boolean>;
79
86
 
80
87
  type Widget<Type extends string, Options> = {
81
88
  // The "type" of widget which will define what the Options field looks like
@@ -5,6 +5,7 @@ type Props = {
5
5
  translationIndex: number;
6
6
  paragraphIndex?: number;
7
7
  children?: React.ReactNode;
8
+ inline?: boolean;
8
9
  };
9
10
 
10
11
  class QuestionParagraph extends React.Component<Props> {
@@ -16,7 +17,11 @@ class QuestionParagraph extends React.Component<Props> {
16
17
  // to attach some metadata to top-level QuestionParagraphs:
17
18
  return (
18
19
  <div
19
- className={className}
20
+ className={
21
+ this.props.inline
22
+ ? this.props.className ?? undefined
23
+ : className
24
+ }
20
25
  data-perseus-component-index={this.props.translationIndex}
21
26
  data-perseus-paragraph-index={this.props.paragraphIndex}
22
27
  >
package/src/renderer.tsx CHANGED
@@ -172,6 +172,8 @@ type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
172
172
  linterContext: LinterContextProps;
173
173
  legacyPerseusLint?: ReadonlyArray<string>;
174
174
  widgets: PerseusRenderer["widgets"];
175
+ // Skip adding paragraph class
176
+ inline?: boolean;
175
177
  };
176
178
 
177
179
  type State = {
@@ -1060,6 +1062,7 @@ class Renderer extends React.Component<Props, State> {
1060
1062
  className={className}
1061
1063
  translationIndex={this.translationIndex}
1062
1064
  paragraphIndex={state.paragraphIndex}
1065
+ inline={this.props.inline}
1063
1066
  >
1064
1067
  <ErrorBoundary>{output}</ErrorBoundary>
1065
1068
  </QuestionParagraph>
@@ -1919,13 +1922,17 @@ class Renderer extends React.Component<Props, State> {
1919
1922
  this._isTwoColumn = false;
1920
1923
 
1921
1924
  // Parse the string of markdown to a parse tree
1922
- const parsedMarkdown = PerseusMarkdown.parse(content, {
1923
- // Recognize crowdin IDs while translating articles
1924
- // (This should never be hit by exercises, though if you
1925
- // decide you want to add a check that this is an article,
1926
- // go for it.)
1927
- isJipt: this.translationIndex != null,
1928
- });
1925
+ const parsedMarkdown = this.props.inline
1926
+ ? PerseusMarkdown.parseInline(content, {
1927
+ // Recognize crowdin IDs while translating articles
1928
+ // (This should never be hit by exercises, though if you
1929
+ // decide you want to add a check that this is an article,
1930
+ // go for it.)
1931
+ isJipt: this.translationIndex != null,
1932
+ })
1933
+ : PerseusMarkdown.parse(content, {
1934
+ isJipt: this.translationIndex != null,
1935
+ });
1929
1936
 
1930
1937
  // Optionally apply the linter to the parse tree
1931
1938
  if (this.props.linterContext.highlightLint) {
@@ -5,35 +5,18 @@
5
5
  margin: 0;
6
6
  }
7
7
  }
8
- button[id*="perseus-label-image-widget-answer-pill"],
9
- div[id*="perseus-label-image-widget-answer-pill"] {
10
- // Reset text content.
11
- div.paragraph {
12
- all: revert;
13
- }
14
- // Allow for padding around math content. Accounting for the negative margin here:
15
- // https://github.com/Khan/perseus/blob/bafd35daee723456bf259a39b88e7638d91c8c3b/packages/perseus/src/renderer.tsx#L1176
16
- div.perseus-block-math-inner {
17
- margin: -5px 0 !important;
18
- }
19
- }
20
8
  }
21
9
 
22
10
  // Reset mobile text content for answer pills.
23
11
  @media (max-width: 767px) {
24
12
  [id*="perseus-label-image-widget-answer-pill"] {
25
- div.paragraph {
26
- all: revert;
27
- }
28
13
  > div.perseus-renderer-responsive {
29
14
  all: revert;
30
15
  }
31
16
  }
32
-
33
17
  .framework-perseus.perseus-mobile
34
18
  [id*="perseus-label-image-widget-answer-pill"] {
35
- .perseus-renderer > .paragraph,
36
- .perseus-renderer > .paragraph .paragraph {
19
+ mjx-container {
37
20
  all: revert;
38
21
  }
39
22
  }
@@ -5,6 +5,8 @@ import {
5
5
  textQuestion,
6
6
  mathQuestion,
7
7
  numberline,
8
+ longTextFromArticle,
9
+ mixedContentQuestion,
8
10
  } from "../__testdata__/label-image.testdata";
9
11
 
10
12
  import type {PerseusRenderer} from "../../perseus-types";
@@ -59,6 +61,21 @@ export const LabelWidgetWithText = (args: StoryArgs): React.ReactElement => {
59
61
  );
60
62
  };
61
63
 
64
+ export const LabelWidgetWithLongText = (
65
+ args: StoryArgs,
66
+ ): React.ReactElement => {
67
+ const apiOptions: APIOptions = {
68
+ isMobile: args.isMobile,
69
+ };
70
+
71
+ return (
72
+ <RendererWithDebugUI
73
+ question={applyStoryArgs(longTextFromArticle, args)}
74
+ apiOptions={apiOptions}
75
+ />
76
+ );
77
+ };
78
+
62
79
  export const LabelWidgetWithMath = (args: StoryArgs): React.ReactElement => {
63
80
  const apiOptions: APIOptions = {
64
81
  isMobile: args.isMobile,
@@ -85,6 +102,19 @@ export const LabelImageNumberline = (args: StoryArgs): React.ReactElement => {
85
102
  );
86
103
  };
87
104
 
105
+ export const LabelImageMixedContent = (args: StoryArgs): React.ReactElement => {
106
+ const apiOptions: APIOptions = {
107
+ isMobile: args.isMobile,
108
+ };
109
+
110
+ return (
111
+ <RendererWithDebugUI
112
+ question={applyStoryArgs(mixedContentQuestion, args)}
113
+ apiOptions={apiOptions}
114
+ />
115
+ );
116
+ };
117
+
88
118
  export default {
89
119
  title: "Perseus/Widgets/Label Image",
90
120
  args: {
@@ -1,3 +1,13 @@
1
+ import {
2
+ ItemExtras,
3
+ type PerseusExpressionWidgetOptions,
4
+ type Version,
5
+ type PerseusItem,
6
+ type PerseusRenderer,
7
+ type ExpressionWidget,
8
+ type PerseusAnswerArea,
9
+ } from "../../perseus-types";
10
+
1
11
  import {
2
12
  arrayOfLength,
3
13
  randomBoolean,
@@ -8,14 +18,6 @@ import {
8
18
  randomWord,
9
19
  } from "./randomizers";
10
20
 
11
- import type {
12
- PerseusExpressionWidgetOptions,
13
- Version,
14
- PerseusItem,
15
- PerseusRenderer,
16
- ExpressionWidget,
17
- } from "../../perseus-types";
18
-
19
21
  const createItemJson = (
20
22
  widgetOptions: PerseusExpressionWidgetOptions,
21
23
  version: Version,
@@ -35,13 +37,9 @@ const createItemJson = (
35
37
  },
36
38
  _multi: null,
37
39
  answer: null,
38
- answerArea: {
39
- zTable: false,
40
- chi2Table: false,
41
- tTable: false,
42
- calculator: false,
43
- periodicTable: false,
44
- },
40
+ answerArea: Object.fromEntries(
41
+ ItemExtras.map((extra) => [extra, false]),
42
+ ) as PerseusAnswerArea,
45
43
  itemDataVersion: {
46
44
  major: 0,
47
45
  minor: 1,
@@ -178,3 +178,131 @@ export const numberline: PerseusRenderer = {
178
178
  },
179
179
  },
180
180
  };
181
+
182
+ export const longTextFromArticle: PerseusRenderer = {
183
+ content: "[[☃ label-image 1]]",
184
+ images: {},
185
+ widgets: {
186
+ "label-image 1": {
187
+ type: "label-image",
188
+ alignment: "default",
189
+ static: false,
190
+ graded: true,
191
+ options: {
192
+ static: false,
193
+ choices: [
194
+ "long wavelength & low frequency",
195
+ "short wavelength & high frequency",
196
+ ],
197
+ imageAlt:
198
+ "Section of a rainbow in the sky. From left to right, the colors appear as violet, indigo, blue, green, yellow, orange, and red.",
199
+ imageUrl:
200
+ "https://cdn.kastatic.org/ka-content-images/227d402cb09ebc1b67f197467212fa4ab3ced5b3.jpg",
201
+ imageWidth: 400,
202
+ imageHeight: 289,
203
+ markers: [
204
+ {
205
+ answers: ["short wavelength & high frequency"],
206
+ label: "Violet side of the rainbow",
207
+ x: 35.8,
208
+ y: 13,
209
+ },
210
+ {
211
+ answers: ["long wavelength & low frequency"],
212
+ label: "Red side of the rainbow",
213
+ x: 71.8,
214
+ y: 50.5,
215
+ },
216
+ ],
217
+ multipleAnswers: false,
218
+ hideChoicesFromInstructions: true,
219
+ },
220
+ version: {
221
+ major: 0,
222
+ minor: 0,
223
+ },
224
+ },
225
+ },
226
+ };
227
+
228
+ export const mixedContentQuestion: PerseusRenderer = {
229
+ content:
230
+ "A beach ball floats on water. The ball is at rest.\n\n[[☃ image 1]]\n\n**Complete the free body diagram by labeling the forces acting on the ball.**\n\n[[☃ label-image 1]]",
231
+ images: {
232
+ "https://cdn.kastatic.org/ka-content-images/0fa7eefde0067d986d2e6dc377e8d2db3e1d466f.svg":
233
+ {
234
+ width: 150,
235
+ height: 300,
236
+ },
237
+ },
238
+ widgets: {
239
+ "image 1": {
240
+ type: "image",
241
+ alignment: "block",
242
+ static: false,
243
+ graded: true,
244
+ options: {
245
+ static: false,
246
+ title: "",
247
+ range: [
248
+ [0, 10],
249
+ [0, 10],
250
+ ],
251
+ box: [350, 219],
252
+ backgroundImage: {
253
+ url: "https://cdn.kastatic.org/ka-content-images/581c7d3928ba5f81e9dc5720291ec895764cc7e8.png",
254
+ width: 350,
255
+ height: 219,
256
+ },
257
+ labels: [],
258
+ alt: "An inflated beach ball floats on the surface of water. Only a small portion of the ball is underwater.",
259
+ caption: "",
260
+ },
261
+ version: {
262
+ major: 0,
263
+ minor: 0,
264
+ },
265
+ },
266
+ "label-image 1": {
267
+ type: "label-image",
268
+ alignment: "default",
269
+ static: false,
270
+ graded: true,
271
+ options: {
272
+ static: false,
273
+ choices: [
274
+ "lift $(F_\\text{L})$",
275
+ "buoyant force $(F_\\text{B})$",
276
+ "gravity $(F_\\text{g})$",
277
+ "normal force $(F_\\text{N})$",
278
+ ],
279
+ imageAlt:
280
+ "A model shows a square with two arrows pointing out of it. One arrow points up and another arrow points down.",
281
+ imageUrl:
282
+ "https://cdn.kastatic.org/ka-content-images/b24e40b5a36890130acfb7bd237c4bf0d85c7b44.svg",
283
+ imageWidth: 150,
284
+ imageHeight: 300,
285
+ markers: [
286
+ {
287
+ answers: ["buoyant force $(F_\\text{B})$"],
288
+ label: "An arrow that points upward from the square",
289
+ x: 64.2,
290
+ y: 17.6,
291
+ },
292
+ {
293
+ answers: ["gravity $(F_\\text{g})$"],
294
+ label: "An arrow that points downward from the square",
295
+ x: 66.2,
296
+ y: 81.8,
297
+ },
298
+ ],
299
+ multipleAnswers: false,
300
+ hideChoicesFromInstructions: true,
301
+ },
302
+ version: {
303
+ major: 0,
304
+ minor: 0,
305
+ },
306
+ },
307
+ },
308
+ };
@@ -121,6 +121,35 @@ describe("expression mobile", () => {
121
121
  );
122
122
  });
123
123
 
124
+ it("shows keypad after focused input interaction", async () => {
125
+ render(<ConnectedRenderer />);
126
+
127
+ const input = screen.getByLabelText(
128
+ "Math input box Tap with one or two fingers to open keyboard",
129
+ );
130
+
131
+ fireEvent.touchStart(input);
132
+
133
+ // waiting because `visibility` is animated
134
+ await waitFor(() =>
135
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
136
+ );
137
+
138
+ userEvent.click(screen.getByRole("tab", {name: "Dismiss"}));
139
+
140
+ // wait for the keypad to close
141
+ await waitFor(() =>
142
+ expect(screen.queryByRole("button", {name: "1"})).toBeNull(),
143
+ );
144
+
145
+ fireEvent.touchStart(input);
146
+
147
+ // confirm that the keypad has reopened
148
+ await waitFor(() =>
149
+ expect(screen.getByRole("button", {name: "1"})).toBeVisible(),
150
+ );
151
+ });
152
+
124
153
  it("is possible to use the keypad", async () => {
125
154
  render(<ConnectedRenderer />);
126
155
 
@@ -22,7 +22,7 @@ export default story;
22
22
  export const SingleAnswer = {
23
23
  args: {
24
24
  id: "1",
25
- selectedAnswers: ["Answer Pill!"],
25
+ selectedAnswers: ["Answer Pill"],
26
26
  markerRef: null,
27
27
  side: "top",
28
28
  onClick: () => {},
@@ -42,7 +42,7 @@ export const MultipleAnswers = {
42
42
  export const Correct = {
43
43
  args: {
44
44
  id: "1",
45
- selectedAnswers: ["Right Answer!"],
45
+ selectedAnswers: ["Right Answer"],
46
46
  showCorrectness: "correct",
47
47
  markerRef: null,
48
48
  side: "top",
@@ -53,7 +53,7 @@ export const Correct = {
53
53
  export const Incorrect = {
54
54
  args: {
55
55
  id: "1",
56
- selectedAnswers: ["Wrong Answer!"],
56
+ selectedAnswers: ["Wrong Answer"],
57
57
  showCorrectness: "incorrect",
58
58
  markerRef: null,
59
59
  side: "top",
@@ -72,7 +72,7 @@ export const AnswerPill = (props: {
72
72
  incorrect && styles.incorrect,
73
73
  ]}
74
74
  >
75
- <Renderer content={answerString} />
75
+ <Renderer content={answerString} inline />
76
76
  </Pill>
77
77
  )}
78
78
  </Popper>
@@ -667,7 +667,7 @@ export class LabelImage extends React.Component<
667
667
  [`margin${
668
668
  markerPosition.charAt(0).toUpperCase() +
669
669
  markerPosition.slice(1)
670
- }`]: 5, // move pill further from marker
670
+ }`]: 15, // move pill further from marker
671
671
  };
672
672
 
673
673
  const showAnswerChoice =
@@ -50,8 +50,11 @@ class PassageRefTarget extends React.Component<any> {
50
50
  return (
51
51
  <Renderer
52
52
  content={this.props.content}
53
- // @ts-expect-error - TS2322 - Type '{ content: any; inline: boolean; apiOptions: any; linterContext: any; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Renderer> & Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | ... 3 more ... | "legacyPerseusLint"> & InexactPartial<...> & InexactPartial<...>'.
54
- inline={true}
53
+ // This was already here before inline was recently added (#873)
54
+ // It was for a different use case a long time ago:
55
+ // https://phabricator.khanacademy.org/D12113
56
+ // Commenting out to prevent unanticipated side effects
57
+ // inline={true}
55
58
  apiOptions={this.props.apiOptions}
56
59
  linterContext={this.props.linterContext}
57
60
  />