@khanacademy/perseus 1.3.5 → 1.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 +30 -0
- package/dist/es/index.css +3 -2
- package/dist/es/index.js +82 -61
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +3 -2
- package/dist/index.js +78 -51
- package/dist/index.js.map +1 -1
- package/package.json +33 -33
- package/src/components/__stories__/tex.stories.jsx +8 -2
- package/src/index.js +1 -1
- package/src/interactive2/movable-point.jsx +2 -4
- package/src/item-renderer.jsx +3 -6
- package/src/mixins/__tests__/{widget-prop-blacklist_test.js → widget-prop-denylist_test.js} +1 -1
- package/src/mixins/changeable.jsx +2 -2
- package/src/mixins/editor-jsonify.jsx +2 -2
- package/src/mixins/provide-keypad.jsx +3 -3
- package/src/mixins/widget-jsonify-deprecated.jsx +3 -3
- package/src/mixins/{widget-prop-blacklist.js → widget-prop-denylist.js} +2 -3
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +0 -10
- package/src/styles/perseus-renderer.less +0 -3
- package/src/styles/widgets/radio.less +4 -0
- package/src/types.js +1 -7
- package/src/util/react-render.js +27 -0
- package/src/util.js +1 -1
- package/src/widget-container.jsx +1 -1
- package/src/widgets/__stories__/on-screen-keypad.stories.jsx +118 -0
- package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +6 -6
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +0 -2
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +3 -6
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +15 -13
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +6 -7
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +0 -10
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +0 -104
- package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +2 -4
- package/src/widgets/__tests__/graded-group-set_test.jsx +29 -9
- package/src/widgets/categorizer.jsx +6 -1
- package/src/widgets/cs-program.jsx +1 -1
- package/src/widgets/graded-group-set.jsx +43 -39
- package/src/widgets/graded-group.jsx +3 -3
- package/src/widgets/iframe.jsx +1 -1
- package/src/widgets/input-number.jsx +2 -2
- package/src/widgets/passage-ref.jsx +2 -2
- package/src/widgets/radio/widget.jsx +7 -6
- package/src/widgets/radio.jsx +2 -2
|
@@ -63,10 +63,9 @@ exports[`video widget should snapshot on mobile: first mobile render 1`] = `
|
|
|
63
63
|
class="default_xu2jcg-o_O-inlineStyles_sc01dy"
|
|
64
64
|
/>
|
|
65
65
|
<a
|
|
66
|
-
class="
|
|
66
|
+
class="shared_ken7mo-o_O-resting_nelfll visited-no-recolor"
|
|
67
67
|
href="/transcript/videoNotFound"
|
|
68
68
|
rel="noopener noreferrer"
|
|
69
|
-
tabindex="0"
|
|
70
69
|
target="_blank"
|
|
71
70
|
>
|
|
72
71
|
See video transcript
|
|
@@ -144,10 +143,9 @@ exports[`video widget should snapshot: first render 1`] = `
|
|
|
144
143
|
class="default_xu2jcg-o_O-inlineStyles_sc01dy"
|
|
145
144
|
/>
|
|
146
145
|
<a
|
|
147
|
-
class="
|
|
146
|
+
class="shared_ken7mo-o_O-resting_nelfll visited-no-recolor"
|
|
148
147
|
href="/transcript/videoNotFound"
|
|
149
148
|
rel="noopener noreferrer"
|
|
150
|
-
tabindex="0"
|
|
151
149
|
target="_blank"
|
|
152
150
|
>
|
|
153
151
|
See video transcript
|
|
@@ -118,17 +118,37 @@ describe("graded group widget", () => {
|
|
|
118
118
|
).not.toBeInTheDocument();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
describe("should be able to jump to an arbitrary question using Indicators", () => {
|
|
122
|
+
it("by click", () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
renderQuestion(article1);
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
userEvent.click(
|
|
128
|
+
screen.getByRole("button", {name: "Skip to Problem 1c"}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Assert
|
|
132
|
+
expect(screen.getByText("Problem 1c")).toBeVisible();
|
|
133
|
+
});
|
|
124
134
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
135
|
+
it("by key", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
renderQuestion(article1);
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
// Act
|
|
140
|
+
userEvent.tab(); // 1a
|
|
141
|
+
userEvent.tab(); // 1b
|
|
142
|
+
userEvent.keyboard(" ");
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
expect(screen.getByText("Problem 1b")).toBeVisible();
|
|
146
|
+
|
|
147
|
+
userEvent.tab(); // 1c
|
|
148
|
+
userEvent.keyboard("[Enter]");
|
|
149
|
+
|
|
150
|
+
expect(screen.getByText("Problem 1c")).toBeVisible();
|
|
151
|
+
});
|
|
132
152
|
});
|
|
133
153
|
|
|
134
154
|
it("should return input paths", () => {
|
|
@@ -77,7 +77,7 @@ export class Categorizer extends React.Component<Props, State> {
|
|
|
77
77
|
<table className={"categorizer-table " + css(styles.mobileTable)}>
|
|
78
78
|
<thead>
|
|
79
79
|
<tr>
|
|
80
|
-
<
|
|
80
|
+
<td className={css(styles.emptyHeaderCell)} />
|
|
81
81
|
{this.props.categories.map((category, i) => {
|
|
82
82
|
// Array index is the correct key here, as that's
|
|
83
83
|
// how category grading actually works -- no way
|
|
@@ -285,6 +285,11 @@ const styles = StyleSheet.create({
|
|
|
285
285
|
verticalAlign: "middle",
|
|
286
286
|
},
|
|
287
287
|
|
|
288
|
+
emptyHeaderCell: {
|
|
289
|
+
backgroundColor: "inherit",
|
|
290
|
+
borderBottom: "2px solid #ccc",
|
|
291
|
+
},
|
|
292
|
+
|
|
288
293
|
radioSpan: {
|
|
289
294
|
fontSize: 30,
|
|
290
295
|
paddingRight: 3,
|
|
@@ -184,7 +184,7 @@ class CSProgram extends React.Component<$FlowFixMe> {
|
|
|
184
184
|
"allow-top-navigation",
|
|
185
185
|
].join(" ");
|
|
186
186
|
|
|
187
|
-
// We sandbox the iframe so that we
|
|
187
|
+
// We sandbox the iframe so that we allowlist only the functionality
|
|
188
188
|
// that we need. This makes it a bit safer in case some content
|
|
189
189
|
// creator "went wild".
|
|
190
190
|
// http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
3
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
3
4
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
4
5
|
import {StyleSheet, css} from "aphrodite";
|
|
5
6
|
import * as React from "react";
|
|
@@ -7,10 +8,8 @@ import * as React from "react";
|
|
|
7
8
|
import {getDependencies} from "../dependencies.js";
|
|
8
9
|
import * as Changeable from "../mixins/changeable.jsx";
|
|
9
10
|
import {
|
|
10
|
-
grayLight,
|
|
11
11
|
gray76,
|
|
12
12
|
tableBackgroundAccent,
|
|
13
|
-
kaGreen,
|
|
14
13
|
phoneMargin,
|
|
15
14
|
negativePhoneMargin,
|
|
16
15
|
} from "../styles/constants.js";
|
|
@@ -28,40 +27,45 @@ const GradedGroup = GradedGroupWidget.widget;
|
|
|
28
27
|
|
|
29
28
|
type IndicatorsProps = {|
|
|
30
29
|
currentGroup: number,
|
|
31
|
-
|
|
32
|
-
groupTitles: $ReadOnlyArray<string>,
|
|
30
|
+
gradedGroups: $ReadOnlyArray<PerseusGradedGroupWidgetOptions>,
|
|
33
31
|
onChangeCurrentGroup: (groupNumber: number) => void,
|
|
34
32
|
|};
|
|
35
33
|
|
|
36
|
-
// TODO(jeremy): This indicator panel is not accessible for keyboard
|
|
37
|
-
// navigation. Add in keyboard handling and tab indexes to make accessible.
|
|
38
34
|
class Indicators extends React.Component<IndicatorsProps> {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
items.push(
|
|
43
|
-
<div
|
|
44
|
-
role="button"
|
|
45
|
-
aria-label={i18n._("Skip to %(title)s", {
|
|
46
|
-
title: this.props.groupTitles[i],
|
|
47
|
-
})}
|
|
48
|
-
key={i}
|
|
49
|
-
className={css(
|
|
50
|
-
styles.indicator,
|
|
51
|
-
i === this.props.currentGroup &&
|
|
52
|
-
styles.selectedIndicator,
|
|
53
|
-
)}
|
|
54
|
-
onClick={() => this.props.onChangeCurrentGroup(i)}
|
|
55
|
-
>
|
|
56
|
-
{i === this.props.currentGroup && (
|
|
57
|
-
<span className={css(a11y.srOnly)}>
|
|
58
|
-
{i18n._("Current")}
|
|
59
|
-
</span>
|
|
60
|
-
)}
|
|
61
|
-
</div>,
|
|
62
|
-
);
|
|
35
|
+
handleKeyDown = (e: SyntheticKeyboardEvent<>, i: number) => {
|
|
36
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
37
|
+
this.props.onChangeCurrentGroup(i);
|
|
63
38
|
}
|
|
64
|
-
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
render(): React.Node {
|
|
42
|
+
return (
|
|
43
|
+
<ul className={css(styles.indicatorContainer)}>
|
|
44
|
+
{this.props.gradedGroups.map(({title}, i) => (
|
|
45
|
+
<li
|
|
46
|
+
role="button"
|
|
47
|
+
aria-label={i18n._("Skip to %(title)s", {
|
|
48
|
+
title,
|
|
49
|
+
})}
|
|
50
|
+
key={title}
|
|
51
|
+
className={css(
|
|
52
|
+
styles.indicator,
|
|
53
|
+
i === this.props.currentGroup &&
|
|
54
|
+
styles.selectedIndicator,
|
|
55
|
+
)}
|
|
56
|
+
tabIndex={0}
|
|
57
|
+
onClick={() => this.props.onChangeCurrentGroup(i)}
|
|
58
|
+
onKeyDown={(e) => this.handleKeyDown(e, i)}
|
|
59
|
+
>
|
|
60
|
+
{i === this.props.currentGroup && (
|
|
61
|
+
<span className={css(a11y.srOnly)}>
|
|
62
|
+
{i18n._("Current")}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -189,11 +193,8 @@ class GradedGroupSet extends React.Component<Props, State> {
|
|
|
189
193
|
</div>
|
|
190
194
|
<div className={css(styles.spacer)} />
|
|
191
195
|
<Indicators
|
|
192
|
-
numGroups={numGroups}
|
|
193
196
|
currentGroup={this.state.currentGroup}
|
|
194
|
-
|
|
195
|
-
(g) => g.title,
|
|
196
|
-
)}
|
|
197
|
+
gradedGroups={this.props.gradedGroups}
|
|
197
198
|
onChangeCurrentGroup={(currentGroup) =>
|
|
198
199
|
this.setState({currentGroup})
|
|
199
200
|
}
|
|
@@ -253,7 +254,7 @@ const styles = StyleSheet.create({
|
|
|
253
254
|
|
|
254
255
|
title: {
|
|
255
256
|
fontSize: 12,
|
|
256
|
-
color:
|
|
257
|
+
color: Color.offBlack64,
|
|
257
258
|
textTransform: "uppercase",
|
|
258
259
|
marginBottom: 11,
|
|
259
260
|
letterSpacing: 0.8,
|
|
@@ -262,19 +263,22 @@ const styles = StyleSheet.create({
|
|
|
262
263
|
indicatorContainer: {
|
|
263
264
|
display: "flex",
|
|
264
265
|
flexDirection: "row",
|
|
266
|
+
listStyle: "none",
|
|
267
|
+
margin: "unset",
|
|
265
268
|
},
|
|
266
269
|
|
|
267
270
|
indicator: {
|
|
268
271
|
width: 10,
|
|
269
272
|
height: 10,
|
|
270
|
-
borderRadius:
|
|
271
|
-
|
|
273
|
+
borderRadius: "100%",
|
|
274
|
+
border: "3px solid",
|
|
275
|
+
borderColor: Color.blue,
|
|
272
276
|
marginLeft: 5,
|
|
273
277
|
cursor: "pointer",
|
|
274
278
|
},
|
|
275
279
|
|
|
276
280
|
selectedIndicator: {
|
|
277
|
-
backgroundColor:
|
|
281
|
+
backgroundColor: Color.blue,
|
|
278
282
|
},
|
|
279
283
|
|
|
280
284
|
container: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
3
3
|
import Button from "@khanacademy/wonder-blocks-button";
|
|
4
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
4
5
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
5
6
|
import {StyleSheet, css} from "aphrodite";
|
|
6
7
|
import classNames from "classnames";
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
phoneMargin,
|
|
19
20
|
negativePhoneMargin,
|
|
20
21
|
tableBackgroundAccent,
|
|
21
|
-
kaGreen,
|
|
22
22
|
} from "../styles/constants.js";
|
|
23
23
|
import a11y from "../util/a11y.js";
|
|
24
24
|
|
|
@@ -413,7 +413,7 @@ const styles = StyleSheet.create({
|
|
|
413
413
|
padding: 0,
|
|
414
414
|
border: "none",
|
|
415
415
|
marginTop: 20,
|
|
416
|
-
color:
|
|
416
|
+
color: Color.blue,
|
|
417
417
|
cursor: "pointer",
|
|
418
418
|
display: "block",
|
|
419
419
|
clear: "both",
|
|
@@ -422,7 +422,7 @@ const styles = StyleSheet.create({
|
|
|
422
422
|
explanationTitle: {
|
|
423
423
|
backgroundColor: "unset",
|
|
424
424
|
marginTop: 20,
|
|
425
|
-
color:
|
|
425
|
+
color: Color.blue,
|
|
426
426
|
marginBottom: 10,
|
|
427
427
|
cursor: "pointer",
|
|
428
428
|
fontSize: 14,
|
package/src/widgets/iframe.jsx
CHANGED
|
@@ -134,7 +134,7 @@ class Iframe extends React.Component<$FlowFixMe> {
|
|
|
134
134
|
// sandboxProperties += " allow-top-navigation";
|
|
135
135
|
// }
|
|
136
136
|
|
|
137
|
-
// We sandbox the iframe so that we
|
|
137
|
+
// We sandbox the iframe so that we allowlist only the functionality
|
|
138
138
|
// that we need. This makes it a bit safer in case some content
|
|
139
139
|
// creator "went wild".
|
|
140
140
|
// http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
|
|
@@ -205,11 +205,11 @@ class InputNumber extends React.Component<Props> {
|
|
|
205
205
|
this.props.rightAlign ? styles.rightAlign : styles.leftAlign,
|
|
206
206
|
];
|
|
207
207
|
// Correct
|
|
208
|
-
if (rubric && correct && this.props.currentValue) {
|
|
208
|
+
if (rubric && correct === true && this.props.currentValue) {
|
|
209
209
|
inputStyles.push(styles.answerStateCorrect);
|
|
210
210
|
}
|
|
211
211
|
// Incorrect
|
|
212
|
-
if (rubric &&
|
|
212
|
+
if (rubric && correct === false && this.props.currentValue) {
|
|
213
213
|
inputStyles.push(styles.answerStateIncorrect);
|
|
214
214
|
}
|
|
215
215
|
// Unanswered
|
|
@@ -5,7 +5,7 @@ import * as React from "react";
|
|
|
5
5
|
import _ from "underscore";
|
|
6
6
|
|
|
7
7
|
import * as Changeable from "../mixins/changeable.jsx";
|
|
8
|
-
import {removeDenylistProps} from "../mixins/widget-prop-
|
|
8
|
+
import {removeDenylistProps} from "../mixins/widget-prop-denylist.js";
|
|
9
9
|
import PerseusMarkdown from "../perseus-markdown.jsx";
|
|
10
10
|
|
|
11
11
|
import PassageWidgetExport from "./passage.jsx";
|
|
@@ -75,7 +75,7 @@ class PassageRef extends React.Component<Props, State> {
|
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// TODO(
|
|
78
|
+
// TODO(tamarab): getUserInput needs to be updated to only return
|
|
79
79
|
// props input by the user. Currently returns all the widget's props.
|
|
80
80
|
getUserInput: () => UserInput = () => {
|
|
81
81
|
return removeDenylistProps(this.props);
|
|
@@ -13,12 +13,7 @@ import type {
|
|
|
13
13
|
PerseusRadioChoice,
|
|
14
14
|
PerseusRadioWidgetOptions,
|
|
15
15
|
} from "../../perseus-types.js";
|
|
16
|
-
import type {
|
|
17
|
-
PerseusScore,
|
|
18
|
-
WidgetProps,
|
|
19
|
-
ChoiceState,
|
|
20
|
-
RadioChoiceWithMetadata,
|
|
21
|
-
} from "../../types.js";
|
|
16
|
+
import type {PerseusScore, WidgetProps, ChoiceState} from "../../types.js";
|
|
22
17
|
import type {FocusFunction, ChoiceType} from "./base-radio.jsx";
|
|
23
18
|
|
|
24
19
|
// RenderProps is the return type for radio.jsx#transform
|
|
@@ -54,6 +49,12 @@ type DefaultProps = {|
|
|
|
54
49
|
linterContext: Props["linterContext"],
|
|
55
50
|
|};
|
|
56
51
|
|
|
52
|
+
export type RadioChoiceWithMetadata = {|
|
|
53
|
+
...PerseusRadioChoice,
|
|
54
|
+
originalIndex: number,
|
|
55
|
+
correct: boolean,
|
|
56
|
+
|};
|
|
57
|
+
|
|
57
58
|
class Radio extends React.Component<Props> {
|
|
58
59
|
focusFunction: FocusFunction;
|
|
59
60
|
|
package/src/widgets/radio.jsx
CHANGED
|
@@ -7,8 +7,8 @@ import Util from "../util.js";
|
|
|
7
7
|
import Radio from "./radio/widget.jsx";
|
|
8
8
|
|
|
9
9
|
import type {PerseusRadioWidgetOptions} from "../perseus-types.js";
|
|
10
|
-
import type {WidgetExports
|
|
11
|
-
import type {RenderProps} from "./radio/widget.jsx";
|
|
10
|
+
import type {WidgetExports} from "../types.js";
|
|
11
|
+
import type {RenderProps, RadioChoiceWithMetadata} from "./radio/widget.jsx";
|
|
12
12
|
|
|
13
13
|
const {shuffle, random} = Util;
|
|
14
14
|
|