@khanacademy/perseus 16.0.0 → 17.1.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 (57) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/article-renderer.d.ts +1 -2
  3. package/dist/components.d.ts +0 -1
  4. package/dist/dependencies.d.ts +1 -0
  5. package/dist/es/index.css +20 -90
  6. package/dist/es/index.js +457 -1028
  7. package/dist/es/index.js.map +1 -1
  8. package/dist/index.css +20 -90
  9. package/dist/index.d.ts +0 -1
  10. package/dist/index.js +456 -1031
  11. package/dist/index.js.map +1 -1
  12. package/dist/perseus-types.d.ts +1 -0
  13. package/dist/server-item-renderer.d.ts +42 -15
  14. package/dist/strings.js +1 -23
  15. package/dist/widgets/label-image/answer-pill.d.ts +10 -0
  16. package/dist/widgets/label-image/hide-answers-toggle.d.ts +6 -0
  17. package/dist/widgets/label-image/marker.d.ts +6 -9
  18. package/dist/widgets/label-image/types.d.ts +1 -0
  19. package/dist/widgets/label-image.d.ts +101 -12
  20. package/package.json +30 -22
  21. package/src/__stories__/server-item-renderer.stories.tsx +20 -1
  22. package/src/__testdata__/server-item-renderer.testdata.ts +173 -0
  23. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +3 -3
  24. package/src/__tests__/server-item-renderer.test.tsx +38 -1
  25. package/src/article-renderer.tsx +13 -15
  26. package/src/components.ts +0 -1
  27. package/src/dependencies.ts +4 -0
  28. package/src/index.ts +0 -1
  29. package/src/perseus-types.ts +2 -0
  30. package/src/server-item-renderer.tsx +26 -14
  31. package/src/styles/khan-exercise.css +0 -24
  32. package/src/styles/widgets/label-image.less +32 -0
  33. package/src/widgets/__stories__/label-image.stories.tsx +73 -3
  34. package/src/widgets/__testdata__/label-image.testdata.ts +118 -1
  35. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +2 -2
  36. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +62 -0
  37. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +83 -0
  38. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +39 -0
  39. package/src/widgets/label-image/answer-choices.tsx +7 -1
  40. package/src/widgets/label-image/answer-pill.tsx +89 -0
  41. package/src/widgets/label-image/hide-answers-toggle.tsx +40 -0
  42. package/src/widgets/label-image/marker.tsx +116 -174
  43. package/src/widgets/label-image/types.ts +1 -0
  44. package/src/widgets/label-image.test.ts +142 -52
  45. package/src/widgets/label-image.tsx +212 -135
  46. package/tsconfig-build.tsbuildinfo +1 -1
  47. package/dist/components/tex-buttons.d.ts +0 -25
  48. package/dist/item-renderer.d.ts +0 -125
  49. package/dist/mixins/provide-keypad.d.ts +0 -39
  50. package/src/__stories__/item-renderer.stories.tsx +0 -30
  51. package/src/__testdata__/item-renderer.testdata.ts +0 -299
  52. package/src/__tests__/__snapshots__/item-renderer.test.tsx.snap +0 -224
  53. package/src/__tests__/item-renderer.test.tsx +0 -930
  54. package/src/components/__stories__/text-buttons.stories.tsx +0 -54
  55. package/src/components/tex-buttons.tsx +0 -334
  56. package/src/item-renderer.tsx +0 -529
  57. package/src/mixins/provide-keypad.tsx +0 -137
@@ -1,6 +1,6 @@
1
1
  import type {PerseusRenderer} from "../../perseus-types";
2
2
 
