@khanacademy/perseus-editor 2.15.2 → 2.15.4

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 (38) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/editor-page.d.ts +4 -10
  3. package/dist/editor.d.ts +2 -2
  4. package/dist/es/index.js +25 -14
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.js +25 -14
  7. package/dist/index.js.map +1 -1
  8. package/dist/widgets/interactive-graph-editor.d.ts +92 -92
  9. package/package.json +2 -2
  10. package/src/__stories__/editor-page.stories.tsx +65 -0
  11. package/src/__stories__/editor.stories.tsx +2 -4
  12. package/src/editor-page.tsx +5 -11
  13. package/src/editor.tsx +2 -2
  14. package/src/widgets/__stories__/categorizer-editor.stories.tsx +24 -0
  15. package/src/widgets/__stories__/definition-editor.stories.tsx +18 -0
  16. package/src/widgets/__stories__/dropdown-editor.stories.tsx +18 -0
  17. package/src/widgets/__stories__/explanation-editor.stories.tsx +18 -0
  18. package/src/widgets/__stories__/input-number-editor.stories.tsx +18 -0
  19. package/src/widgets/__stories__/matcher-editor.stories.tsx +18 -0
  20. package/src/widgets/__stories__/number-line-editor.stories.tsx +18 -0
  21. package/src/widgets/__stories__/numeric-input-editor.stories.tsx +18 -0
  22. package/src/widgets/__stories__/radio-editor.stories.tsx +3 -3
  23. package/src/widgets/__stories__/sorter-editor.stories.tsx +18 -0
  24. package/src/widgets/__tests__/categorizer-editor.test.tsx +51 -0
  25. package/src/widgets/__tests__/definition-editor.test.tsx +41 -0
  26. package/src/widgets/__tests__/dropdown-editor.test.tsx +57 -0
  27. package/src/widgets/__tests__/explanation-editor.test.tsx +59 -0
  28. package/src/widgets/__tests__/input-number-editor.test.tsx +128 -0
  29. package/src/widgets/__tests__/matcher-editor.test.tsx +47 -0
  30. package/src/widgets/__tests__/number-line-editor.test.tsx +144 -0
  31. package/src/widgets/__tests__/numeric-input-editor.test.tsx +177 -0
  32. package/src/widgets/__tests__/radio-editor.test.tsx +118 -0
  33. package/src/widgets/__tests__/sorter-editor.test.tsx +55 -0
  34. package/src/widgets/dropdown-editor.tsx +1 -0
  35. package/src/widgets/input-number-editor.tsx +2 -0
  36. package/src/widgets/number-line-editor.tsx +16 -5
  37. package/src/widgets/numeric-input-editor.tsx +2 -0
  38. package/tsconfig-build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Perseus editors",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "2.15.2",
6
+ "version": "2.15.4",
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/perseus": "^17.3.2",
27
+ "@khanacademy/perseus": "^17.4.0",
28
28
  "@khanacademy/perseus-core": "1.4.1"
29
29
  },
