@khanacademy/perseus 1.1.2 → 1.3.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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/es/index.css +4 -5
  3. package/dist/es/index.js +1282 -1429
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +4 -5
  6. package/dist/index.js +1283 -1429
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +21 -3
  10. package/src/components/highlighting/ui/highlight-tooltip.jsx +29 -23
  11. package/src/components/highlighting/ui/selection-tracker.jsx +17 -28
  12. package/src/styles/articles.less +0 -2
  13. package/src/styles/perseus-renderer.less +1 -1
  14. package/src/styles/widgets/passage.less +7 -3
  15. package/src/widgets/__stories__/passage-ref.stories.jsx +6 -2
  16. package/src/widgets/__stories__/passage.stories.jsx +9 -1
  17. package/src/widgets/__stories__/radio.stories.jsx +11 -0
  18. package/src/widgets/__testdata__/passage-ref_testdata.js +62 -0
  19. package/src/widgets/__testdata__/passage_testdata.js +25 -0
  20. package/src/widgets/__testdata__/radio_testdata.js +52 -0
  21. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +16 -2
  22. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +18 -18
  23. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +36 -36
  24. package/src/widgets/__tests__/passage_test.jsx +6 -8
  25. package/src/widgets/__tests__/radio_test.jsx +3 -1
  26. package/src/widgets/passage.jsx +12 -41
  27. package/src/widgets/radio/widget.jsx +128 -129
  28. package/src/components/new-tooltip/README.md +0 -3
  29. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +0 -665
  30. package/src/components/new-tooltip/__tests__/style-constants_test.js +0 -8
  31. package/src/components/new-tooltip/new-tooltip.jsx +0 -155
  32. package/src/components/new-tooltip/style-constants.js +0 -13
  33. package/src/components/new-tooltip/types.js +0 -183
  34. package/src/widgets/passage/get-line-height-for-node.js +0 -9
@@ -219,13 +219,13 @@ exports[`passage widget should snapshot multiple passages (mobile: false) 1`] =
219
219
  <div>
220
220
  <div>
221
221
  <div
222
- class="measurer_vtwrnx"
222
+ aria-hidden="true"
223
+ class="measurer_225hir"
223
224
  >
224
- <div>
225
- <div
226
- class="paragraph"
227
- />
228
- <div />
225
+ <div
226
+ class="paragraph"
227
+ >
228
+ Line Height Measurement
229
229
  </div>
230
230
  </div>
231
231
  <div
@@ -711,13 +711,13 @@ exports[`passage widget should snapshot multiple passages (mobile: false) 1`] =
711
711
  <div>
712
712
  <div>
713
713
  <div
714
- class="measurer_vtwrnx"
714
+ aria-hidden="true"
715
+ class="measurer_225hir"
715
716
  >
716
- <div>
717
- <div
718
- class="paragraph"
719
- />
720
- <div />
717
+ <div
718
+ class="paragraph"
719
+ >
720
+ Line Height Measurement
721
721
  </div>
722
722
  </div>
723
723
  <div
@@ -1090,13 +1090,13 @@ exports[`passage widget should snapshot multiple passages (mobile: true) 1`] = `
1090
1090
  <div>
1091
1091
  <div>
1092
1092
  <div
1093
- class="measurer_vtwrnx"
1093
+ aria-hidden="true"
1094
+ class="measurer_225hir"
1094
1095
  >
1095
- <div>
1096
- <div
1097
- class="paragraph"
1098
- />
1099
- <div />
1096
+ <div
1097
+ class="paragraph"
1098
+ >
1099
+ Line Height Measurement
1100
1100
  </div>
1101
1101
  </div>
1102
1102
  <div
@@ -1582,13 +1582,13 @@ exports[`passage widget should snapshot multiple passages (mobile: true) 1`] = `
1582
1582
  <div>
1583
1583
  <div>
1584
1584
  <div
1585
- class="measurer_vtwrnx"
1585
+ aria-hidden="true"
1586
+ class="measurer_225hir"
1586
1587
  >
1587
- <div>
1588
- <div
1589
- class="paragraph"
1590
- />
1591
- <div />
1588
+ <div
1589
+ class="paragraph"
1590
+ >
1591
+ Line Height Measurement
1592
1592
  </div>
