@khanacademy/perseus 1.3.6 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/es/index.css +3 -2
  3. package/dist/es/index.js +80 -59
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +3 -2
  6. package/dist/index.js +76 -49
  7. package/dist/index.js.map +1 -1
  8. package/package.json +33 -33
  9. package/src/components/__stories__/tex.stories.jsx +8 -2
  10. package/src/index.js +1 -1
  11. package/src/interactive2/movable-point.jsx +2 -4
  12. package/src/item-renderer.jsx +3 -6
  13. package/src/mixins/__tests__/{widget-prop-blacklist_test.js → widget-prop-denylist_test.js} +1 -1
  14. package/src/mixins/changeable.jsx +2 -2
  15. package/src/mixins/editor-jsonify.jsx +2 -2
  16. package/src/mixins/provide-keypad.jsx +3 -3
  17. package/src/mixins/widget-jsonify-deprecated.jsx +3 -3
  18. package/src/mixins/{widget-prop-blacklist.js → widget-prop-denylist.js} +2 -3
  19. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +0 -10
  20. package/src/styles/perseus-renderer.less +0 -3
  21. package/src/styles/widgets/radio.less +4 -0
  22. package/src/types.js +1 -7
  23. package/src/util/react-render.js +27 -0
  24. package/src/util.js +1 -1
  25. package/src/widget-container.jsx +1 -1
  26. package/src/widgets/__stories__/on-screen-keypad.stories.jsx +118 -0
  27. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +6 -6
  28. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +0 -2
  29. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +3 -6
  30. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +15 -13
  31. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +6 -7
  32. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +0 -10
  33. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +0 -104
  34. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +2 -4
  35. package/src/widgets/__tests__/graded-group-set_test.jsx +29 -9
  36. package/src/widgets/categorizer.jsx +6 -1
  37. package/src/widgets/cs-program.jsx +1 -1
  38. package/src/widgets/graded-group-set.jsx +43 -39
  39. package/src/widgets/graded-group.jsx +3 -3
  40. package/src/widgets/iframe.jsx +1 -1
  41. package/src/widgets/passage-ref.jsx +2 -2
  42. package/src/widgets/radio/widget.jsx +7 -6
  43. 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="shared_1ff70zb-o_O-default_nelfll visited-no-recolor"
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="shared_1ff70zb-o_O-default_nelfll visited-no-recolor"
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
- it("should be able to jump to an arbitrary question using Indicators", () => {
122
- // Arrange
123
- renderQuestion(article1);
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
- // Act
126
- userEvent.click(
127
- screen.getByRole("button", {name: "Skip to Problem 1c"}),
128
- );
135
+ it("by key", () => {
136
+ // Arrange
137
+ renderQuestion(article1);
129
138
 
130
- // Assert
131
- expect(screen.getByText("Problem 1c")).toBeVisible();
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
- <th>&nbsp;</th>
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 whitelist only the functionality
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
- numGroups: number,
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
- render(): React.Node {
40
- const items = [];
41
- for (let i = 0; i < this.props.numGroups; i++) {
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
- return <div className={css(styles.indicatorContainer)}>{items}</div>;
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
- groupTitles={this.props.gradedGroups.map(
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: gray76,
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: 5,
271
- backgroundColor: grayLight,
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: kaGreen,
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: kaGreen,
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: kaGreen,
425
+ color: Color.blue,
426
426
  marginBottom: 10,
427
427
  cursor: "pointer",
428
428
  fontSize: 14,
@@ -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 whitelist only the functionality
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/
@@ -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-blacklist.js";
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(TB): getUserInput needs to be updated to only return
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
 
@@ -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, RadioChoiceWithMetadata} from "../types.js";
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