3
- export const question: PerseusRenderer = {
3
+ export const textQuestion: PerseusRenderer = {
4
4
  content:
5
5
  "Carol created a chart and a bar graph to show how many of each type of vehicle were in her supermarket parking lot.\n\nVehicle Type | Number in the parking lot\n:- | :-: \nTrucks| $25$ \nVans | $5$ \nCars| $40$ \nSUVs | $10$ \n\n**Label each bar on the bar graph.**\n\n[[☃ label-image 1]]\n\n",
6
6
  images: {
@@ -61,3 +61,120 @@ export const question: PerseusRenderer = {
61
61
  },
62
62
  },
63
63
  };
64
+
65
+ export const mathQuestion: PerseusRenderer = {
66
+ content:
67
+ "Carol created a chart and a bar graph to show how many of each type of vehicle were in her supermarket parking lot.\n\nVehicle Type | Number in the parking lot\n:- | :-: \nTrucks| $25$ \nVans | $5$ \nCars| $40$ \nSUVs | $10$ \n\n**Label each bar on the bar graph.**\n\n[[☃ label-image 1]]\n\n",
68
+ images: {
69
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/1e28332fd2321975639ab50c9ce442e568c18421":
70
+ {
71
+ width: 341,
72
+ height: 310,
73
+ },
74
+ },
75
+ widgets: {
76
+ "label-image 1": {
77
+ type: "label-image",
78
+ alignment: "default",
79
+ static: false,
80
+ graded: true,
81
+ options: {
82
+ static: false,
83
+ choices: [
84
+ "$\\dfrac{1}{2}$",
85
+ "$\\dfrac{3}{4}$",
86
+ "$\\dfrac{5}{6}$",
87
+ "$\\dfrac{7}{8}$",
88
+ ],
89
+ imageAlt:
90
+ "A bar graph with four bar lines that shows the horizontal axis labeled Number in the parking lot and the vertical axis labeled Vehicle Type. The horizontal axis is labeled, from left to right: 0, 10, 20, 30, 40, and 50. The vertical axis has, from the bottom to the top, four unlabeled bar lines as follows: the first unlabeled bar line extends to the middle of 0 and 10, the second unlabeled bar line extends to 40, the third unlabeled bar line extends to the middle of 20 and 30, and fourth unlabeled bar line extends to 10.",
91
+ imageUrl:
92
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/56c60c72e96cd353e4a8b5434506cd3a21e717af",
93
+ imageWidth: 415,
94
+ imageHeight: 314,
95
+ markers: [
96
+ {
97
+ answers: ["$\\dfrac{1}{2}$"],
98
+ label: "The fourth unlabeled bar line.",
99
+ x: 25,
100
+ y: 17.7,
101
+ },
102
+ {
103
+ answers: ["$\\dfrac{3}{4}$"],
104
+ label: "The third unlabeled bar line.",
105
+ x: 25,
106
+ y: 35.3,
107
+ },
108
+ {
109
+ answers: ["$\\dfrac{5}{6}$"],
110
+ label: "The second unlabeled bar line.",
111
+ x: 25,
112
+ y: 53,
113
+ },
114
+ {
115
+ answers: ["$\\dfrac{7}{8}$"],
116
+ label: "The first unlabeled bar line.",
117
+ x: 25,
118
+ y: 70.3,
119
+ },
120
+ ],
121
+ multipleAnswers: false,
122
+ hideChoicesFromInstructions: true,
123
+ },
124
+ version: {
125
+ major: 0,
126
+ minor: 0,
127
+ },
128
+ },
129
+ },
130
+ };
131
+
132
+ export const numberline: PerseusRenderer = {
133
+ content:
134
+ "**Label each point on the number line with the correct value.**\n\n[[☃ label-image 1]]",
135
+ images: {},
136
+ widgets: {
137
+ "label-image 1": {
138
+ type: "label-image",
139
+ alignment: "default",
140
+ static: false,
141
+ graded: true,
142
+ options: {
143
+ static: false,
144
+ choices: ["$-\\dfrac{7}{3}$", "$-2\\dfrac{5}{8}$", "$-2.9$"],
145
+ imageAlt:
146
+ "A number line from negative 6 halves to negative 3 halves, labeled in increments of 1 half. There are three points on the line, labeled from left to right with a, b, and c.",
147
+ imageUrl:
148
+ "web+graphie://ka-perseus-graphie.s3.amazonaws.com/05faa925d02e5effd3069bf24da4777e3ae1a28b",
149
+ imageWidth: 360,
150
+ imageHeight: 160,
151
+ markers: [
152
+ {
153
+ answers: ["$-2.9$"],
154
+ label: "Point a is the leftmost of two points between negative 6 halves and negative 5 halves.",
155
+ x: 14.25,
156
+ y: 50,
157
+ },
158
+ {
159
+ answers: ["$-2\\dfrac{5}{8}$"],
160
+ label: "Point b is the rightmost of two points between negative 6 halves and negative 5 halves.",
161
+ x: 29.5,
162
+ y: 50,
163
+ },
164
+ {
165
+ answers: ["$-\\dfrac{7}{3}$"],
166
+ label: "Point c is between negative 5 halves and negative 4 halves.",
167
+ x: 45.5,
168
+ y: 50,
169
+ },
170
+ ],
171
+ multipleAnswers: false,
172
+ hideChoicesFromInstructions: false,
173
+ },
174
+ version: {
175
+ major: 0,
176
+ minor: 0,
177
+ },
178
+ },
179
+ },
180
+ };
@@ -42,7 +42,7 @@ exports[`Dropdown widget should snapshot when opened: dropdown open 1`] = `
42
42
  </span>
43
43
  <span
44
44
  aria-hidden="true"
45
- class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
45
+ class="svg_1q6jc65-o_O-icon_eocf2p-o_O-caret_ntkmq-o_O-inlineStyles_u7ovp6"
46
46
  />
47
47
  </button>
48
48
  </div>
@@ -107,7 +107,7 @@ exports[`Dropdown widget should snapshot: initial render 1`] = `
107
107
  </span>
108
108
  <span
109
109
  aria-hidden="true"
110
- class="svg_aeiopc-o_O-caret_ntkmq-o_O-inlineStyles_13xtep5"
110
+ class="svg_1q6jc65-o_O-icon_eocf2p-o_O-caret_ntkmq-o_O-inlineStyles_u7ovp6"
111
111
  />
112
112
  </button>
113
113
  </div>
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+
3
+ import {AnswerPill} from "../answer-pill";
4
+
5
+ import type {Meta} from "@storybook/react";
6
+
7
+ const story: Meta<React.ComponentProps<typeof AnswerPill>> = {
8
+ title: "Perseus/Widgets/Label Image/Answer Pill",
9
+ component: AnswerPill,
10
+ render: (args) => <AnswerPill {...args} />,
11
+ argTypes: {
12
+ showCorrectness: {
13
+ options: ["not answered", "correct", "incorrect"],
14
+ control: {
15
+ type: "radio",
16
+ },
17
+ },
18
+ },
19
+ };
20
+ export default story;
21
+
22
+ export const SingleAnswer = {
23
+ args: {
24
+ id: "1",
25
+ selectedAnswers: ["Answer Pill!"],
26
+ markerRef: null,
27
+ side: "top",
28
+ onClick: () => {},
29
+ },
30
+ };
31
+
32
+ export const MultipleAnswers = {
33
+ args: {
34
+ id: "1",
35
+ selectedAnswers: ["Answer 1", "Answer 2"],
36
+ markerRef: null,
37
+ side: "top",
38
+ onClick: () => {},
39
+ },
40
+ };
41
+
42
+ export const Correct = {
43
+ args: {
44
+ id: "1",
45
+ selectedAnswers: ["Right Answer!"],
46
+ showCorrectness: "correct",
47
+ markerRef: null,
48
+ side: "top",
49
+ onClick: () => {},
50
+ },
51
+ };
52
+
53
+ export const Incorrect = {
54
+ args: {
55
+ id: "1",
56
+ selectedAnswers: ["Wrong Answer!"],
57
+ showCorrectness: "incorrect",
58
+ markerRef: null,
59
+ side: "top",
60
+ onClick: () => {},
61
+ },
62
+ };
@@ -0,0 +1,83 @@
1
+ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
+ import {render} from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+ import * as React from "react";
5
+
6
+ import {testDependencies} from "../../../../../../testing/test-dependencies";
7
+ import * as Dependencies from "../../../dependencies";
8
+ import {AnswerPill} from "../answer-pill";
9
+
10
+ describe("AnswerPill", () => {
11
+ beforeEach(() => {
12
+ jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
13
+ testDependencies,
14
+ );
15
+ });
16
+
17
+ it("should display the correct answer string when there is only one selected answer", () => {
18
+ const selectedAnswers = ["Answer 1"];
19
+ const screen = render(
20
+ <AnswerPill
21
+ selectedAnswers={selectedAnswers}
22
+ side={"top"}
23
+ onClick={() => {}}
24
+ />,
25
+ {wrapper: RenderStateRoot},
26
+ );
27
+ expect(screen.getByText("Answer 1")).toBeInTheDocument();
28
+ });
29
+
30
+ it("should display the correct answer string when there are multiple selected answers", () => {
31
+ const selectedAnswers = ["Answer 1", "Answer 2", "Answer 3"];
32
+ const screen = render(
33
+ <AnswerPill
34
+ selectedAnswers={selectedAnswers}
35
+ side={"top"}
36
+ onClick={() => {}}
37
+ />,
38
+ {wrapper: RenderStateRoot},
39
+ );
40
+ expect(screen.getByText("3 answers")).toBeInTheDocument();
41
+ });
42
+
43
+ it("should be interactable when no right answer has been chosen", () => {
44
+ // arrange
45
+ const clickSpy = jest.fn();
46
+ const selectedAnswers = ["Answer 1", "Answer 2", "Answer 3"];
47
+ const screen = render(
48
+ <AnswerPill
49
+ selectedAnswers={selectedAnswers}
50
+ side={"top"}
51
+ onClick={clickSpy}
52
+ />,
53
+ {wrapper: RenderStateRoot},
54
+ );
55
+
56
+ // act
57
+ screen.getByText("3 answers").click();
58
+
59
+ // assert
60
+ expect(clickSpy).toHaveBeenCalled();
61
+ });
62
+
63
+ it("should not be interactable when a correct answer has been chosen", () => {
64
+ // arrange
65
+ const clickSpy = jest.fn();
66
+ const selectedAnswers = ["Answer 1", "Answer 2", "Answer 3"];
67
+ const screen = render(
68
+ <AnswerPill
69
+ selectedAnswers={selectedAnswers}
70
+ side={"top"}
71
+ onClick={clickSpy}
72
+ showCorrectness="correct"
73
+ />,
74
+ {wrapper: RenderStateRoot},
75
+ );
76
+
77
+ // act
78
+ screen.getByText("3 answers").click();
79
+
80
+ // assert
81
+ expect(clickSpy).not.toHaveBeenCalled();
82
+ });
83
+ });
@@ -0,0 +1,39 @@
1
+ import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
2
+ import {render} from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+ import userEvent from "@testing-library/user-event";
5
+ import * as React from "react";
6
+
7
+ import {HideAnswersToggle} from "../hide-answers-toggle";
8
+ import {strings} from "../strings";
9
+
10
+ const labelText = strings.hideAnswersToggleLabel;
11
+
12
+ describe("HideAnswersToggle", () => {
13
+ it("should render the toggle switch", () => {
14
+ const screen = render(
15
+ <HideAnswersToggle areAnswersHidden={false} onChange={undefined} />,
16
+ {
17
+ wrapper: RenderStateRoot,
18
+ },
19
+ );
20
+ const toggleSwitch = screen.getByLabelText(
21
+ labelText,
22
+ ) as HTMLInputElement;
23
+ expect(toggleSwitch).toBeInTheDocument();
24
+ expect(toggleSwitch.checked).toBe(false);
25
+ });
26
+
27
+ it("should call the onChange callback when the toggle switch is clicked", async () => {
28
+ const onChange = jest.fn((checked) => expect(checked).toBe(true));
29
+ const screen = render(
30
+ <HideAnswersToggle areAnswersHidden={false} onChange={onChange} />,
31
+ {wrapper: RenderStateRoot},
32
+ );
33
+ const toggleSwitchBefore = screen.getByLabelText(
34
+ labelText,
35
+ ) as HTMLInputElement;
36
+ userEvent.click(toggleSwitchBefore);
37
+ expect(onChange).toHaveBeenCalledTimes(1);
38
+ });
39
+ });
@@ -236,7 +236,13 @@ export default class AnswerChoices extends React.Component<
236
236
  const {groupName} = this.state;
