@khanacademy/perseus 12.3.0 → 13.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.
@@ -16,7 +16,9 @@ KeypadProps & {
16
16
  item: PerseusItem | any;
17
17
  onShowCalculator?: () => unknown;
18
18
  onShowChi2Table?: () => unknown;
19
- onShowPeriodicTable?: () => unknown;
19
+ onShowPeriodicTable?: (options?: {
20
+ showKey: boolean;
21
+ }) => unknown;
20
22
  onShowTTable?: () => unknown;
21
23
  onShowZTable?: () => unknown;
22
24
  problemNum: number;
@@ -413,17 +413,19 @@ export type PerseusPassageRefWidgetOptions = {
413
413
  referenceNumber: number;
414
414
  summaryText: string;
415
415
  };
416
+ export declare const plotterPlotTypes: readonly ["bar", "line", "pic", "histogram", "dotplot"];
417
+ export type PlotType = typeof plotterPlotTypes[number];
416
418
  export type PerseusPlotterWidgetOptions = {
417
419
  labels: ReadonlyArray<string>;
418
420
  categories: ReadonlyArray<string>;
419
- type: string;
421
+ type: PlotType;
420
422
  maxY: number;
421
423
  scaleY: number;
422
424
  labelInterval: number | null | undefined;
423
425
  snapsPerLine: number;
424
426
  starting: ReadonlyArray<number>;
425
427
  correct: ReadonlyArray<number>;
426
- picUrl: number | null | undefined;
428
+ picUrl: string | null | undefined;
427
429
  picSize: number | null | undefined;
428
430
  picBoxHeight: number | null | undefined;
429
431
  plotDimensions: ReadonlyArray<number>;
@@ -0,0 +1,3 @@
1
+ export declare const strings: {
2
+ hideAnswersToggleLabel: string;
3
+ };
@@ -1,25 +1,11 @@
1
1
  import * as React from "react";
2
- import type { APIOptions } from "../types";
3
- declare const plotTypes: string[];
4
- type PlotType = typeof plotTypes[number];
5
- type Props = {
6
- type: PlotType;
7
- labels: ReadonlyArray<string>;
8
- categories: ReadonlyArray<string | number>;
9
- scaleY: number;
10
- maxY: number;
11
- snapsPerLine: number;
12
- picSize: number;
13
- picBoxHeight: number;
14
- picUrl: string;
15
- plotDimensions: ReadonlyArray<number>;
16
- labelInterval: number;
17
- starting: Array<number>;
18
- static: boolean;
19
- markings: any;
20
- onChange: any;
21
- trackInteraction: () => void;
22
- apiOptions: APIOptions;
2
+ import type { PerseusPlotterWidgetOptions } from "../perseus-types";
3
+ import type { WidgetProps } from "../types";
4
+ type Rubric = PerseusPlotterWidgetOptions;
5
+ type RenderProps = PerseusPlotterWidgetOptions;
6
+ type Props = WidgetProps<RenderProps, Rubric> & {
7
+ labelInterval: NonNullable<PerseusPlotterWidgetOptions["labelInterval"]>;
8
+ picSize: NonNullable<PerseusPlotterWidgetOptions["picSize"]>;
23
9
  };
24
10
  type DefaultProps = {
25
11
  type: Props["type"];
@@ -35,7 +21,7 @@ type DefaultProps = {
35
21
  labelInterval: Props["labelInterval"];
36
22
  };
37
23
  type State = {
38
- values: Array<number>;
24
+ values: ReadonlyArray<number>;
39
25
  categoryHeights: Record<string, number>;
40
26
  };
41
27
  export declare class Plotter extends React.Component<Props, State> {
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": "12.3.0",
6
+ "version": "13.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -22,13 +22,13 @@
22
22
  "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'"
23
23
  },
24
24
  "dependencies": {
25
- "@khanacademy/kas": "^0.3.4",
26
- "@khanacademy/kmath": "^0.1.5",
27
- "@khanacademy/math-input": "^14.2.1",
28
- "@khanacademy/perseus-core": "1.2.0",
29
- "@khanacademy/perseus-linter": "^0.3.7",
30
- "@khanacademy/pure-markdown": "^0.2.9",
31
- "@khanacademy/simple-markdown": "^0.10.3"
25
+ "@khanacademy/kas": "^0.3.5",
26
+ "@khanacademy/kmath": "^0.1.6",
27
+ "@khanacademy/math-input": "^15.0.0",
28
+ "@khanacademy/perseus-core": "1.3.0",
29
+ "@khanacademy/perseus-linter": "^0.3.8",
30
+ "@khanacademy/pure-markdown": "^0.2.10",
31
+ "@khanacademy/simple-markdown": "^0.10.4"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@khanacademy/wonder-blocks-button": "^4.1.3",
@@ -48,6 +48,7 @@
48
48
  "@khanacademy/wonder-blocks-tooltip": "^2.1.2",
49
49
  "@khanacademy/wonder-blocks-typography": "^2.1.3",
50
50
  "@khanacademy/wonder-stuff-core": "^1.5.1",
51
+ "@phosphor-icons/core": "^2.0.2",
51
52
  "aphrodite": "^1.2.5",
52
53
  "classnames": "^1.1.4",
53
54
  "create-react-class": "^15.6.3",
@@ -82,6 +83,7 @@
82
83
  "@khanacademy/wonder-blocks-tooltip": "^2.1.2",
83
84
  "@khanacademy/wonder-blocks-typography": "^2.1.3",
84
85
  "@khanacademy/wonder-stuff-core": "^1.5.1",
86
+ "@phosphor-icons/core": "^2.0.2",
85
87
  "aphrodite": "^1.2.5",
86
88
  "classnames": "^1.1.4",
87
89
  "create-react-class": "^15.6.3",
@@ -805,6 +805,47 @@ describe("item renderer", () => {
805
805
  expect(onShowTTable).toHaveBeenCalled();
806
806
  expect(onShowChi2Table).toHaveBeenCalled();
807
807
  });
808
+
809
+ it("should call onShowPeriodicTable with showKey option if selected", () => {
810
+ // Arrange
811
+ const item = {
812
+ ...itemWithInput,
813
+ answerArea: {
814
+ zTable: false,
815
+ chi2Table: false,
816
+ tTable: false,
817
+ calculator: false,
818
+ periodicTable: true,
819
+ periodicTableWithKey: true,
820
+ },
821
+ } as const;
822
+
823
+ const onShowCalculator = jest.fn();
824
+ const onShowPeriodicTable = jest.fn();
825
+ const onShowZTable = jest.fn();
826
+ const onShowTTable = jest.fn();
827
+ const onShowChi2Table = jest.fn();
828
+ const localExtraProps = {
829
+ ...extraProps,
830
+ onShowCalculator,
831
+ onShowPeriodicTable,
832
+ onShowZTable,
833
+ onShowTTable,
834
+ onShowChi2Table,
835
+ } as const;
836
+
837
+ // Act
838
+ renderQuestion(item, undefined, localExtraProps);
839
+
840
+ // Assert
841
+ expect(onShowCalculator).not.toHaveBeenCalled();
842
+ expect(onShowZTable).not.toHaveBeenCalled();
843
+ expect(onShowTTable).not.toHaveBeenCalled();
844
+ expect(onShowChi2Table).not.toHaveBeenCalled();
845
+ expect(onShowPeriodicTable).toHaveBeenCalledWith(
846
+ expect.objectContaining({showKey: true}),
847
+ );
848
+ });
808
849
  });
809
850
 
810
851
  describe("controlPeripherals is enabled", () => {
@@ -0,0 +1,126 @@
1
+ import "@testing-library/jest-dom"; // Joyous side-effects
2
+ import {render, screen} from "@testing-library/react";
3
+ import * as React from "react";
4
+
5
+ import {testDependencies} from "../../../../testing/test-dependencies";
6
+ import * as Dependencies from "../dependencies";
7
+ import WidgetContainer from "../widget-container";
8
+ import {registerWidget} from "../widgets";
9
+ import PassageWidget from "../widgets/passage";
10
+
11
+ import type {WidgetExports} from "../types";
12
+
13
+ const MockWidgetComponent = ({
14
+ text,
15
+ fail = false,
16
+ }: {
17
+ text: string;
18
+ fail: boolean;
19
+ }) => {
20
+ if (fail) {
21
+ throw new Error("MockWidget failed to render");
22
+ }
23
+
24
+ return <div>{text}</div>;
25
+ };
26
+
27
+ const MockWidget: WidgetExports<typeof MockWidgetComponent> = {
28
+ name: "mock-widget",
29
+ displayName: "Mock Widget",
30
+ widget: MockWidgetComponent,
31
+ };
32
+
33
+ describe("widget-container", () => {
34
+ it("should render nothing when requested widget not registered", () => {
35
+ // Arrange
36
+ const warnMock = jest.spyOn(console, "warn").mockImplementation();
37
+
38
+ // Act
39
+ render(
40
+ <WidgetContainer
41
+ type="invalid-widget"
42
+ id="invalid-widget 1"
43
+ shouldHighlight={false}
44
+ initialProps={{apiOptions: {isMobile: false}}}
45
+ />,
46
+ );
47
+
48
+ // Assert
49
+ expect(warnMock).toHaveBeenCalledWith(
50
+ "Widget type 'invalid-widget' not found!",
51
+ );
52
+ });
53
+
54
+ it("should render the requested widget", async () => {
55
+ // Arrange
56
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
57
+ testDependencies,
58
+ );
59
+
60
+ registerWidget("passage", PassageWidget);
61
+
62
+ // Act
63
+ render(
64
+ <WidgetContainer
65
+ type="passage"
66
+ id="passage 1"
67
+ shouldHighlight={false}
68
+ initialProps={{
69
+ passageTitle: "Greeting",
70
+ passageText: "Hello world!",
71
+ footnotes: null,
72
+ showLineNumbers: true,
73
+
74
+ findWidgets: () => [],
75
+
76
+ apiOptions: {isMobile: false},
77
+ }}
78
+ />,
79
+ );
80
+
81
+ // Assert
82
+ expect(await screen.findByText("Greeting")).toBeInTheDocument();
83
+ expect(await screen.findByText("Hello world")).toBeInTheDocument();
84
+ });
85
+
86
+ it("should send analytics even when widget rendering errors", () => {
87
+ // Arrange
88
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
89
+ testDependencies,
90
+ );
91
+ jest.spyOn(console, "error").mockImplementation(() => {});
92
+
93
+ const onAnalyticsEventSpy = jest.fn();
94
+ const depsV2 = {analytics: {onAnalyticsEvent: onAnalyticsEventSpy}};
95
+
96
+ registerWidget("mock-widget", MockWidget);
97
+
98
+ // Act
99
+ render(
100
+ <Dependencies.DependenciesContext.Provider value={depsV2}>
101
+ <WidgetContainer
102
+ type="mock-widget"
103
+ id="mock-widget 1"
104
+ shouldHighlight={false}
105
+ initialProps={{
106
+ text: "Hello world!",
107
+ fail: true,
108
+
109
+ findWidgets: () => [],
110
+
111
+ apiOptions: {isMobile: false},
112
+ }}
113
+ />
114
+ </Dependencies.DependenciesContext.Provider>,
115
+ );
116
+
117
+ // Assert
118
+ expect(onAnalyticsEventSpy).toHaveBeenCalledWith({
119
+ type: "perseus:widget-rendering-error",
120
+ payload: {
121
+ widgetType: "mock-widget",
122
+ widgetId: "mock-widget 1",
123
+ },
124
+ });
125
+ });
126
+ });
@@ -21,6 +21,7 @@ class ErrorBoundary extends React.Component<Props, State> {
21
21
 
22
22
  componentDidCatch(error: Error, info: any) {
23
23
  this.setState({error: error.toString()});
24
+ this.props.onError?.(error, info);
24
25
  Log.error("Perseus error boundary caught error", Errors.Internal, {
25
26
  cause: error,
26
27
  loggedMetadata: {
package/src/index.ts CHANGED
@@ -62,7 +62,10 @@ export {default as KhanMath} from "./util/math";
62
62
  export {default as LoadingContext} from "./loading-context";
63
63
  export {default as mediaQueries} from "./styles/media-queries";
64
64
  export {default as PerseusMarkdown} from "./perseus-markdown";
65
- export {PerseusExpressionAnswerFormConsidered} from "./perseus-types";
65
+ export {
66
+ PerseusExpressionAnswerFormConsidered,
67
+ plotterPlotTypes,
68
+ } from "./perseus-types";
66
69
  export {traverse} from "./traversal";
67
70
  export {isItemRenderableByVersion} from "./renderability";
68
71
  export {violatingWidgets} from "./a11y";
@@ -123,6 +126,7 @@ export type {
123
126
  PerseusInteractiveGraphWidgetOptions,
124
127
  PerseusRadioWidgetOptions,
125
128
  PerseusExpressionWidgetOptions,
129
+ PerseusPlotterWidgetOptions,
126
130
  PerseusRenderer,
127
131
  } from "./perseus-types";
128
132
  export type {Coord} from "./interactive2/types";
@@ -39,7 +39,7 @@ type Props = // These props are used by the ProvideKeypad mixin.
39
39
  item: PerseusItem | any /* any for _multi items */;
40
40
  onShowCalculator?: () => unknown;
41
41
  onShowChi2Table?: () => unknown;
42
- onShowPeriodicTable?: () => unknown;
42
+ onShowPeriodicTable?: (options?: {showKey: boolean}) => unknown;
43
43
  onShowTTable?: () => unknown;
44
44
  onShowZTable?: () => unknown;
45
45
  problemNum: number;
@@ -254,7 +254,9 @@ class ItemRenderer extends React.Component<Props, State> {
254
254
  }
255
255
  if (answerArea.periodicTable) {
256
256
  this.props.onShowPeriodicTable &&
257
- this.props.onShowPeriodicTable();
257
+ this.props.onShowPeriodicTable({
258
+ showKey: answerArea.periodicTableWithKey,
259
+ });
258
260
  }
259
261
  if (answerArea.zTable) {
260
262
  this.props.onShowZTable && this.props.onShowZTable();
@@ -848,13 +848,22 @@ export type PerseusPassageRefWidgetOptions = {
848
848
  summaryText: string;
849
849
  };
850
850
 
851
+ export const plotterPlotTypes = [
852
+ "bar",
853
+ "line",
854
+ "pic",
855
+ "histogram",
856
+ "dotplot",
857
+ ] as const;
858
+ export type PlotType = typeof plotterPlotTypes[number];
859
+
851
860
  export type PerseusPlotterWidgetOptions = {
852
861
  // Translatable Text; The Axis labels. e.g. ["X Label", "Y Label"]
853
862
  labels: ReadonlyArray<string>;
854
863
  // Translatable Text; Categories to display along the X access. e.g. [">0", ">6", ">12", ">18"]
855
864
  categories: ReadonlyArray<string>;
856
865
  // The type of the graph. options "bar", "line", "pic", "histogram", "dotplot"
857
- type: string;
866
+ type: PlotType;
858
867
  // The maximimum Y tick to display in the graph
859
868
  maxY: number;
860
869
  // The scale of the Y Axis
@@ -868,7 +877,7 @@ export type PerseusPlotterWidgetOptions = {
868
877
  // The Y values that represent the correct answer expected
869
878
  correct: ReadonlyArray<number>;
870
879
  // A picture to represent items in a graph.
871
- picUrl: number | null | undefined;
880
+ picUrl: string | null | undefined;
872
881
  // deprecated
873
882
  picSize: number | null | undefined;
874
883
  // deprecated
@@ -498,7 +498,9 @@
498
498
  margin-bottom: 2 * @baseUnit;
499
499
  }
500
500
 
501
- .perseus-renderer > .paragraph ul:not(.perseus-widget-radio) {
501
+ .perseus-renderer
502
+ > .paragraph
503
+ ul:not(.perseus-widget-radio, .indicatorContainer) {
502
504
  margin: 0 0 0 1em;
503
505
  padding: 0;
504
506
 
@@ -133,7 +133,7 @@
133
133
  margin: -11px 0px 22px 0px; // first-level lists need padding
134
134
  }
135
135
 
136
- .paragraph ul:not(.perseus-widget-radio) {
136
+ .paragraph ul:not(.perseus-widget-radio, .indicatorContainer) {
137
137
  padding-left: 35px;
138
138
  list-style-type: disc;
139
139
  }
@@ -144,11 +144,12 @@ class WidgetContainer extends React.Component<Props, State> {
144
144
  widget_type: type,
145
145
  widget_id: this.props.id,
146
146
  }}
147
- onError={(error) => {
147
+ onError={() => {
148
148
  analytics.onAnalyticsEvent({
149
149
  type: "perseus:widget-rendering-error",
150
150
  payload: {
151
151
  widgetType: type,
152
+ widgetId: this.props.id,
152
153
  },
153
154
  });
154
155
  }}
@@ -38,32 +38,63 @@ exports[`graded group widget should snapshot 1`] = `
38
38
  class="spacer_bc4egv"
39
39
  />
40
40
  <ul
41
- class="indicatorContainer_rjg7c2"
41
+ class="indicatorContainer_2pd7o4 indicatorContainer"
42
42
  >
43
43
  <li
44
- aria-label="Skip to Problem 1a"
45
- class="indicator_qk5jgn-o_O-selectedIndicator_14jsc6v"
46
- role="button"
47
- tabindex="0"
44
+ class="indicator_7k5kza"
48
45
  >
49
- <span
50
- class="srOnly_19bpjuy"
46
+ <button
47
+ aria-disabled="false"
48
+ aria-label="Skip to Problem 1a"
49
+ class="button_vr44p2-o_O-reset_152ygtm-o_O-link_13xlah4-o_O-indicatorButton_8uaklu"
50
+ role="button"
51
+ type="button"
51
52
  >
52
- Current
53
- </span>
53
+ <div
54
+ class="default_xu2jcg-o_O-indicatorDot_yy8ygm"
55
+ >
56
+ <div
57
+ class="default_xu2jcg-o_O-indicatorDotActive_1pky1a"
58
+ >
59
+ <span
60
+ class="srOnly_19bpjuy"
61
+ >
62
+ Current
63
+ </span>
64
+ </div>
65
+ </div>
66
+ </button>
54
67
  </li>
55
68
  <li
56
- aria-label="Skip to Problem 1b"
57
- class="indicator_qk5jgn"
58
- role="button"
59
- tabindex="0"
60
- />
69
+ class="indicator_7k5kza"
70
+ >
71
+ <button
72
+ aria-disabled="false"
73
+ aria-label="Skip to Problem 1b"
74
+ class="button_vr44p2-o_O-reset_152ygtm-o_O-link_13xlah4-o_O-indicatorButton_8uaklu"
75
+ role="button"
76
+ type="button"
77
+ >
78
+ <div
79
+ class="default_xu2jcg-o_O-indicatorDot_yy8ygm"
80
+ />
81
+ </button>
82
+ </li>
61
83
  <li
62
- aria-label="Skip to Problem 1c"
63
- class="indicator_qk5jgn"
64
- role="button"
65
- tabindex="0"
66
- />
84
+ class="indicator_7k5kza"
85
+ >
86
+ <button
87
+ aria-disabled="false"
88
+ aria-label="Skip to Problem 1c"
89
+ class="button_vr44p2-o_O-reset_152ygtm-o_O-link_13xlah4-o_O-indicatorButton_8uaklu"
90
+ role="button"
91
+ type="button"
92
+ >
93
+ <div
94
+ class="default_xu2jcg-o_O-indicatorDot_yy8ygm"
95
+ />
96
+ </button>
97
+ </li>
67
98
  </ul>
68
99
  </div>
69
100
  <div
@@ -4,13 +4,12 @@ import React from "react";
4
4
 
5
5
  import {testDependencies} from "../../../../../testing/test-dependencies";
6
6
  import {setDependencies} from "../../dependencies";
7
+ import {ApiOptions} from "../../perseus-api";
7
8
  import {question1} from "../__testdata__/plotter.testdata";
8
9
  import {Plotter} from "../plotter";
9
10
 
10
11
  import {renderQuestion} from "./renderQuestion";
11
12
 
12
- import type {APIOptions} from "../../types";
13
-
14
13
  describe("plotter widget", () => {
15
14
  beforeEach(() => {
16
15
  setDependencies(testDependencies);
@@ -26,15 +25,17 @@ describe("plotter widget", () => {
26
25
 
27
26
  it("should show drag text when not static", () => {
28
27
  // Arrange and Act
29
- const apiOptions: APIOptions = {isMobile: true};
30
28
  render(
29
+ // @ts-expect-error - TS2769 - test works, but I can't figure out how to make the types happy
31
30
  <Plotter
32
31
  static={false}
33
32
  trackInteraction={() => {}}
34
33
  onChange={() => {}}
35
- apiOptions={apiOptions}
34
+ apiOptions={{
35
+ ...ApiOptions.defaults,
36
+ isMobile: true,
37
+ }}
36
38
  starting={[0]}
37
- markings={""}
38
39
  />,
39
40
  );
40
41
 
@@ -46,15 +47,17 @@ describe("plotter widget", () => {
46
47
 
47
48
  it("should not show drag text when static", () => {
48
49
  // Arrange and Act
49
- const apiOptions: APIOptions = {isMobile: true};
50
50
  render(
51
+ // @ts-expect-error - TS2769 - test works, but I can't figure out how to make the types happy
51
52
  <Plotter
52
53
  static={true}
53
54
  trackInteraction={() => {}}
54
55
  onChange={() => {}}
55
- apiOptions={apiOptions}
56
+ apiOptions={{
57
+ ...ApiOptions.defaults,
58
+ isMobile: true,
59
+ }}
56
60
  starting={[0]}
57
- markings={""}
58
61
  />,
59
62
  );
60
63