@khanacademy/perseus 1.2.0 → 1.3.1

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 (29) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/es/index.css +3 -3
  3. package/dist/es/index.js +145 -179
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +3 -3
  6. package/dist/index.js +143 -177
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/index.js +1 -0
  10. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +21 -11
  11. package/src/styles/widgets/passage.less +7 -3
  12. package/src/widgets/__stories__/passage-ref.stories.jsx +6 -2
  13. package/src/widgets/__stories__/passage.stories.jsx +9 -1
  14. package/src/widgets/__stories__/radio.stories.jsx +11 -0
  15. package/src/widgets/__testdata__/passage-ref_testdata.js +62 -0
  16. package/src/widgets/__testdata__/passage_testdata.js +25 -0
  17. package/src/widgets/__testdata__/radio_testdata.js +52 -0
  18. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +21 -11
  19. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +18 -18
  20. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +36 -36
  21. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +221 -117
  22. package/src/widgets/__tests__/passage_test.jsx +6 -8
  23. package/src/widgets/__tests__/radio_test.jsx +3 -1
  24. package/src/widgets/passage.jsx +12 -41
  25. package/src/widgets/radio/__stories__/sat-styles.stories.jsx +191 -0
  26. package/src/widgets/radio/base-radio.jsx +1 -2
  27. package/src/widgets/radio/choice.jsx +4 -10
  28. package/src/widgets/radio/widget.jsx +128 -129
  29. package/src/widgets/passage/get-line-height-for-node.js +0 -9
package/dist/index.css CHANGED
@@ -3070,11 +3070,11 @@ body.mobile .perseus-matrix .math-output {
3070
3070
  margin: 0 0 10px;
3071
3071
  }
3072
3072
  .perseus-widget-passage-container .passage-text div.paragraph {
3073
- font-family: "Noto Serif", serif;
3074
3073
  font-weight: 400;
3075
- font-size: 20px;
3076
- line-height: 24px;
3074
+ font-size: 16px;
3075
+ line-height: 20px;
3077
3076
  text-indent: 20px;
3077
+ margin: 0;
3078
3078
  }
3079
3079
  .perseus-widget-passage-container .passage-text div.paragraph span {
3080
3080
  text-indent: 0;
package/dist/index.js CHANGED
@@ -25242,10 +25242,6 @@ const styles$w = aphrodite.StyleSheet.create({
25242
25242
  }
25243
25243
  });
25244
25244
 
25245
- const getLineHeightForNode = (line1, line2) => {
25246
- return $__default["default"](line2).offset().top - $__default["default"](line1).offset().top;
25247
- };
25248
-
25249
25245
  function getInitialParseState() {
25250
25246
  return {
25251
25247
  currentRef: [],
@@ -25613,65 +25609,40 @@ var PassageMarkdown = {
25613
25609
  getInitialParseState
25614
25610
  };
25615
25611
 
25616
- // A fake paragraph to measure the line height of the passage. In CSS we always
25617
- // set the line height to 22 pixels, but when using the browser zoom feature,
25618
- // the line height often ends up being a fractional number of pixels close to
25619
- // 22 pixels.
25612
+ // A fake paragraph to measure the line height of the passage,
25613
+ // so we can adapt to browser zoom
25620
25614
  class LineHeightMeasurer extends React__namespace.Component {
25621
25615
  constructor() {
25622
25616
  super(...arguments);
25623
25617
 
25624
- _defineProperty(this, "_cachedLineHeight", void 0);
25625
-
25626
- _defineProperty(this, "body", void 0);
25627
-
25628
- _defineProperty(this, "end", void 0);
25618
+ _defineProperty(this, "_line", void 0);
25629
25619
  }
25630
25620
 
25631
25621
  measureLineHeight() {
25632
- if (typeof this._cachedLineHeight !== "number") {
25633
- this.forceMeasureLineHeight();
25622
+ if (!this._line) {
25623
+ return 0;
25634
25624
  }
25635
25625
 
25636
- return this._cachedLineHeight;
25637
- }
25638
-
25639
- forceMeasureLineHeight() {
25640
- const body = this.body;
25641
- const end = this.end;
25642
-
25643
- if (!body || !end) {
25644
- return;
25645
- } // Add some text which magically fills an entire line.
25646
-
25647
-
25648
- body.textContent = " \u0080"; // Now, the line height is the difference between the top of the
25649
- // second line and the top of the first line.
25650
-
25651
- this._cachedLineHeight = getLineHeightForNode(body, end); // Clear out the first line so it doesn't overlap the passage.
25652
-
25653
- body.textContent = "";
25626
+ return this._line.clientHeight;
25654
25627
  }
25655
25628
 
25656
25629
  render() {
25657
25630
  return /*#__PURE__*/React__namespace.createElement("div", {
25658
- className: aphrodite.css(styles$v.measurer)
25659
- }, /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement("div", {
25660
- ref: ref => this.body = ref,
25631
+ className: aphrodite.css(styles$v.measurer),
25632
+ "aria-hidden": "true"
25633
+ }, /*#__PURE__*/React__namespace.createElement("div", {
25634
+ ref: ref => this._line = ref,
25661
25635
  className: "paragraph"
25662
- }), /*#__PURE__*/React__namespace.createElement("div", {
25663
- ref: ref => this.end = ref
25664
- })));
25636
+ }, "Line Height Measurement"));
25665
25637
  }
25666
25638
 
25667
25639
  }