30
30
  "devDependencies": {
@@ -0,0 +1,65 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import {EditorPage} from "..";
5
+ import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";
6
+
7
+ import type {
8
+ DeviceType,
9
+ Hint,
10
+ PerseusAnswerArea,
11
+ PerseusRenderer,
12
+ } from "@khanacademy/perseus";
13
+
14
+ registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry:
15
+
16
+ export default {
17
+ title: "Perseus/Editor/EditorPage",
18
+ };
19
+
20
+ const onChangeAction = action("onChange");
21
+
22
+ export const Demo = (): React.ReactElement => {
23
+ const [previewDevice, setPreviewDevice] =
24
+ React.useState<DeviceType>("phone");
25
+ const [jsonMode, setJsonMode] = React.useState<boolean | undefined>(false);
26
+ const [answerArea, setAnswerArea] = React.useState<
27
+ PerseusAnswerArea | undefined | null
28
+ >();
29
+ const [question, setQuestion] = React.useState<
30
+ PerseusRenderer | undefined
31
+ >();
32
+ const [hints, setHints] = React.useState<ReadonlyArray<Hint> | undefined>();
33
+
34
+ return (
35
+ <EditorPage
36
+ apiOptions={{isMobile: false}}
37
+ previewDevice={previewDevice}
38
+ onPreviewDeviceChange={(newDevice) => setPreviewDevice(newDevice)}
39
+ developerMode={true}
40
+ jsonMode={jsonMode}
41
+ answerArea={answerArea}
42
+ question={question}
43
+ hints={hints}
44
+ frameSource="about:blank"
45
+ previewURL="about:blank"
46
+ itemId="1"
47
+ onChange={(props) => {
48
+ onChangeAction(props);
49
+
50
+ if ("jsonMode" in props) {
51
+ setJsonMode(props.jsonMode);
52
+ }
53
+ if ("answerArea" in props) {
54
+ setAnswerArea(props.answerArea);
55
+ }
56
+ if ("question" in props) {
57
+ setQuestion(props.question);
58
+ }
59
+ if ("hints" in props) {
60
+ setHints(props.hints);
61
+ }
62
+ }}
63
+ />
64
+ );
65
+ };
@@ -4,19 +4,17 @@ import {View} from "@khanacademy/wonder-blocks-core";
4
4
  import {action} from "@storybook/addon-actions";
5
5
  import * as React from "react";
6
6
 
7
+ import {Editor} from "..";
7
8
  import SideBySide from "../../../../testing/side-by-side";
8
9
  import {question1} from "../__testdata__/input-number.testdata";
9
- import Editor from "../editor";
10
10
  import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing";
11
11
 
12
12
  import type {PerseusRenderer} from "@khanacademy/perseus";
13
13
 
14
- import "../styles/perseus-editor.less";
15
-
16
14
  registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry:
17
15
 
18
16
  export default {
19
- title: "Perseus/Editor",
17
+ title: "Perseus/Editor/Editor",
20
18
  };
21
19
 
22
20
  export const Demo = (): React.ReactElement => {
@@ -15,6 +15,7 @@ import type {
15
15
  Hint,
16
16
  ImageUploader,
17
17
  Version,
18
+ PerseusItem,
18
19
  } from "@khanacademy/perseus";
19
20
  import type {KEScore} from "@khanacademy/perseus-core";
20
21
 
@@ -25,7 +26,7 @@ type Props = {
25
26
  answerArea?: any; // related to the question,
26
27
  // TODO(CP-4838): Should this be a required prop?
27
28
  contentPaths?: ReadonlyArray<string>;
28
- // Only used in the perseus demos. Consider removing.
29
+ // "Power user" mode. Shows the raw JSON of the question.
29
30
  developerMode: boolean;
30
31
  // Source HTML for the iframe to render
31
32
  frameSource: string;
@@ -53,15 +54,8 @@ type Props = {
53
54
  previewURL: string;
54
55
  };
55
56
 
56
- type PerseusJson = {
57
- question: any;
58
- answerArea: any;
59
- hints: ReadonlyArray<Hint>;
60
- itemDataVersion?: Version;
61
- };
62
-
63
57
  type State = {
64
- json: PerseusJson;
58
+ json: PerseusItem;
65
59
  gradeMessage: string;
66
60
  wasAnswered: boolean;
67
61
  highlightLint: boolean;
@@ -207,7 +201,7 @@ class EditorPage extends React.Component<Props, State> {
207
201
  return issues1.concat(issues2);
208
202
  }
209
203
 
210
- serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusJson {
204
+ serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusItem {
211
205
  if (this.props.jsonMode) {
212
206
  return this.state.json;
213
207
  }
@@ -226,7 +220,7 @@ class EditorPage extends React.Component<Props, State> {
226
220
  this.props.onChange(newProps, cb, silent);
227
221
  };
228
222
 
229
- changeJSON: (newJson: PerseusJson) => void = (newJson: PerseusJson) => {
223
+ changeJSON: (newJson: PerseusItem) => void = (newJson: PerseusItem) => {
230
224
  this.setState({
231
225
  json: newJson,
232
226
  });
package/src/editor.tsx CHANGED
@@ -132,7 +132,7 @@ type Props = Readonly<{
132
132
  onChange: ChangeHandler;
133
133
  }>;
134
134
 
135
- type DeafultProps = {
135
+ type DefaultProps = {
136
136
  content: string;
137
137
  disabled: boolean;
138
138
  images: Record<any, any>;
@@ -157,7 +157,7 @@ class Editor extends React.Component<Props, State> {
157
157
  deferredChange: any | null | undefined;
158
158
  widgetIds: any | null | undefined;
159
159
 
160
- static defaultProps: DeafultProps = {
160
+ static defaultProps: DefaultProps = {
161
161
  content: "",
162
162
  placeholder: "",
163
163
  widgets: {},
@@ -0,0 +1,24 @@
1
+ import {ApiOptions} from "@khanacademy/perseus";
2
+ import {action} from "@storybook/addon-actions";
3
+ import * as React from "react";
4
+
5
+ import CategorizerEditor from "../categorizer-editor";
6
+
7
+ type StoryArgs = Record<any, any>;
8
+
9
+ type Story = {
10
+ title: string;
11
+ };
12
+
13
+ export default {
14
+ title: "Perseus/Editor/Widgets/Categorizer Editor",
15
+ } as Story;
16
+
17
+ export const Default = (args: StoryArgs): React.ReactElement => {
18
+ return (
19
+ <CategorizerEditor
20
+ onChange={action("onChange")}
21
+ apiOptions={ApiOptions.defaults}
22
+ />
23
+ );
24
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import DefinitionEditor from "../definition-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Definition Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <DefinitionEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import DropdownEditor from "../dropdown-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Dropdown Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <DropdownEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import ExplanationEditor from "../explanation-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Explanation Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <ExplanationEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import InputNumberEditor from "../input-number-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/InputNumber Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <InputNumberEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import MatcherEditor from "../matcher-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Matcher Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <MatcherEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import NumberLineEditor from "../number-line-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Number Line Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <NumberLineEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import NumericInputEditor from "../numeric-input-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/NumericInput Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <NumericInputEditor onChange={action("onChange")} />;
18
+ };
@@ -97,12 +97,12 @@ class WithState extends React.Component<Empty, PerseusRadioWidgetOptions> {
97
97
  <RadioEditor
98
98
  {...this.state}
99
99
  apiOptions={this.apiOptions}
100
- onChange={(props) =>
100
+ onChange={(props) => {
101
101
  this.setState({
102
102
  ...this._widget.serialize(),
103
103
  ...props,
104
- })
105
- }
104
+ });
105
+ }}
106
106
  // @ts-expect-error [FEI-5003] - TS2322 - Type 'RadioEditor | null' is not assignable to type 'RadioEditor'.
107
107
  ref={(widget) => (this._widget = widget)}
108
108
  />
@@ -0,0 +1,18 @@
1
+ import {action} from "@storybook/addon-actions";
2
+ import * as React from "react";
3
+
4
+ import SorterEditor from "../sorter-editor";
5
+
6
+ type StoryArgs = Record<any, any>;
7
+
8
+ type Story = {
9
+ title: string;
10
+ };
11
+
12
+ export default {
13
+ title: "Perseus/Editor/Widgets/Sorter Editor",
14
+ } as Story;
15
+
16
+ export const Default = (args: StoryArgs): React.ReactElement => {
17
+ return <SorterEditor onChange={action("onChange")} />;
18
+ };
@@ -0,0 +1,51 @@
1
+ import {ApiOptions, Dependencies} from "@khanacademy/perseus";
2
+ import {render, screen} from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import * as React from "react";
5
+
6
+ import "@testing-library/jest-dom";
7
+
8
+ import {testDependencies} from "../../../../../testing/test-dependencies";
9
+ import CategorizerEditor from "../categorizer-editor";
10
+
11
+ describe("categorizer-editor", () => {
12
+ beforeEach(() => {
13
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
14
+ testDependencies,
15
+ );
16
+ });
17
+
18
+ it("should render", async () => {
19
+ render(
20
+ <CategorizerEditor
21
+ onChange={() => undefined}
22
+ apiOptions={ApiOptions.defaults}
23
+ />,
24
+ );
25
+
26
+ expect(
27
+ await screen.findByText("Randomize item order"),
28
+ ).toBeInTheDocument();
29
+ });
30
+
31
+ it("should be possible to change randomize item order", async () => {
32
+ const onChangeMock = jest.fn();
33
+
34
+ render(
35
+ <CategorizerEditor
36
+ onChange={onChangeMock}
37
+ apiOptions={ApiOptions.defaults}
38
+ />,
39
+ );
40
+
41
+ userEvent.click(
42
+ screen.getByRole("checkbox", {
43
+ name: "Randomize item order",
44
+ }),
45
+ );
46
+
47
+ expect(onChangeMock).toBeCalledWith(
48
+ expect.objectContaining({randomizeItems: true}),
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,41 @@
1
+ import {Dependencies} from "@khanacademy/perseus";
2
+ import {render, screen} from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import * as React from "react";
5
+
6
+ import "@testing-library/jest-dom";
7
+
8
+ import {testDependencies} from "../../../../../testing/test-dependencies";
9
+ import DefinitionEditor from "../definition-editor";
10
+
11
+ describe("definition-editor", () => {
12
+ beforeEach(() => {
13
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
14
+ testDependencies,
15
+ );
16
+ });
17
+
18
+ it("should render", async () => {
19
+ render(<DefinitionEditor onChange={() => undefined} />);
20
+
21
+ expect(
22
+ await screen.findByText("Definition style guide"),
23
+ ).toBeInTheDocument();
24
+ });
25
+
26
+ it("should be possible to change the definition", async () => {
27
+ const onChangeMock = jest.fn();
28
+
29
+ render(<DefinitionEditor onChange={onChangeMock} />);
30
+
31
+ const input = screen.getByRole("textbox", {
32
+ name: "Word to be defined:",
33
+ });
34
+ userEvent.type(input, "a");
35
+
36
+ expect(onChangeMock).toBeCalledWith(
37
+ expect.objectContaining({togglePrompt: "a"}),
38
+ undefined,
39
+ );
40
+ });
41
+ });
@@ -0,0 +1,57 @@
1
+ import {Dependencies} from "@khanacademy/perseus";
2
+ import {render, screen} from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import * as React from "react";
5
+
6
+ import "@testing-library/jest-dom";
7
+
8
+ import {testDependencies} from "../../../../../testing/test-dependencies";
9
+ import DropdownEditor from "../dropdown-editor";
10
+
11
+ describe("dropdown-editor", () => {
12
+ beforeEach(() => {
13
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
14
+ testDependencies,
15
+ );
16
+ });
17
+
18
+ it("should be possible to modify placeholder text", async () => {
19
+ const onChangeMock = jest.fn();
20
+
21
+ render(<DropdownEditor onChange={onChangeMock} />);
22
+
23
+ const input = screen.getByPlaceholderText("Placeholder value");
24
+ userEvent.type(input, "a");
25
+
26
+ expect(onChangeMock).toBeCalledWith({placeholder: "a"});
27
+ });
28
+
29
+ it("should be possible to delete choice", async () => {
30
+ const onChangeMock = jest.fn();
31
+
32
+ render(<DropdownEditor onChange={onChangeMock} />);
33
+
34
+ userEvent.click(screen.getByRole("link", {name: "Delete choice"}));
35
+
36
+ expect(onChangeMock).toBeCalledWith({choices: []});
37
+ });
38
+
39
+ it("should be possible to add choice", async () => {
40
+ const onChangeMock = jest.fn();
41
+
42
+ render(<DropdownEditor onChange={onChangeMock} />);
43
+
44
+ userEvent.click(screen.getByRole("link", {name: "Add a choice"}));
45
+
46
+ expect(onChangeMock).toBeCalledWith(
47
+ {
48
+ choices: [
49
+ {content: "", correct: false},
50
+ {content: "", correct: false},
51
+ ],
52
+ },
53
+ // there's some anonymous function that's also passed
54
+ expect.anything(),
55
+ );
56
+ });
57
+ });
@@ -0,0 +1,59 @@
1
+ import {Dependencies} from "@khanacademy/perseus";
2
+ import {render, screen} from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import * as React from "react";
5
+
6
+ import "@testing-library/jest-dom";
7
+
8
+ import {testDependencies} from "../../../../../testing/test-dependencies";
9
+ import ExplanationEditor from "../explanation-editor";
10
+
11
+ describe("explanation-editor", () => {
12
+ beforeEach(() => {
13
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
14
+ testDependencies,
15
+ );
16
+ });
17
+
18
+ it("should render", async () => {
19
+ render(<ExplanationEditor onChange={() => undefined} />);
20
+
21
+ expect(
22
+ await screen.findByText("Prompt to show explanation:"),
23
+ ).toBeInTheDocument();
24
+ });
25
+
26
+ it("should be possible to change prompt to show explanation", async () => {
27
+ const onChangeMock = jest.fn();
28
+
29
+ render(<ExplanationEditor onChange={onChangeMock} />);
30
+
31
+ const input = screen.getByRole("textbox", {
32
+ name: "Prompt to show explanation:",
33
+ });
34
+ userEvent.type(input, "a");
35
+
36
+ expect(onChangeMock).toBeCalledWith(
37
+ // The dropdown initalizes with "Explain"
38
+ expect.objectContaining({showPrompt: "Explaina"}),
39
+ undefined,
40
+ );
41
+ });
42
+
43
+ it("should be possible to change prompt to hide explanation", async () => {
44
+ const onChangeMock = jest.fn();
45
+
46
+ render(<ExplanationEditor onChange={onChangeMock} />);
47
+
48
+ const input = screen.getByRole("textbox", {
49
+ name: "Prompt to hide explanation:",
50
+ });
51
+ userEvent.type(input, "a");
52
+
53
+ expect(onChangeMock).toBeCalledWith(
54
+ // The dropdown initalizes with "Hide explain"
55
+ expect.objectContaining({hidePrompt: "Hide explanationa"}),
56
+ undefined,
57
+ );
58
+ });
59
+ });