@khanacademy/perseus 12.3.0 → 12.4.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/CHANGELOG.md +19 -0
- package/dist/es/index.css +3 -3
- package/dist/es/index.js +52 -15
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +3 -3
- package/dist/index.js +56 -16
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/widget-container.test.tsx +126 -0
- package/src/error-boundary.tsx +1 -0
- package/src/styles/articles.less +3 -1
- package/src/styles/perseus-renderer.less +1 -1
- package/src/widget-container.tsx +2 -1
- package/src/widgets/__tests__/__snapshots__/graded-group-set.test.ts.snap +50 -19
- package/src/widgets/graded-group-set.tsx +70 -25
- package/tsconfig-build.tsbuildinfo +1 -1
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.
|
|
6
|
+
"version": "12.4.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.
|
|
26
|
-
"@khanacademy/kmath": "^0.1.
|
|
27
|
-
"@khanacademy/math-input": "^14.2.
|
|
28
|
-
"@khanacademy/perseus-core": "1.
|
|
29
|
-
"@khanacademy/perseus-linter": "^0.3.
|
|
30
|
-
"@khanacademy/pure-markdown": "^0.2.
|
|
31
|
-
"@khanacademy/simple-markdown": "^0.10.
|
|
25
|
+
"@khanacademy/kas": "^0.3.5",
|
|
26
|
+
"@khanacademy/kmath": "^0.1.6",
|
|
27
|
+
"@khanacademy/math-input": "^14.2.2",
|
|
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",
|
|
@@ -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
|
+
});
|
package/src/error-boundary.tsx
CHANGED
|
@@ -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/styles/articles.less
CHANGED
|
@@ -498,7 +498,9 @@
|
|
|
498
498
|
margin-bottom: 2 * @baseUnit;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
.perseus-renderer
|
|
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
|
|
package/src/widget-container.tsx
CHANGED
|
@@ -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={(
|
|
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="
|
|
41
|
+
class="indicatorContainer_2pd7o4 indicatorContainer"
|
|
42
42
|
>
|
|
43
43
|
<li
|
|
44
|
-
|
|
45
|
-
class="indicator_qk5jgn-o_O-selectedIndicator_14jsc6v"
|
|
46
|
-
role="button"
|
|
47
|
-
tabindex="0"
|
|
44
|
+
class="indicator_7k5kza"
|
|
48
45
|
>
|
|
49
|
-
<
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
2
2
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
3
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
3
4
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
5
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
4
6
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
5
7
|
import {StyleSheet, css} from "aphrodite";
|
|
8
|
+
import classNames from "classnames";
|
|
6
9
|
import * as React from "react";
|
|
7
10
|
|
|
8
11
|
import {getDependencies} from "../dependencies";
|
|
@@ -38,28 +41,43 @@ class Indicators extends React.Component<IndicatorsProps> {
|
|
|
38
41
|
|
|
39
42
|
render(): React.ReactNode {
|
|
40
43
|
return (
|
|
41
|
-
<ul
|
|
44
|
+
<ul
|
|
45
|
+
// reduntantly add class name for use in .less files--
|
|
46
|
+
// the styles object key gets hashed
|
|
47
|
+
className={classNames(
|
|
48
|
+
css(styles.indicatorContainer),
|
|
49
|
+
"indicatorContainer",
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
42
52
|
{this.props.gradedGroups.map(({title}, i) => (
|
|
43
|
-
<li
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
title,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
<li className={css(styles.indicator)} key={title}>
|
|
54
|
+
<Clickable
|
|
55
|
+
role="button"
|
|
56
|
+
aria-label={i18n._("Skip to %(title)s", {
|
|
57
|
+
title,
|
|
58
|
+
})}
|
|
59
|
+
style={styles.indicatorButton}
|
|
60
|
+
onClick={() => this.props.onChangeCurrentGroup(i)}
|
|
61
|
+
onKeyDown={(e) => this.handleKeyDown(e, i)}
|
|
62
|
+
>
|
|
63
|
+
{({hovered, focused, pressed}) => (
|
|
64
|
+
<View
|
|
65
|
+
style={[
|
|
66
|
+
styles.indicatorDot,
|
|
67
|
+
(hovered || focused || pressed) &&
|
|
68
|
+
styles.indicatorDotFocused,
|
|
69
|
+
]}
|
|
70
|
+
>
|
|
71
|
+
{i === this.props.currentGroup && (
|
|
72
|
+
<View style={styles.indicatorDotActive}>
|
|
73
|
+
<span className={css(a11y.srOnly)}>
|
|
74
|
+
{i18n._("Current")}
|
|
75
|
+
</span>
|
|
76
|
+
</View>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
)}
|
|
80
|
+
</Clickable>
|
|
63
81
|
</li>
|
|
64
82
|
))}
|
|
65
83
|
</ul>
|
|
@@ -262,20 +280,47 @@ const styles = StyleSheet.create({
|
|
|
262
280
|
flexDirection: "row",
|
|
263
281
|
listStyle: "none",
|
|
264
282
|
margin: "unset",
|
|
283
|
+
paddingInlineStart: "unset",
|
|
284
|
+
flexWrap: "wrap",
|
|
265
285
|
},
|
|
266
286
|
|
|
267
287
|
indicator: {
|
|
288
|
+
width: 24,
|
|
289
|
+
height: 24,
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
indicatorButton: {
|
|
293
|
+
width: "100%",
|
|
294
|
+
height: "100%",
|
|
295
|
+
display: "flex",
|
|
296
|
+
flexWrap: "wrap",
|
|
297
|
+
placeContent: "center",
|
|
298
|
+
cursor: "pointer",
|
|
299
|
+
|
|
300
|
+
":focus": {
|
|
301
|
+
outline: "none",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
indicatorDot: {
|
|
306
|
+
boxSizing: "content-box",
|
|
268
307
|
width: 10,
|
|
269
308
|
height: 10,
|
|
270
309
|
borderRadius: "100%",
|
|
271
|
-
|
|
310
|
+
borderWidth: 2,
|
|
272
311
|
borderColor: Color.blue,
|
|
273
|
-
|
|
274
|
-
|
|
312
|
+
borderStyle: "solid",
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
indicatorDotFocused: {
|
|
316
|
+
borderWidth: 5,
|
|
317
|
+
borderStyle: "double",
|
|
275
318
|
},
|
|
276
319
|
|
|
277
|
-
|
|
320
|
+
indicatorDotActive: {
|
|
278
321
|
backgroundColor: Color.blue,
|
|
322
|
+
width: "100%",
|
|
323
|
+
height: "100%",
|
|
279
324
|
},
|
|
280
325
|
|
|
281
326
|
container: {
|