25668
-
25669
25640
  const styles$v = aphrodite.StyleSheet.create({
25670
25641
  measurer: {
25671
25642
  position: "absolute",
25672
- width: "100%",
25673
25643
  top: 0,
25674
25644
  left: 0,
25645
+ visibility: "hidden",
25675
25646
  // keep from blocking text selection
25676
25647
  zIndex: -1
25677
25648
  }
@@ -25718,7 +25689,7 @@ class Passage extends React__namespace.Component {
25718
25689
  // listen for resize events.
25719
25690
 
25720
25691
 
25721
- (_this$_lineHeightMeas = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas === void 0 ? void 0 : _this$_lineHeightMeas.forceMeasureLineHeight();
25692
+ (_this$_lineHeightMeas = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas === void 0 ? void 0 : _this$_lineHeightMeas.measureLineHeight();
25722
25693
 
25723
25694
  this._updateState();
25724
25695
  }, 500);
@@ -27273,7 +27244,7 @@ function Choice(props) {
27273
27244
 
27274
27245
  const satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
27275
27246
  const satIncorrectChecked = apiOptions.satStyling && reviewMode && !correct && checked;
27276
- const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!apiOptions.satStyling && styles$o.description, apiOptions.satStyling && styles$o.satDescription, satCorrectChoice && styles$o.satDescriptionCorrect, satCorrectChoice && checked && styles$o.satDescriptionCorrectChecked, satIncorrectChecked && styles$o.satDescriptionIncorrectChecked));
27247
+ const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!apiOptions.satStyling && styles$o.description, apiOptions.satStyling && styles$o.satDescription, satCorrectChoice && checked && styles$o.satDescriptionCorrectChecked, satIncorrectChecked && styles$o.satDescriptionIncorrectChecked));
27277
27248
  const rationaleClassName = classNames__default["default"]("perseus-radio-rationale-content", aphrodite.css(styles$o.rationale, !apiOptions.satStyling && styles$o.nonSatRationale, apiOptions.satStyling && styles$o.satReviewRationale)); // We want to show the choices as dimmed out when the choices are
27278
27249
  // disabled. However, we don't want to do this in the SAT product and
27279
27250
  // we also don't want to do this when we're in review mode in the
@@ -27287,12 +27258,12 @@ function Choice(props) {
27287
27258
  style: {
27288
27259
  dispay: "flex",
27289
27260
  flexDirection: "column"
27290
- }
27261
+ },
27262
+ className: descriptionClassName
27291
27263
  }, /*#__PURE__*/React__namespace.createElement("div", {
27292
27264
  style: {
27293
27265
  display: "flex",
27294
27266
  flexDirection: "row",
27295
- width: "100%",
27296
27267
  opacity: showDimmed ? 0.5 : 1.0
27297
27268
  }
27298
27269
  }, /*#__PURE__*/React__namespace.createElement("div", {
@@ -27311,7 +27282,8 @@ function Choice(props) {
27311
27282
  },
27312
27283
  onChange: () => {},
27313
27284
  disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
27314
- tabIndex: -1
27285
+ tabIndex: -1,
27286
+ className: ClassNames.RADIO.OPTION_CONTENT
27315
27287
  }), /*#__PURE__*/React__namespace.createElement("label", {
27316
27288
  htmlFor: choiceId
27317
27289
  }, a11yText, " \xA0 ", content)), /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
@@ -27323,7 +27295,6 @@ function Choice(props) {
27323
27295
  crossedOut: false
27324
27296
  });
27325
27297
  },
27326
- className: descriptionClassName,
27327
27298
  disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