237
237
 
238
238
  return (
239
- <fieldset>
239
+ <fieldset
240
+ style={{
241
+ border: "none",
242
+ margin: 0,
243
+ padding: 0,
244
+ }}
245
+ >
240
246
  {choices.map((choice, index) => (
241
247
  <AnswerChoice
242
248
  {...choice}
@@ -0,0 +1,89 @@
1
+ import Color from "@khanacademy/wonder-blocks-color";
2
+ import {useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core";
3
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
4
+ import Pill from "@khanacademy/wonder-blocks-pill";
5
+ import {StyleSheet, type CSSProperties} from "aphrodite";
6
+ import * as React from "react";
7
+ import {Popper} from "react-popper";
8
+
9
+ import Renderer from "../../renderer";
10
+
11
+ export const AnswerPill = (props: {
12
+ selectedAnswers: readonly string[];
13
+ showCorrectness?: "correct" | "incorrect";
14
+ markerRef?: HTMLElement;
15
+ side: "top" | "bottom" | "left" | "right";
16
+ onClick: () => void;
17
+ style?: CSSProperties;
18
+ }) => {
19
+ const {selectedAnswers, showCorrectness, markerRef, side, onClick, style} =
20
+ props;
21
+
22
+ const idFactory = useUniqueIdWithMock();
23
+
24
+ const answerString =
25
+ selectedAnswers.length > 1
26
+ ? // always need `ngettext` for variable numbers even if we don't use the singular, see https://khanacademy.slack.com/archives/C0918TZ5G/p1700163024293079
27
+ i18n.ngettext(
28
+ "%(num)s answer",
29
+ "%(num)s answers",
30
+ selectedAnswers.length,
31
+ )
32
+ : selectedAnswers[0];
33
+
34
+ // It should no longer be possible to interact with an answer after it
35
+ // has been labeled (answered) correctly.
36
+ const correct = showCorrectness === "correct";
37
+ const incorrect = showCorrectness === "incorrect";
38
+
39
+ const bringToFront = {
40
+ boxShadow: `0 8px 8px ${Color.offBlack64}`,
41
+ zIndex: 1000,
42
+ };
43
+
44
+ return (
45
+ <Popper
46
+ placement={side}
47
+ referenceElement={markerRef}
48
+ modifiers={[
49
+ {
50
+ name: "preventOverflow",
51
+ options: {
52
+ rootBoundary: "viewport",
53
+ },
54
+ },
55
+ ]}
56
+ >
57
+ {({ref, style: popperStyle}) => (
58
+ <Pill
59
+ size="large"
60
+ kind="accent"
61
+ id={idFactory.get("perseus-label-image-widget-answer-pill")}
62
+ onClick={correct ? undefined : onClick}
63
+ ref={ref}
64
+ style={[
65
+ style,
66
+ popperStyle,
67
+ {
68
+ ":hover": bringToFront,
69
+ ":focus": bringToFront,
70
+ },
71
+ correct && styles.correct,
72
+ incorrect && styles.incorrect,
73
+ ]}
74
+ >
75
+ <Renderer content={answerString} />
76
+ </Pill>
77
+ )}
78
+ </Popper>
79
+ );
80
+ };
81
+
82
+ const styles = StyleSheet.create({
83
+ correct: {
84
+ backgroundColor: "#00880b", // WB green darkened by 18%
85
+ },
86
+ incorrect: {
87
+ backgroundColor: Color.offBlack64,
88
+ },
89
+ });
@@ -0,0 +1,40 @@
1
+ import {View, useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core";
2
+ import Switch from "@khanacademy/wonder-blocks-switch";
3
+ import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
4
+ import {StyleSheet} from "aphrodite";
5
+ import * as React from "react";
6
+
7
+ import {strings} from "./strings";
8
+
9
+ export const HideAnswersToggle = (props: {
10
+ areAnswersHidden: boolean;
11
+ onChange: React.ComponentPropsWithoutRef<typeof Switch>["onChange"];
12
+ }) => {
13
+ const idFactory = useUniqueIdWithMock();
14
+ const switchId = idFactory.get("hide-answers-toggle");
15
+ const labelId = idFactory.get("hide-answers-label");
16
+ return (
17
+ <View style={styles.switchWrapper}>
18
+ <Switch
19
+ id={switchId}
20
+ checked={props.areAnswersHidden}
21
+ onChange={props.onChange}
22
+ aria-labelledby={labelId}
23
+ />
24
+ <LabelMedium id={labelId} htmlFor={switchId}>
25
+ {strings.hideAnswersToggleLabel}
26
+ </LabelMedium>
27
+ </View>
28
+ );
29
+ };
30
+
31
+ const styles = StyleSheet.create({
32
+ switchWrapper: {
33
+ display: "flex",
34
+ flexDirection: "row",
35
+ flexWrap: "wrap-reverse",
36
+ alignItems: "center",
37
+ gap: "0.5em",
38
+ marginTop: "1em",
39
+ },
40
+ });