1593
1593
  </div>
1594
1594
  <div
@@ -1840,13 +1840,13 @@ exports[`passage widget should snapshot simple passage (mobile: false) 1`] = `
1840
1840
  <div>
1841
1841
  <div>
1842
1842
  <div
1843
- class="measurer_vtwrnx"
1843
+ aria-hidden="true"
1844
+ class="measurer_225hir"
1844
1845
  >
1845
- <div>
1846
- <div
1847
- class="paragraph"
1848
- />
1849
- <div />
1846
+ <div
1847
+ class="paragraph"
1848
+ >
1849
+ Line Height Measurement
1850
1850
  </div>
1851
1851
  </div>
1852
1852
  <div
@@ -1941,13 +1941,13 @@ exports[`passage widget should snapshot simple passage (mobile: true) 1`] = `
1941
1941
  <div>
1942
1942
  <div>
1943
1943
  <div
1944
- class="measurer_vtwrnx"
1944
+ aria-hidden="true"
1945
+ class="measurer_225hir"
1945
1946
  >
1946
- <div>
1947
- <div
1948
- class="paragraph"
1949
- />
1950
- <div />
1947
+ <div
1948
+ class="paragraph"
1949
+ >
1950
+ Line Height Measurement
1951
1951
  </div>
1952
1952
  </div>
1953
1953
  <div
@@ -8,15 +8,12 @@ import {testDependencies} from "../../../../../testing/test-dependencies.js";
8
8
  import * as Dependencies from "../../dependencies.js";
9
9
  import {ApiOptions} from "../../perseus-api.jsx";
10
10
  import {question1, question2} from "../__testdata__/passage_testdata.js";
11
- import PassageWidgetExport from "../passage.jsx";
12
- import * as getLineHeightModule from "../passage/get-line-height-for-node.js";
11
+ import PassageWidgetExport, {LineHeightMeasurer} from "../passage.jsx";
13
12
 
14
13
  import {renderQuestion} from "./renderQuestion.jsx";
15
14
 
16
15
  import type {APIOptions} from "../../types.js";
17
16
 