27328
27299
  style: {
27329
27300
  flex: 1
@@ -27456,16 +27427,12 @@ const styles$o = aphrodite.StyleSheet.create({
27456
27427
  boxSizing: "border-box",
27457
27428
  cursor: "pointer",
27458
27429
  marginLeft: 0,
27459
- padding: "17px 14px"
27460
- },
27461
- satDescriptionCorrect: {
27462
- color: satCorrectColor
27430
+ padding: "17px 14px 17px 0"
27463
27431
  },
27464
27432
  satDescriptionCorrectChecked: {
27465
27433
  backgroundColor: satCorrectBackgroundColor
27466
27434
  },
27467
27435
  satDescriptionIncorrectChecked: {
27468
- color: satIncorrectColor,
27469
27436
  backgroundColor: satIncorrectBackgroundColor
27470
27437
  },
27471
27438
  rationale: {
@@ -27815,8 +27782,7 @@ const styles$n = aphrodite.StyleSheet.create({
27815
27782
  }
27816
27783
  },
27817
27784
  radio: {
27818
- // Avoid centering
27819
- width: "100%"
27785
+ padding: 0
27820
27786
  },
27821
27787
  responsiveRadioContainer: {
27822
27788
  borderBottom: "1px solid ".concat(radioBorderColor$1),
@@ -28046,6 +28012,129 @@ class Radio$1 extends React__namespace.Component {
28046
28012
  });
28047
28013
  }
28048
28014
 
28015
+ static validate(userInput, rubric) {
28016
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
28017
+ return sum + (selected ? 1 : 0);
28018
+ }, 0);
28019
+
28020
+ if (numSelected === 0) {
28021
+ return {
28022
+ type: "invalid",
28023
+ message: null
28024
+ };
28025
+ }
28026
+
28027
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
28028
+ return {
28029
+ type: "invalid",
28030
+ message: i18n__namespace._("Please choose the correct number of answers.")
28031
+ }; // If NOTA and some other answer are checked, ...
28032
+ }
28033
+
28034
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
28035
+ return {
28036
+ type: "invalid",
28037
+ message: i18n__namespace._("'None of the above' may not be selected " + "when other answers are selected.")
28038
+ };
28039
+ }
28040
+
28041
+ const correct = userInput.choicesSelected.every((selected, i) => {
28042
+ let isCorrect;
28043
+
28044
+ if (userInput.noneOfTheAboveIndex === i) {
28045
+ isCorrect = rubric.choices.every((choice, j) => {
28046
+ return i === j || !choice.correct;
28047
+ });
28048
+ } else {
28049
+ isCorrect = !!rubric.choices[i].correct;
28050
+ }
28051
+
28052
+ return isCorrect === selected;
28053
+ });
28054
+ return {
28055
+ type: "points",
28056
+ earned: correct ? 1 : 0,
28057
+ total: 1,
28058
+ message: null
28059
+ };
28060
+ }
28061
+
28062
+ static getUserInputFromProps(props) {
28063
+ // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
28064
+ // future timeline implementers: this used to be {value: i} before
28065
+ // multiple select was added)
28066
+ if (props.choiceStates) {
28067
+ let noneOfTheAboveIndex = null;
28068
+ let noneOfTheAboveSelected = false;
28069
+ const choiceStates = props.choiceStates;
28070
+ const choicesSelected = choiceStates.map(() => false);
28071
+ const countChoices = props.countChoices;
28072
+ const numCorrect = props.numCorrect;
28073
+
28074
+ for (let i = 0; i < choicesSelected.length; i++) {
28075
+ const index = props.choices[i].originalIndex;
28076
+ choicesSelected[index] = choiceStates[i].selected;
28077
+
28078
+ if (props.choices[i].isNoneOfTheAbove) {
28079
+ noneOfTheAboveIndex = index;
28080
+
28081
+ if (choicesSelected[i]) {
28082
+ noneOfTheAboveSelected = true;
28083
+ }
28084
+ }
28085
+ }
28086
+
28087
+ return {
28088
+ countChoices,
28089
+ choicesSelected,
28090
+ numCorrect,
28091
+ noneOfTheAboveIndex,
28092
+ noneOfTheAboveSelected
28093
+ }; // Support legacy choiceState implementation
28094
+ }
28095
+ /* istanbul ignore if - props.values is deprecated */
28096
+
28097
+
28098
+ const {
28099
+ values
28100
+ } = props;
28101
+
28102
+ if (values) {
28103
+ let noneOfTheAboveIndex = null;
28104
+ let noneOfTheAboveSelected = false;
28105
+ const choicesSelected = [...values];
28106
+ const countChoices = props.countChoices;
28107
+ const numCorrect = props.numCorrect;
28108
+ const valuesLength = values.length;
28109
+
28110
+ for (let i = 0; i < valuesLength; i++) {
28111
+ const index = props.choices[i].originalIndex;
28112
+ choicesSelected[index] = values[i];
28113
+
28114
+ if (props.choices[i].isNoneOfTheAbove) {
28115
+ noneOfTheAboveIndex = index;
28116
+
28117
+ if (choicesSelected[i]) {
28118
+ noneOfTheAboveSelected = true;
28119
+ }
28120
+ }
28121
+ }
28122
+
28123
+ return {
28124
+ choicesSelected,
28125
+ noneOfTheAboveIndex,
28126
+ noneOfTheAboveSelected,
28127
+ countChoices,
28128
+ numCorrect
28129
+ };
28130
+ } // Nothing checked
28131
+
28132
+
28133
+ return {
28134
+ choicesSelected: props.choices.map(() => false)
28135
+ };
28136
+ }
28137
+
28049
28138
  // TODO(LP-10672): I think this might be unused right now. I can't find anywhere
28050
28139
  // that we pass a value to `.focus()` and it seems to have been used for
28051
28140
  // adding hints when editing.
@@ -28176,129 +28265,6 @@ class Radio$1 extends React__namespace.Component {
28176
28265
  });
28177
28266
  }
28178
28267
 
28179
- static validate(userInput, rubric) {
28180
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
28181
- return sum + (selected ? 1 : 0);
28182
- }, 0);
28183
-
28184
- if (numSelected === 0) {
28185
- return {
28186
- type: "invalid",
28187
- message: null
28188
- };
28189
- }
28190
-
28191
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
28192
- return {
28193
- type: "invalid",
28194
- message: i18n__namespace._("Please choose the correct number of answers.")
28195
- }; // If NOTA and some other answer are checked, ...
28196
- }
28197
-
28198
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
28199
- return {
28200
- type: "invalid",
28201
- message: i18n__namespace._("'None of the above' may not be selected " + "when other answers are selected.")
28202
- };
28203
- }
28204
-
28205
- const correct = userInput.choicesSelected.every((selected, i) => {
28206
- let isCorrect;
28207
-
28208
- if (userInput.noneOfTheAboveIndex === i) {
28209
- isCorrect = rubric.choices.every((choice, j) => {
28210
- return i === j || !choice.correct;
28211
- });
28212
- } else {
28213
- isCorrect = !!rubric.choices[i].correct;
28214
- }
28215
-
28216
- return isCorrect === selected;
28217
- });
28218
- return {
28219
- type: "points",
28220
- earned: correct ? 1 : 0,
28221
- total: 1,
28222
- message: null
28223
- };
28224
- }
28225
-
28226
- static getUserInputFromProps(props) {
28227
- // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
28228
- // future timeline implementers: this used to be {value: i} before
28229
- // multiple select was added)
28230
- if (props.choiceStates) {
28231
- let noneOfTheAboveIndex = null;
28232
- let noneOfTheAboveSelected = false;
28233
- const choiceStates = props.choiceStates;
28234
- const choicesSelected = choiceStates.map(() => false);
28235
- const countChoices = props.countChoices;
28236
- const numCorrect = props.numCorrect;
28237
-
28238
- for (let i = 0; i < choicesSelected.length; i++) {
28239
- const index = props.choices[i].originalIndex;
28240
- choicesSelected[index] = choiceStates[i].selected;
28241
-
28242
- if (props.choices[i].isNoneOfTheAbove) {
28243
- noneOfTheAboveIndex = index;
28244
-
28245
- if (choicesSelected[i]) {
28246
- noneOfTheAboveSelected = true;
28247
- }
28248
- }
28249
- }
28250
-
28251
- return {
28252
- countChoices,
28253
- choicesSelected,
28254
- numCorrect,
28255
- noneOfTheAboveIndex,
28256
- noneOfTheAboveSelected
28257
- }; // Support legacy choiceState implementation
28258
- }
28259
- /* istanbul ignore if - props.values is deprecated */
28260
-
28261
-
28262
- const {
28263
- values
28264
- } = props;
28265
-
28266
- if (values) {
28267
- let noneOfTheAboveIndex = null;
28268
- let noneOfTheAboveSelected = false;
28269
- const choicesSelected = [...values];
28270
- const countChoices = props.countChoices;
28271
- const numCorrect = props.numCorrect;
28272
- const valuesLength = values.length;
28273
-
28274
- for (let i = 0; i < valuesLength; i++) {
28275
- const index = props.choices[i].originalIndex;
28276
- choicesSelected[index] = values[i];
28277
-
28278
- if (props.choices[i].isNoneOfTheAbove) {
28279
- noneOfTheAboveIndex = index;
28280
-
28281
- if (choicesSelected[i]) {
28282
- noneOfTheAboveSelected = true;
28283
- }
28284
- }
28285
- }
28286
-
28287
- return {
28288
- choicesSelected,
28289
- noneOfTheAboveIndex,
28290
- noneOfTheAboveSelected,
28291
- countChoices,
28292
- numCorrect
28293
- };
28294
- } // Nothing checked
28295
-
28296
-
28297
- return {
28298
- choicesSelected: props.choices.map(() => false)
28299
- };
28300
- }
28301
-
28302
28268
  }
28303
28269
 
28304
28270
  _defineProperty(Radio$1, "defaultProps", {