18
- jest.mock("../passage/get-line-height-for-node.js");
19
-
20
17
  function renderPassage(overwrite) {
21
18
  const widgetPropsBase = {
22
19
  footnotes: "",
@@ -53,10 +50,6 @@ function renderPassage(overwrite) {
53
50
 
54
51
  describe("passage widget", () => {
55
52
  beforeEach(() => {
56
- jest.spyOn(getLineHeightModule, "getLineHeightForNode").mockReturnValue(
57
- 21,
58
- );
59
-
60
53
  // passage widget uses the height of the entire rendered passage along
61
54
  // with the above line height calculations to determine the number of
62
55
  // lines rendered (as a way to provide line numbering).
@@ -66,6 +59,11 @@ describe("passage widget", () => {
66
59
  "get",
67
60
  ).mockReturnValue(200.0);
68
61
 
62
+ jest.spyOn(
63
+ LineHeightMeasurer.prototype,
64
+ "measureLineHeight",
65
+ ).mockReturnValue(20);
66
+
69
67
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
70
68
  testDependencies,
71
69
  );
@@ -800,7 +800,9 @@ describe("multi-choice question", () => {
800
800
 
801
801
  // Act
802
802
  const option = screen.getAllByRole("checkbox");
803
- userEvent.click(option[3]); // incorrect
803
+ userEvent.click(option[0]); // correct
804
+ userEvent.click(option[1]); // correct
805
+ userEvent.click(option[2]); // incorrect
804
806
 
805
807
  // Assert
806
808
  expect(renderer).toHaveInvalidInput(
@@ -1,5 +1,4 @@
1
1
  // @flow
2
-
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
5
4
  import {StyleSheet, css} from "aphrodite";
@@ -12,7 +11,6 @@ import HighlightableContent from "../components/highlighting/highlightable-conte
12
11
  import {getDependencies} from "../dependencies.js";
13
12
  import Renderer from "../renderer.jsx";
14
13
 
15
- import {getLineHeightForNode} from "./passage/get-line-height-for-node.js";
16
14
  import PassageMarkdown from "./passage/passage-markdown.jsx";
17
15
 
18
16
  import type {SerializedHighlightSet} from "../components/highlighting/types.js";
@@ -27,51 +25,24 @@ import type {
27
25
  import type {ParseState} from "./passage/passage-markdown.jsx";
28
26
  import type {SingleASTNode} from "@khanacademy/simple-markdown";
29
27
 
30
- // A fake paragraph to measure the line height of the passage. In CSS we always
31
- // set the line height to 22 pixels, but when using the browser zoom feature,
32
- // the line height often ends up being a fractional number of pixels close to
33
- // 22 pixels.
34
- class LineHeightMeasurer extends React.Component<{...}> {
35
- _cachedLineHeight: number;
36
- body: ?HTMLDivElement;
37
- end: ?HTMLDivElement;
28
+ // A fake paragraph to measure the line height of the passage,
29
+ // so we can adapt to browser zoom
30
+ export class LineHeightMeasurer extends React.Component<{...}> {
31
+ _line: ?HTMLDivElement;
38
32
 
39
33
  measureLineHeight(): number {
40
- if (typeof this._cachedLineHeight !== "number") {
41
- this.forceMeasureLineHeight();
42
- }
43
-
44
- return this._cachedLineHeight;
45
- }
46
-
47
- forceMeasureLineHeight() {
48
- const body = this.body;
49
- const end = this.end;
50
-
51
- if (!body || !end) {
52
- return;
34
+ if (!this._line) {
35
+ return 0;
53
36
  }
54
37
 
55
- // Add some text which magically fills an entire line.
56
- body.textContent = " \u0080";
57
-
58
- // Now, the line height is the difference between the top of the
59
- // second line and the top of the first line.
60
- this._cachedLineHeight = getLineHeightForNode(body, end);
61
-
62
- // Clear out the first line so it doesn't overlap the passage.
63
- body.textContent = "";
38
+ return this._line.clientHeight;
64
39
  }
65
40
 
66
41
  render(): React.Node {
67
42
  return (
68
- <div className={css(styles.measurer)}>
69
- <div>
70
- <div
71
- ref={(ref) => (this.body = ref)}
72
- className="paragraph"
73
- />
74
- <div ref={(ref) => (this.end = ref)} />
43
+ <div className={css(styles.measurer)} aria-hidden="true">
44
+ <div ref={(ref) => (this._line = ref)} className="paragraph">
45
+ Line Height Measurement
75
46
  </div>
76
47
  </div>
77
48
  );
@@ -81,9 +52,9 @@ class LineHeightMeasurer extends React.Component<{...}> {
81
52
  const styles = StyleSheet.create({
82
53
  measurer: {
83
54
  position: "absolute",
84
- width: "100%",
85
55
  top: 0,
86
56
  left: 0,
57
+ visibility: "hidden",
87
58
  // keep from blocking text selection
88
59
  zIndex: -1,
89
60
  },
@@ -168,7 +139,7 @@ class Passage extends React.Component<PassageProps, PassageState> {
168
139
  // height changes we expect are subpixel changes when the user
169
140
  // zooms in/out, and the only way to listen for zoom events is to
170
141
  // listen for resize events.
171
- this._lineHeightMeasurerRef?.forceMeasureLineHeight();
142
+ this._lineHeightMeasurerRef?.measureLineHeight();
172
143
  this._updateState();
173
144
  }, 500);
174
145
  window.addEventListener("resize", this._onResize);
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/sort-comp */
2
1
  // @flow
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
@@ -66,6 +65,134 @@ class Radio extends React.Component<Props> {
66
65
  linterContext: linterContextDefault,
67
66
  };
68
67
 
68
+ static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
69
+ const numSelected = userInput.choicesSelected.reduce(
70
+ (sum, selected) => {
71
+ return sum + (selected ? 1 : 0);
72
+ },
73
+ 0,
74
+ );
75
+
76
+ if (numSelected === 0) {
77
+ return {
78
+ type: "invalid",
79
+ message: null,
80
+ };
81
+ }
82
+
83
+ if (
84
+ userInput.numCorrect &&
85
+ userInput.numCorrect > 1 &&
86
+ numSelected !== userInput.numCorrect
87
+ ) {
88
+ return {
89
+ type: "invalid",
90
+ message: i18n._("Please choose the correct number of answers."),
91
+ };
92
+ // If NOTA and some other answer are checked, ...
93
+ }
94
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
95
+ return {
96
+ type: "invalid",
97
+ message: i18n._(
98
+ "'None of the above' may not be selected " +
99
+ "when other answers are selected.",
100
+ ),
101
+ };
102
+ }
103
+
104
+ const correct = userInput.choicesSelected.every((selected, i) => {
105
+ let isCorrect;
106
+ if (userInput.noneOfTheAboveIndex === i) {
107
+ isCorrect = rubric.choices.every((choice, j) => {
108
+ return i === j || !choice.correct;
109
+ });
110
+ } else {
111
+ isCorrect = !!rubric.choices[i].correct;
112
+ }
113
+ return isCorrect === selected;
114
+ });
115
+
116
+ return {
117
+ type: "points",
118
+ earned: correct ? 1 : 0,
119
+ total: 1,
120
+ message: null,
121
+ };
122
+ }
123
+
124
+ static getUserInputFromProps(props: Props): UserInput {
125
+ // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
126
+ // future timeline implementers: this used to be {value: i} before
127
+ // multiple select was added)
128
+ if (props.choiceStates) {
129
+ let noneOfTheAboveIndex = null;
130
+ let noneOfTheAboveSelected = false;
131
+
132
+ const choiceStates = props.choiceStates;
133
+ const choicesSelected = choiceStates.map(() => false);
134
+ const countChoices = props.countChoices;
135
+ const numCorrect = props.numCorrect;
136
+
137
+ for (let i = 0; i < choicesSelected.length; i++) {
138
+ const index = props.choices[i].originalIndex;
139
+
140
+ choicesSelected[index] = choiceStates[i].selected;
141
+
142
+ if (props.choices[i].isNoneOfTheAbove) {
143
+ noneOfTheAboveIndex = index;
144
+
145
+ if (choicesSelected[i]) {
146
+ noneOfTheAboveSelected = true;
147
+ }
148
+ }
149
+ }
150
+
151
+ return {
152
+ countChoices,
153
+ choicesSelected,
154
+ numCorrect,
155
+ noneOfTheAboveIndex,
156
+ noneOfTheAboveSelected,
157
+ };
158
+ // Support legacy choiceState implementation
159
+ }
160
+ /* istanbul ignore if - props.values is deprecated */
161
+ const {values} = props;
162
+ if (values) {
163
+ let noneOfTheAboveIndex = null;
164
+ let noneOfTheAboveSelected = false;
165
+
166
+ const choicesSelected = [...values];
167
+ const countChoices = props.countChoices;
168
+ const numCorrect = props.numCorrect;
169
+ const valuesLength = values.length;
170
+
171
+ for (let i = 0; i < valuesLength; i++) {
172
+ const index = props.choices[i].originalIndex;
173
+ choicesSelected[index] = values[i];
174
+
175
+ if (props.choices[i].isNoneOfTheAbove) {
176
+ noneOfTheAboveIndex = index;
177
+ if (choicesSelected[i]) {
178
+ noneOfTheAboveSelected = true;
179
+ }
180
+ }
181
+ }
182
+ return {
183
+ choicesSelected,
184
+ noneOfTheAboveIndex,
185
+ noneOfTheAboveSelected,
186
+ countChoices,
187
+ numCorrect,
188
+ };
189
+ }
190
+ // Nothing checked
191
+ return {
192
+ choicesSelected: props.choices.map(() => false),
193
+ };
194
+ }
195
+
69
196
  _renderRenderer: (content?: string) => React.Node = (
70
197
  content?: string = "",
71
198
  ) => {
@@ -382,134 +509,6 @@ class Radio extends React.Component<Props> {
382
509
  />
383
510
  );
384
511
  }
385
-
386
- static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
387
- const numSelected = userInput.choicesSelected.reduce(
388
- (sum, selected) => {
389
- return sum + (selected ? 1 : 0);
390
- },
391
- 0,
392
- );
393
-
394
- if (numSelected === 0) {
395
- return {
396
- type: "invalid",
397
- message: null,
398
- };
399
- }
400
-
401
- if (
402
- userInput.numCorrect &&
403
- userInput.numCorrect > 1 &&
404
- numSelected !== userInput.numCorrect
405
- ) {
406
- return {
407
- type: "invalid",
408
- message: i18n._("Please choose the correct number of answers."),
409
- };
410
- // If NOTA and some other answer are checked, ...
411
- }
412
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
413
- return {
414
- type: "invalid",
415
- message: i18n._(
416
- "'None of the above' may not be selected " +
417
- "when other answers are selected.",
418
- ),
419
- };
420
- }
421
-
422
- const correct = userInput.choicesSelected.every((selected, i) => {
423
- let isCorrect;
424
- if (userInput.noneOfTheAboveIndex === i) {
425
- isCorrect = rubric.choices.every((choice, j) => {
426
- return i === j || !choice.correct;
427
- });
428
- } else {
429
- isCorrect = !!rubric.choices[i].correct;
430
- }
431
- return isCorrect === selected;
432
- });
433
-
434
- return {
435
- type: "points",
436
- earned: correct ? 1 : 0,
437
- total: 1,
438
- message: null,
439
- };
440
- }
441
-
442
- static getUserInputFromProps(props: Props): UserInput {
443
- // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
444
- // future timeline implementers: this used to be {value: i} before
445
- // multiple select was added)
446
- if (props.choiceStates) {
447
- let noneOfTheAboveIndex = null;
448
- let noneOfTheAboveSelected = false;
449
-
450
- const choiceStates = props.choiceStates;
451
- const choicesSelected = choiceStates.map(() => false);
452
- const countChoices = props.countChoices;
453
- const numCorrect = props.numCorrect;
454
-
455
- for (let i = 0; i < choicesSelected.length; i++) {
456
- const index = props.choices[i].originalIndex;
457
-
458
- choicesSelected[index] = choiceStates[i].selected;
459
-
460
- if (props.choices[i].isNoneOfTheAbove) {
461
- noneOfTheAboveIndex = index;
462
-
463
- if (choicesSelected[i]) {
464
- noneOfTheAboveSelected = true;
465
- }
466
- }
467
- }
468
-
469
- return {
470
- countChoices,
471
- choicesSelected,
472
- numCorrect,
473
- noneOfTheAboveIndex,
474
- noneOfTheAboveSelected,
475
- };
476
- // Support legacy choiceState implementation
477
- }
478
- /* istanbul ignore if - props.values is deprecated */
479
- const {values} = props;
480
- if (values) {
481
- let noneOfTheAboveIndex = null;
482
- let noneOfTheAboveSelected = false;
483
-
484
- const choicesSelected = [...values];
485
- const countChoices = props.countChoices;
486
- const numCorrect = props.numCorrect;
487
- const valuesLength = values.length;
488
-
489
- for (let i = 0; i < valuesLength; i++) {
490
- const index = props.choices[i].originalIndex;
491
- choicesSelected[index] = values[i];
492
-
493
- if (props.choices[i].isNoneOfTheAbove) {
494
- noneOfTheAboveIndex = index;
495
- if (choicesSelected[i]) {
496
- noneOfTheAboveSelected = true;
497
- }
498
- }
499
- }
500
- return {
501
- choicesSelected,
502
- noneOfTheAboveIndex,
503
- noneOfTheAboveSelected,
504
- countChoices,
505
- numCorrect,
506
- };
507
- }
508
- // Nothing checked
509
- return {
510
- choicesSelected: props.choices.map(() => false),
511
- };
512
- }
513
512
  }
514
513
 
515
514
  export default Radio;
@@ -1,3 +0,0 @@
1
- # `NewTooltip`: KA tooltip component
2
-
3
- This is a wrapper around [Wonderblock's popover component](https://github.com/Khan/wonder-blocks). It is API compatiable with Perseus and remains for compatiability and legacy reasons. If you are anyone but the Perseus group at Khan Academy please use the wonderblocks component directly.