@khanacademy/perseus 0.4.3 → 1.1.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 (71) hide show
  1. package/.eslintrc.js +6 -1
  2. package/CHANGELOG.md +45 -0
  3. package/dist/es/index.css +15 -8
  4. package/dist/es/index.js +1214 -1404
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +15 -8
  7. package/dist/index.js +1217 -1411
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +0 -4
  10. package/package.json +9 -8
  11. package/src/components/number-input.jsx +2 -2
  12. package/src/components/simple-keypad-input.jsx +18 -21
  13. package/src/index.js +1 -1
  14. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
  15. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  16. package/src/perseus-api.jsx +10 -36
  17. package/src/perseus-types.js +18 -145
  18. package/src/styles/constants.js +0 -2
  19. package/src/styles/perseus-renderer.less +8 -2
  20. package/src/styles/variables.less +0 -2
  21. package/src/styles/widgets/passage.less +9 -5
  22. package/src/types.js +18 -35
  23. package/src/util/answer-types.js +2 -3
  24. package/src/util/math.js +3 -10
  25. package/src/util.js +0 -6
  26. package/src/version.js +1 -1
  27. package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
  28. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  29. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  30. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
  31. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
  32. package/src/widgets/__tests__/group_test.jsx +4 -4
  33. package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
  34. package/src/widgets/__tests__/passage_test.jsx +81 -0
  35. package/src/widgets/__tests__/radio/base-radio_test.jsx +291 -41
  36. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  37. package/src/widgets/__tests__/radio/choice_test.jsx +302 -0
  38. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  39. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  40. package/src/widgets/__tests__/radio_test.jsx +39 -15
  41. package/src/widgets/definition.jsx +1 -1
  42. package/src/widgets/group.jsx +84 -102
  43. package/src/widgets/input-number.jsx +6 -1
  44. package/src/widgets/numeric-input.jsx +87 -80
  45. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  46. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  47. package/src/widgets/passage/passage-markdown.jsx +176 -120
  48. package/src/widgets/passage.jsx +48 -46
  49. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  50. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  51. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  52. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  53. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  54. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  55. package/src/widgets/radio/base-radio.jsx +485 -556
  56. package/src/widgets/radio/choice-a11y-text.js +62 -0
  57. package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
  58. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  59. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
  60. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -0
  61. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  62. package/src/widgets/radio/choice-none-above.jsx +49 -40
  63. package/src/widgets/radio/choice.jsx +319 -347
  64. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  65. package/src/widgets/radio/focus-ring.jsx +29 -22
  66. package/src/widgets/radio/option-status.jsx +58 -36
  67. package/src/widgets/radio/widget.jsx +181 -136
  68. package/src/widgets/radio.jsx +16 -17
  69. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  70. package/src/widgets/radio/choice-icon.jsx +0 -469
  71. package/src/widgets/radio/cross-out-button.jsx +0 -134
package/dist/index.js CHANGED
@@ -73,7 +73,7 @@ var Link__default = /*#__PURE__*/_interopDefaultLegacy(Link);
73
73
 
74
74
  var version = {
75
75
  apiVersion: {
76
- major: 11,
76
+ major: 12,
77
77
  minor: 0
78
78
  },
79
79
  itemDataVersion: {
@@ -438,38 +438,9 @@ const ApiOptions = {
438
438
  // values. For now, though, you must either specify all fields
439
439
  // of `styling`, or omit the `styling` option entirely.
440
440
  styling: PropTypes__default["default"].shape({
441
- // Which version of radio widget styles to use in non-SAT
442
- // contexts.
443
- //
444
- // "legacy" was the version of the widget display after XOM but
445
- // before we started adding MCR styles. It doesn't have support
446
- // for rationales. It has since been removed.
447
- //
448
- // "intermediate" is a design which adds several new additions
449
- // to the "legacy" styles such as:
450
- // 1. Using the XOM "desktop" styles (with a visible check icon
451
- // and lines in between choices) on mobile devices.
452
- // 2. Designs for rationales
453
- // 3. Using the single-select styles for multi-select styles
454
- //
455
- // "final" is a design which will build off of the
456
- // "intermediate" designs and adds some improved designs as well
457
- // as:
458
- // 1. a/b/c/d/etc. letters inside of the prompt check box
459
- // 2. New iconography and styles to indicate choice correctness
460
- //
461
- // The "legacy" and "intermediate" designs will be A/B tested
462
- // against each other to ensure that its changes don't cause
463
- // problems due to the new designs. Once the "intermediate"
464
- // designs are finished, they will be A/B tested against the
465
- // "final" designs.
466
- //
467
- // If no flag is provided, "legacy" styles will be shown.
468
- //
469
- // TODO(emily): Remove this by Aug 1, 2017, at which point all
470
- // callsites should have been switched to using the "final"
471
- // designs.
472
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"])
441
+ // TODO(Nicole): Remove this after landing wonderblock-ifying
442
+ // the radio widget
443
+ primaryProductColor: PropTypes__default["default"].string
473
444
  }),
474
445
  // The color used for the hint progress indicator (eg. 1 / 3)
475
446
  hintProgressColor: PropTypes__default["default"].string,
@@ -520,9 +491,7 @@ const ApiOptions = {
520
491
  },
521
492
  setDrawingAreaAvailable: function () {},
522
493
  useDraftEditor: false,
523
- styling: {
524
- radioStyleVersion: "final"
525
- },
494
+ styling: {},
526
495
  canScrollPage: false,
527
496
  inModal: false,
528
497
  crossOutEnabled: false,
@@ -1448,8 +1417,7 @@ const KhanAnswerTypes = {
1448
1417
  */
1449
1418
  number: {
1450
1419
  convertToPredicate: function (correctAnswer, options) {
1451
- // TODO(alpert): Don't think this $.trim is necessary
1452
- const correctFloat = parseFloat($__default["default"].trim(correctAnswer));
1420
+ const correctFloat = parseFloat(correctAnswer);
1453
1421
  return [function (guess, maxError) {
1454
1422
  return Math.abs(guess - correctFloat) < maxError;
1455
1423
  }, $__default["default"].extend({}, options, {
@@ -1643,16 +1611,10 @@ function inputPathsEqual(a, b) {
1643
1611
  return a.length === b.length && a.every((item, index) => {
1644
1612
  return b[index] === item;
1645
1613
  });
1646
- } // Keep `rWidgetRule` in sync with `WIDGET_RE`
1647
- // in content_internal/perseus_traversal.py
1648
- // sync-start:widget-id-regex 1271666319 services/content-editing/perseus/translation/content_traversal.go
1649
- // sync-start:widget-id-regex 680240484 services/static/javascript/perseus-merged-editor-package/editor.jsx
1650
- // sync-start:widget-id-regex 1400368396 content_internal/perseus_traversal.py
1651
-
1614
+ }
1652
1615
 
1653
1616
  const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
1654
- const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/; // sync-end:widget-id-regex
1655
-
1617
+ const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
1656
1618
  const rWidgetParts = new RegExp(rWidgetRule.source + "$");
1657
1619
  const snowman = "\u2603";
1658
1620
  const noScore = {
@@ -3085,11 +3047,10 @@ const warningColor = "#f86700";
3085
3047
  const warningColorHover = "#df5c00";
3086
3048
  const warningColorActive = "#c75300";
3087
3049
  const publishBlockingErrorColor = "#be2612";
3088
- const radioBorderColor$2 = "#BABEC2";
3089
- const checkedColor$1 = "#71B307"; // sync-start:article-max-width 986452807 services/static/javascript/perseus-all-package/styles/variables.less
3090
-
3050
+ const radioBorderColor$1 = "#BABEC2";
3051
+ const checkedColor$1 = "#71B307";
3091
3052
  const articleMaxWidthInPx = 688;
3092
- const articleMaxWidthTableInPx = 512; // sync-end:article-max-width
3053
+ const articleMaxWidthTableInPx = 512;
3093
3054
 
3094
3055
  var constants$1 = /*#__PURE__*/Object.freeze({
3095
3056
  __proto__: null,
@@ -3154,7 +3115,7 @@ var constants$1 = /*#__PURE__*/Object.freeze({
3154
3115
  warningColorHover: warningColorHover,
3155
3116
  warningColorActive: warningColorActive,
3156
3117
  publishBlockingErrorColor: publishBlockingErrorColor,
3157
- radioBorderColor: radioBorderColor$2,
3118
+ radioBorderColor: radioBorderColor$1,
3158
3119
  checkedColor: checkedColor$1,
3159
3120
  articleMaxWidthInPx: articleMaxWidthInPx,
3160
3121
  articleMaxWidthTableInPx: articleMaxWidthTableInPx
@@ -17888,17 +17849,17 @@ class Lint extends React__namespace.Component {
17888
17849
  let warningTextStyle;
17889
17850
 
17890
17851
  if (this.props.severity === 1) {
17891
- severityStyle = styles$w.indicatorError;
17852
+ severityStyle = styles$A.indicatorError;
17892
17853
  warningText = "Error";
17893
- warningTextStyle = styles$w.publishBlockingError;
17854
+ warningTextStyle = styles$A.publishBlockingError;
17894
17855
  } else if (this.props.severity === 2) {
17895
- severityStyle = styles$w.indicatorWarning;
17856
+ severityStyle = styles$A.indicatorWarning;
17896
17857
  warningText = "Warning";
17897
- warningTextStyle = styles$w.warning;
17858
+ warningTextStyle = styles$A.warning;
17898
17859
  } else {
17899
- severityStyle = styles$w.indicatorGuideline;
17860
+ severityStyle = styles$A.indicatorGuideline;
17900
17861
  warningText = "Recommendation";
17901
- warningTextStyle = styles$w.warning;
17862
+ warningTextStyle = styles$A.warning;
17902
17863
  }
17903
17864
 
17904
17865
  return /*#__PURE__*/React__namespace.createElement("a", {
@@ -17906,16 +17867,16 @@ class Lint extends React__namespace.Component {
17906
17867
  target: "lint-help-window",
17907
17868
  className: aphrodite.css(style)
17908
17869
  }, /*#__PURE__*/React__namespace.createElement("span", {
17909
- className: aphrodite.css(styles$w.indicator, severityStyle)
17870
+ className: aphrodite.css(styles$A.indicator, severityStyle)
17910
17871
  }, this.props.severity === 1 && /*#__PURE__*/React__namespace.createElement(InlineIcon, exclamationIcon)), /*#__PURE__*/React__namespace.createElement("div", {
17911
- className: aphrodite.css(styles$w.tooltip, tooltipAbove && styles$w.tooltipAbove)
17872
+ className: aphrodite.css(styles$A.tooltip, tooltipAbove && styles$A.tooltipAbove)
17912
17873
  }, this.props.message.split("\n\n").map((m, i) => /*#__PURE__*/React__namespace.createElement("p", {
17913
17874
  key: i,
17914
- className: aphrodite.css(styles$w.tooltipParagraph)
17875
+ className: aphrodite.css(styles$A.tooltipParagraph)
17915
17876
  }, /*#__PURE__*/React__namespace.createElement("span", {
17916
17877
  className: aphrodite.css(warningTextStyle)
17917
17878
  }, warningText, ":", " "), m)), /*#__PURE__*/React__namespace.createElement("div", {
17918
- className: aphrodite.css(styles$w.tail, tooltipAbove && styles$w.tailAbove)
17879
+ className: aphrodite.css(styles$A.tail, tooltipAbove && styles$A.tailAbove)
17919
17880
  })));
17920
17881
  });
17921
17882
  }
@@ -17963,19 +17924,19 @@ class Lint extends React__namespace.Component {
17963
17924
 
17964
17925
  if (blockHighlight) {
17965
17926
  return /*#__PURE__*/React__namespace.createElement("span", {
17966
- className: aphrodite.css(styles$w.lintContainer, styles$w.lintContainerBlock)
17967
- }, this.renderLink(styles$w.radioWidgetHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17927
+ className: aphrodite.css(styles$A.lintContainer, styles$A.lintContainerBlock)
17928
+ }, this.renderLink(styles$A.radioWidgetHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17968
17929
  }
17969
17930
 
17970
17931
  if (inline) {
17971
17932
  return /*#__PURE__*/React__namespace.createElement("span", {
17972
- className: aphrodite.css(styles$w.lintContainer)
17973
- }, this.renderLink(styles$w.inlineHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17933
+ className: aphrodite.css(styles$A.lintContainer)
17934
+ }, this.renderLink(styles$A.inlineHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17974
17935
  }
17975
17936
 
17976
17937
  return /*#__PURE__*/React__namespace.createElement("div", {
17977
- className: aphrodite.css(styles$w.lintContainer)
17978
- }, this.renderLink(styles$w.hoverTarget), /*#__PURE__*/React__namespace.createElement("div", null, children));
17938
+ className: aphrodite.css(styles$A.lintContainer)
17939
+ }, this.renderLink(styles$A.hoverTarget), /*#__PURE__*/React__namespace.createElement("div", null, children));
17979
17940
  }
17980
17941
 
17981
17942
  } // $FlowFixMe[prop-missing]
@@ -18000,7 +17961,7 @@ _defineProperty(Lint, "propTypes", {
18000
17961
  severity: PropTypes__default["default"].number
18001
17962
  });
18002
17963
 
18003
- const styles$w = aphrodite.StyleSheet.create({
17964
+ const styles$A = aphrodite.StyleSheet.create({
18004
17965
  // This is the class of the outermost element.
18005
17966
  // We use relative positioning so that the lint indicator can be
18006
17967
  // positioned absolutely relative to the position of the linty container.
@@ -19893,7 +19854,7 @@ class Renderer extends React__namespace.Component {
19893
19854
  paddingRight: margin
19894
19855
  };
19895
19856
  const mobileInnerStyle = { ...innerStyle,
19896
- ...styles$v.mobileZoomableParentFix
19857
+ ...styles$z.mobileZoomableParentFix
19897
19858
  };
19898
19859
  return /*#__PURE__*/React__namespace.createElement("div", {
19899
19860
  key: state.key,
@@ -20033,7 +19994,7 @@ class Renderer extends React__namespace.Component {
20033
19994
  paddingRight: 0
20034
19995
  };
20035
19996
  const mobileInnerStyle = { ...innerStyle,
20036
- ...styles$v.mobileZoomableParentFix
19997
+ ...styles$z.mobileZoomableParentFix
20037
19998
  };
20038
19999
  const wrappedOutput = /*#__PURE__*/React__namespace.createElement("div", {
20039
20000
  style: { ...mobileInnerStyle,
@@ -20714,7 +20675,7 @@ _defineProperty(Renderer, "defaultProps", {
20714
20675
  linterContext: PerseusLinter__namespace.linterContextDefault
20715
20676
  });
20716
20677
 
20717
- const styles$v = {
20678
+ const styles$z = {
20718
20679
  mobileZoomableParentFix: {
20719
20680
  // NOTE(abdul): There is an issue where transform animations will
20720
20681
  // cause the Zoomable component to disappear when running on the
@@ -22888,35 +22849,36 @@ _defineProperty(PossibleAnswers, "propTypes", {
22888
22849
  answers: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string)
22889
22850
  });
22890
22851
 
22891
- const SimpleKeypadInput = createReactClass__default["default"]({
22892
- displayName: "SimpleKeypadInput",
22893
- propTypes: {
22894
- keypadElement: mathInput.keypadElementPropType,
22895
- onFocus: PropTypes__default["default"].func,
22896
- value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
22897
- },
22898
- componentDidMount: function () {
22852
+ class SimpleKeypadInput extends React__namespace.Component {
22853
+ constructor() {
22854
+ super(...arguments);
22855
+
22856
+ _defineProperty(this, "_isMounted", false);
22857
+ }
22858
+
22859
+ componentDidMount() {
22899
22860
  // TODO(scottgrant): This is a hack to remove the deprecated call to
22900
22861
  // this.isMounted() but is still considered an anti-pattern.
22901
22862
  this._isMounted = true;
22902
- },
22903
- componentWillUnmount: function () {
22863
+ }
22864
+
22865
+ componentWillUnmount() {
22904
22866
  this._isMounted = false;
22905
- },
22867
+ }
22906
22868
 
22907
22869
  focus() {
22908
22870
  // $FlowFixMe[object-this-reference]
22909
22871
  this.refs.input.focus(); // eslint-disable-line react/no-string-refs
22910
- },
22872
+ }
22911
22873
 
22912
22874
  blur() {
22913
22875
  // $FlowFixMe[object-this-reference]
22914
22876
  this.refs.input.blur(); // eslint-disable-line react/no-string-refs
22915
- },
22877
+ }
22916
22878
 
22917
- getValue: function () {
22879
+ getValue() {
22918
22880
  return this.props.value;
22919
- },
22881
+ }
22920
22882
 
22921
22883
  render() {
22922
22884
  // $FlowFixMe[object-this-reference]
@@ -22953,7 +22915,12 @@ const SimpleKeypadInput = createReactClass__default["default"]({
22953
22915
  }, rest));
22954
22916
  }
22955
22917
 
22956
- });
22918
+ }
22919
+ SimpleKeypadInput.propTypes = {
22920
+ keypadElement: mathInput.keypadElementPropType,
22921
+ onFocus: PropTypes__default["default"].func,
22922
+ value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
22923
+ };
22957
22924
 
22958
22925
  const ParseTex$1 = TexWrangler.parseTex;
22959
22926
  const answerTypes = {
@@ -23208,9 +23175,13 @@ class InputNumber extends React__namespace.Component {
23208
23175
 
23209
23176
  if (rubric.answerType == null) {
23210
23177
  rubric.answerType = "number";
23211
- }
23178
+ } // note(matthewc): this will get immediately parsed again by
23179
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
23180
+ // expected here
23181
+
23212
23182
 
23213
- const val = KhanAnswerTypes.number.createValidatorFunctional(rubric.value, {
23183
+ const stringValue = "".concat(rubric.value);
23184
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
23214
23185
  simplify: rubric.simplify,
23215
23186
  inexact: rubric.inexact || undefined,
23216
23187
  maxError: rubric.maxError,
@@ -23336,6 +23307,8 @@ class NumericInput extends React__namespace.Component {
23336
23307
  constructor() {
23337
23308
  super(...arguments);
23338
23309
 
23310
+ _defineProperty(this, "inputRef", void 0);
23311
+
23339
23312
  _defineProperty(this, "state", {
23340
23313
  // keeps track of the other set of values when switching
23341
23314
  // between 0 and finite solutions
@@ -23354,9 +23327,8 @@ class NumericInput extends React__namespace.Component {
23354
23327
  correct = score.type === "points" && score.earned === score.total;
23355
23328
 
23356
23329
  if (!correct) {
23357
- const correctAnswers = ___default["default"].filter(rubric.answers, answer => answer.status === "correct");
23358
-
23359
- const answerStrings = ___default["default"].map(correctAnswers, answer => {
23330
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23331
+ const answerStrings = correctAnswers.map(answer => {
23360
23332
  // Figure out how this answer is supposed to be
23361
23333
  // displayed
23362
23334
  let format = "decimal";
@@ -23377,7 +23349,6 @@ class NumericInput extends React__namespace.Component {
23377
23349
 
23378
23350
  return answerString;
23379
23351
  });
23380
-
23381
23352
  answerBlurb = /*#__PURE__*/React__namespace.createElement(PossibleAnswers, {
23382
23353
  answers: answerStrings
23383
23354
  });
@@ -23391,7 +23362,7 @@ class NumericInput extends React__namespace.Component {
23391
23362
  var _this$props$answerFor;
23392
23363
 
23393
23364
  // if the set of specified forms are empty, allow all forms
23394
- const forms = ((_this$props$answerFor = this.props.answerForms) === null || _this$props$answerFor === void 0 ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : ___default["default"].map(___default["default"].keys(formExamples), name => {
23365
+ const forms = ((_this$props$answerFor = this.props.answerForms) === null || _this$props$answerFor === void 0 ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
23395
23366
  return {
23396
23367
  name: name,
23397
23368
  simplify: "required"
@@ -23417,8 +23388,7 @@ class NumericInput extends React__namespace.Component {
23417
23388
 
23418
23389
  const answerFormNames = ___default["default"].uniq((_this$props$answerFor3 = this.props.answerForms) === null || _this$props$answerFor3 === void 0 ? void 0 : _this$props$answerFor3.map(form => form.name));
23419
23390
 
23420
- const allFormsAccepted = answerFormNames.length >= ___default["default"].size(formExamples);
23421
-
23391
+ const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
23422
23392
  return !noFormsAccepted && !allFormsAccepted;
23423
23393
  });
23424
23394
 
@@ -23427,20 +23397,22 @@ class NumericInput extends React__namespace.Component {
23427
23397
  });
23428
23398
 
23429
23399
  _defineProperty(this, "focus", () => {
23430
- // eslint-disable-next-line react/no-string-refs
23431
- this.refs.input.focus();
23400
+ var _this$inputRef;
23401
+
23402
+ (_this$inputRef = this.inputRef) === null || _this$inputRef === void 0 ? void 0 : _this$inputRef.focus();
23432
23403
  return true;
23433
23404
  });
23434
23405
 
23435
- _defineProperty(this, "focusInputPath", inputPath => {
23436
- /* istanbul ignore next */
23437
- // eslint-disable-next-line react/no-string-refs
23438
- this.refs.input.focus();
23406
+ _defineProperty(this, "focusInputPath", () => {
23407
+ var _this$inputRef2;
23408
+
23409
+ (_this$inputRef2 = this.inputRef) === null || _this$inputRef2 === void 0 ? void 0 : _this$inputRef2.focus();
23439
23410
  });
23440
23411
 
23441
- _defineProperty(this, "blurInputPath", inputPath => {
23442
- // eslint-disable-next-line react/no-string-refs
23443
- this.refs.input.blur();
23412
+ _defineProperty(this, "blurInputPath", () => {
23413
+ var _this$inputRef3;
23414
+
23415
+ (_this$inputRef3 = this.inputRef) === null || _this$inputRef3 === void 0 ? void 0 : _this$inputRef3.blur();
23444
23416
  });
23445
23417
 
23446
23418
  _defineProperty(this, "getInputPaths", () => {
@@ -23508,9 +23480,8 @@ class NumericInput extends React__namespace.Component {
23508
23480
  }
23509
23481
 
23510
23482
  static getOneCorrectAnswerFromRubric(rubric) {
23511
- const correctAnswers = ___default["default"].filter(rubric.answers, answer => answer.status === "correct");
23512
-
23513
- const answerStrings = ___default["default"].map(correctAnswers, answer => {
23483
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23484
+ const answerStrings = correctAnswers.map(answer => {
23514
23485
  // Figure out how this answer is supposed to be
23515
23486
  // displayed
23516
23487
  let format = "decimal";
@@ -23540,31 +23511,30 @@ class NumericInput extends React__namespace.Component {
23540
23511
  }
23541
23512
 
23542
23513
  static validate(useInput, rubric) {
23543
- const allAnswerForms = ___default["default"].pluck(answerFormButtons, "value");
23544
-
23545
- const createValidator = answer => KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
23546
- message: answer.message,
23547
- simplify: answer.status === "correct" ? answer.simplify : "optional",
23548
- inexact: true,
23549
- // TODO(merlob) backfill / delete
23550
- maxError: answer.maxError,
23551
- forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23552
- }); // We may have received TeX; try to parse it before grading.
23514
+ const allAnswerForms = answerFormButtons.map(e => e["value"]);
23515
+
23516
+ const createValidator = answer => {
23517
+ const stringAnswer = "".concat(answer.value);
23518
+ return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
23519
+ message: answer.message,
23520
+ simplify: answer.status === "correct" ? answer.simplify : "optional",
23521
+ inexact: true,
23522
+ // TODO(merlob) backfill / delete
23523
+ maxError: answer.maxError,
23524
+ forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23525
+ });
23526
+ }; // We may have received TeX; try to parse it before grading.
23553
23527
  // If `currentValue` is not TeX, this should be a no-op.
23554
23528
 
23555
23529
 
23556
23530
  const currentValue = ParseTex(useInput.currentValue);
23557
-
23558
- const correctAnswers = ___default["default"].where(rubric.answers, {
23559
- status: "correct"
23560
- });
23561
-
23531
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23562
23532
  const normalizedAnswerExpected = correctAnswers.every(answer => Math.abs(answer.value) <= 1); // Look through all correct answers for one that matches either
23563
23533
  // precisely or approximately and return the appropriate message:
23564
23534
  // - if precise, return the message that the answer came with
23565
23535
  // - if it needs to be simplified, etc., show that message
23566
23536
 
23567
- let result = ___default["default"].find(___default["default"].map(correctAnswers, answer => {
23537
+ let result = correctAnswers.map(answer => {
23568
23538
  // The coefficient is an attribute of the widget
23569
23539
  let localValue = currentValue;
23570
23540
 
@@ -23578,22 +23548,17 @@ class NumericInput extends React__namespace.Component {
23578
23548
 
23579
23549
  const validate = createValidator(answer);
23580
23550
  return validate(maybeParsePercentInput(localValue, normalizedAnswerExpected));
23581
- }), match => match.correct || match.empty);
23551
+ }).find(match => match.correct || match.empty);
23582
23552
 
23583
23553
  if (!result) {
23584
23554
  // Otherwise, if the guess is not correct
23585
- const otherAnswers = [].concat(___default["default"].where(rubric.answers, {
23586
- status: "ungraded"
23587
- }), ___default["default"].where(rubric.answers, {
23588
- status: "wrong"
23589
- })); // Look through all other answers and if one matches either
23555
+ const otherAnswers = [].concat(rubric.answers.filter(answer => answer.status === "ungraded"), rubric.answers.filter(answer => answer.status === "wrong")); // Look through all other answers and if one matches either
23590
23556
  // precisely or approximately return the answer's message
23591
23557
 
23592
- const match = ___default["default"].find(otherAnswers, answer => {
23558
+ const match = otherAnswers.find(answer => {
23593
23559
  const validate = createValidator(answer);
23594
23560
  return validate(maybeParsePercentInput(currentValue, normalizedAnswerExpected)).correct;
23595
23561
  });
23596
-
23597
23562
  result = {
23598
23563
  empty: match ? match.status === "ungraded" : false,
23599
23564
  correct: match ? match.status === "correct" : false,
@@ -23640,9 +23605,8 @@ class NumericInput extends React__namespace.Component {
23640
23605
 
23641
23606
  if (this.props.apiOptions.customKeypad) {
23642
23607
  // TODO(charlie): Support "Review Mode".
23643
- return maybeRightAlignKeypadInput( /*#__PURE__*/React__namespace.createElement(SimpleKeypadInput // eslint-disable-next-line react/no-string-refs
23644
- , {
23645
- ref: "input",
23608
+ return maybeRightAlignKeypadInput( /*#__PURE__*/React__namespace.createElement(SimpleKeypadInput, {
23609
+ ref: ref => this.inputRef = ref,
23646
23610
  value: this.props.currentValue,
23647
23611
  keypadElement: this.props.keypadElement,
23648
23612
  onChange: this.handleChange,
@@ -23651,9 +23615,8 @@ class NumericInput extends React__namespace.Component {
23651
23615
  }));
23652
23616
  }
23653
23617
 
23654
- const input = /*#__PURE__*/React__namespace.createElement(InputWithExamples // eslint-disable-next-line react/no-string-refs
23655
- , {
23656
- ref: "input",
23618
+ const input = /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
23619
+ ref: ref => this.inputRef = ref,
23657
23620
  value: this.props.currentValue,
23658
23621
  onChange: this.handleChange,
23659
23622
  className: classNames__default["default"](classes),
@@ -23720,7 +23683,7 @@ const unionAnswerForms = function (answerFormsList) {
23720
23683
  // two elements are equal, and returns a list of unique elements. This is
23721
23684
  // just a helper function here, but works generally.
23722
23685
  const uniqueBy = function (list, iteratee) {
23723
- return ___default["default"].reduce(list, (uniqueList, element) => {
23686
+ return list.reduce((uniqueList, element) => {
23724
23687
  // For each element, decide whether it's already in the list of
23725
23688
  // unique items.
23726
23689
  const inList = ___default["default"].find(uniqueList, iteratee.bind(null, element));
@@ -23734,13 +23697,13 @@ const unionAnswerForms = function (answerFormsList) {
23734
23697
  }; // Pull out all of the forms from the different lists.
23735
23698
 
23736
23699
 
23737
- const allForms = ___default["default"].flatten(answerFormsList); // Pull out the unique forms using uniqueBy.
23738
-
23700
+ const allForms = answerFormsList.flat(); // Pull out the unique forms using uniqueBy.
23739
23701
 
23740
23702
  const uniqueForms = uniqueBy(allForms, ___default["default"].isEqual); // Sort them by the order they appear in the `formExamples` list.
23741
23703
 
23704
+ const formExampleKeys = Object.keys(formExamples);
23742
23705
  return ___default["default"].sortBy(uniqueForms, form => {
23743
- return ___default["default"].keys(formExamples).indexOf(form.name);
23706
+ return formExampleKeys.indexOf(form.name);
23744
23707
  });
23745
23708
  };
23746
23709
  // This function checks if the user inputted a percent value, parsing
@@ -23749,7 +23712,7 @@ const unionAnswerForms = function (answerFormsList) {
23749
23712
  // can accept several input forms with or without "%", the decision
23750
23713
  // to parse based on the presence of "%" in the input, is so that we
23751
23714
  // don't accidently scale the user typed value before grading, CP-930.
23752
- const maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23715
+ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
23753
23716
  // If the input value is not a string ending with "%", then there's
23754
23717
  // nothing more to do. The value will be graded as inputted by user.
23755
23718
  if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
@@ -23774,13 +23737,13 @@ const maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23774
23737
 
23775
23738
 
23776
23739
  return value;
23777
- };
23740
+ }
23778
23741
 
23779
23742
  const propsTransform = function (widgetOptions) {
23780
23743
  const rendererProps = ___default["default"].extend(___default["default"].omit(widgetOptions, "answers"), {
23781
23744
  answerForms: unionAnswerForms( // Pull out the name of each form and whether that form has
23782
23745
  // required simplification.
23783
- ___default["default"].map(widgetOptions.answers, answer => {
23746
+ widgetOptions.answers.map(answer => {
23784
23747
  return ___default["default"].map(answer.answerForms, form => {
23785
23748
  return {
23786
23749
  simplify: answer.simplify,
@@ -24016,7 +23979,7 @@ var mediaQueries = {
24016
23979
  const {
24017
23980
  zIndexAboveScratchpad,
24018
23981
  zIndexInteractiveComponent,
24019
- radioBorderColor: radioBorderColor$1,
23982
+ radioBorderColor,
24020
23983
  checkedColor,
24021
23984
  circleSize,
24022
23985
  radioMarginWidth
@@ -24071,7 +24034,7 @@ var sharedStyles = aphrodite.StyleSheet.create({
24071
24034
  },
24072
24035
  backgroundColor: "#fff",
24073
24036
  border: "2px solid #fff",
24074
- boxShadow: "0 0px 0px 1px ".concat(radioBorderColor$1),
24037
+ boxShadow: "0 0px 0px 1px ".concat(radioBorderColor),
24075
24038
  outline: "none",
24076
24039
  boxSizing: "border-box",
24077
24040
  flexShrink: 0,
@@ -24323,35 +24286,65 @@ _defineProperty(Icon, "defaultProps", {
24323
24286
  color: "currentColor"
24324
24287
  });
24325
24288
 
24326
- class FocusRing extends React__namespace.Component {
24327
- render() {
24328
- const borderColor = this.props.visible ? this.props.color : "transparent";
24329
- const style = {
24330
- borderColor: borderColor
24331
- };
24332
- return /*#__PURE__*/React__namespace.createElement("span", {
24333
- className: aphrodite.css(styles$u.ring),
24334
- style: style
24335
- }, this.props.children);
24289
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
24290
+ // There are two pieces of metadata we want to add to each a11yText:
24291
+ // whether the answer was checked/crossed-out/neither, and whether the
24292
+ // answer is correct/incorrect/not-yet-revealed.
24293
+ //
24294
+ // Translation is tricky for cross-product situations like this, so
24295
+ // we've just enumerated all 9 possibilities as separate strings.
24296
+ if (showCorrectness && correct) {
24297
+ if (checked) {
24298
+ return i18n__namespace._("(Choice %(letter)s, Checked, Correct)", {
24299
+ letter
24300
+ });
24301
+ }
24302
+
24303
+ if (crossedOut) {
24304
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Correct)", {
24305
+ letter
24306
+ });
24307
+ }
24308
+
24309
+ return i18n__namespace._("(Choice %(letter)s, Correct)", {
24310
+ letter
24311
+ });
24336
24312
  }
24337
24313
 
24338
- }
24314
+ if (showCorrectness && !correct) {
24315
+ if (checked) {
24316
+ return i18n__namespace._("(Choice %(letter)s, Checked, Incorrect)", {
24317
+ letter
24318
+ });
24319
+ }
24339
24320
 
24340
- _defineProperty(FocusRing, "defaultProps", {
24341
- visible: true,
24342
- color: kaGreen$1
24343
- });
24321
+ if (crossedOut) {
24322
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Incorrect)", {
24323
+ letter
24324
+ });
24325
+ }
24344
24326
 
24345
- const styles$u = aphrodite.StyleSheet.create({
24346
- ring: {
24347
- margin: "auto",
24348
- display: "inline-block",
24349
- borderRadius: "50%",
24350
- borderWidth: 2,
24351
- padding: 2,
24352
- borderStyle: "solid"
24327
+ return i18n__namespace._("(Choice %(letter)s, Incorrect)", {
24328
+ letter
24329
+ });
24353
24330
  }
24354
- });
24331
+
24332
+ if (checked) {
24333
+ return i18n__namespace._("(Choice %(letter)s, Checked)", {
24334
+ letter
24335
+ });
24336
+ }
24337
+
24338
+ if (crossedOut) {
24339
+ return i18n__namespace._("(Choice %(letter)s, Crossed out)", {
24340
+ letter
24341
+ });
24342
+ }
24343
+
24344
+ return i18n__namespace._("(Choice %(letter)s)", {
24345
+ letter
24346
+ });
24347
+ }
24355
24348
 
24356
24349
  /**
24357
24350
  * Given a choice's position in the radio widget, return the corresponding
@@ -24383,179 +24376,57 @@ function getChoiceLetter(pos) {
24383
24376
  return " ";
24384
24377
  }
24385
24378
 
24386
- const SAT_ICON_SIZE = 25;
24387
-
24388
- class SATChoiceIcon extends React__namespace.Component {
24389
- // TODO(amy): figure out a better scheme for specifying these
24390
- // styles that isn't such a pain to grok. See some neat ideas
24391
- // from MDR in https://phabricator.khanacademy.org/D35249.
24392
- constructStyles(reviewMode, correct, checked) {
24393
- let backgroundColor;
24394
- let borderColor = satBlue;
24395
- let color = satBlue;
24396
-
24397
- if (reviewMode) {
24398
- if (correct) {
24399
- borderColor = satCorrectColor;
24400
- color = checked ? white$1 : satCorrectColor;
24401
- backgroundColor = checked ? satCorrectColor : white$1;
24402
- } else if (checked) {
24403
- borderColor = satIncorrectColor;
24404
- color = white$1;
24405
- backgroundColor = satIncorrectColor;
24406
- }
24407
- } else if (checked) {
24408
- color = white$1;
24409
- backgroundColor = satBlue;
24410
- }
24411
-
24412
- return {
24413
- color,
24414
- backgroundColor,
24415
- borderColor
24416
- };
24417
- }
24418
-
24419
- render() {
24420
- const {
24421
- letter,
24422
- a11yText,
24423
- reviewMode,
24424
- checked,
24425
- correct,
24426
- crossedOut
24427
- } = this.props;
24428
- const {
24429
- color,
24430
- backgroundColor,
24431
- borderColor
24432
- } = this.constructStyles(reviewMode, correct, checked);
24433
- return /*#__PURE__*/React__namespace.createElement("div", {
24434
- className: aphrodite.css(styles$t.iconWrapper)
24435
- }, /*#__PURE__*/React__namespace.createElement("div", {
24436
- className: aphrodite.css(styles$t.satCircle),
24437
- style: {
24438
- backgroundColor,
24439
- borderColor
24440
- }
24441
- }), /*#__PURE__*/React__namespace.createElement("div", {
24442
- style: {
24443
- color
24444
- },
24445
- className: aphrodite.css(styles$t.letter)
24446
- }, /*#__PURE__*/React__namespace.createElement("span", {
24447
- className: "perseus-sr-only"
24448
- }, a11yText), /*#__PURE__*/React__namespace.createElement("span", {
24449
- "aria-hidden": "true"
24450
- }, letter)), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24451
- color: borderColor,
24452
- sat: true
24453
- }));
24454
- }
24379
+ /**
24380
+ * Renders a circular selection ring around the child.
24381
+ */
24455
24382
 
24383
+ function FocusRing(props) {
24384
+ const {
24385
+ visible,
24386
+ color,
24387
+ children,
24388
+ multipleSelect
24389
+ } = props;
24390
+ const borderColor = visible ? color : "transparent";
24391
+ const borderRadius = multipleSelect ? 5 : "50%";
24392
+ const style = {
24393
+ borderColor,
24394
+ borderRadius
24395
+ };
24396
+ return /*#__PURE__*/React__namespace.createElement("span", {
24397
+ "data-test-id": "focus-ring",
24398
+ className: aphrodite.css(styles$y.ring),
24399
+ style: style
24400
+ }, children);
24456
24401
  }
24457
24402
 
24458
- const LIBRARY_ICON_SIZE = 24;
24459
-
24460
- class LibraryChoiceIcon extends React__namespace.Component {
24461
- getChoiceInner() {
24462
- const {
24463
- letter,
24464
- showCorrectness,
24465
- correct
24466
- } = this.props;
24467
-
24468
- if (!showCorrectness) {
24469
- return letter;
24470
- }
24471
-
24472
- if (correct) {
24473
- return /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconCheck, {
24474
- style: {
24475
- position: "relative",
24476
- top: -1
24477
- }
24478
- }));
24479
- }
24480
-
24481
- return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
24482
- } // Handle dynamic styling of the multiple choice icon. Most
24483
- // MC icon styles are constant, but we do allow the caller
24484
- // to specify the selected color, and thus must control styles
24485
- // related to the selected state dynamically.
24486
-
24487
-
24488
- getDynamicStyles() {
24489
- const {
24490
- checked,
24491
- showCorrectness,
24492
- pressed,
24493
- primaryProductColor,
24494
- correct,
24495
- transparentBackground
24496
- } = this.props;
24497
-
24498
- if (!showCorrectness && pressed) {
24499
- return {
24500
- borderColor: primaryProductColor,
24501
- color: primaryProductColor,
24502
- backgroundColor: transparentBackground ? "transparent" : white$1
24503
- };
24504
- }
24505
-
24506
- if (checked) {
24507
- // Note: kaGreen is not only the default product color,
24508
- // but also the "correctness" color
24509
- const bg = showCorrectness && correct ? Color__default["default"].green : primaryProductColor;
24510
- return {
24511
- color: white$1,
24512
- backgroundColor: bg,
24513
- borderColor: bg
24514
- };
24515
- }
24516
-
24517
- return {
24518
- borderColor: Color__default["default"].offBlack64,
24519
- color: Color__default["default"].offBlack64
24520
- };
24403
+ FocusRing.defaultProps = {
24404
+ visible: true,
24405
+ color: kaGreen$1,
24406
+ multipleSelect: false
24407
+ };
24408
+ const styles$y = aphrodite.StyleSheet.create({
24409
+ ring: {
24410
+ margin: "auto",
24411
+ display: "inline-block",
24412
+ borderWidth: 2,
24413
+ padding: 2,
24414
+ borderStyle: "solid"
24521
24415
  }
24416
+ });
24522
24417
 
24523
- render() {
24524
- const {
24525
- a11yText,
24526
- checked,
24527
- crossedOut,
24528
- showCorrectness,
24529
- correct,
24530
- focused,
24531
- primaryProductColor,
24532
- previouslyAnswered
24533
- } = this.props;
24534
- const dynamicStyles = this.getDynamicStyles();
24535
- return /*#__PURE__*/React__namespace.createElement("div", {
24536
- className: aphrodite.css(styles$t.iconWrapper)
24537
- }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
24538
- color: primaryProductColor,
24539
- visible: focused
24540
- }, /*#__PURE__*/React__namespace.createElement("div", {
24541
- style: dynamicStyles,
24542
- className: aphrodite.css(styles$t.libraryCircle, showCorrectness && correct && styles$t.libraryCircleCorrect, showCorrectness && !correct && styles$t.libraryCircleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$t.libraryCircleIncorrectAnswered) // used in BaseRadio to check if we actually clicked on the
24543
- // radio icon
24544
- ,
24545
- "data-is-radio-icon": true
24546
- }, /*#__PURE__*/React__namespace.createElement("div", {
24547
- className: "perseus-sr-only"
24548
- }, a11yText), /*#__PURE__*/React__namespace.createElement("div", {
24549
- "aria-hidden": true
24550
- }, this.getChoiceInner()))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24551
- color: dynamicStyles.borderColor
24552
- }));
24418
+ const SAT_ICON_SIZE = 25;
24419
+ const LIBRARY_ICON_SIZE = 24;
24420
+ const styles$x = aphrodite.StyleSheet.create({
24421
+ iconWrapper: {
24422
+ display: "inline-block",
24423
+ position: "relative"
24553
24424
  }
24425
+ });
24554
24426
 
24555
- } // The "cross-out line" SVG is a bit bigger than the library icon, to provide
24427
+ /* Component for rendering a letter icon in radio choice */
24556
24428
  // extra space for the cute rounded line cap.
24557
24429
 
24558
-
24559
24430
  const CROSS_OUT_LINE_PADDING = 2;
24560
24431
  const CROSS_OUT_LINE_SIZE = LIBRARY_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
24561
24432
  const SAT_CROSS_OUT_LINE_SIZE = SAT_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
@@ -24570,7 +24441,7 @@ function CrossOutLine(props) {
24570
24441
  width: crossOutLineSize,
24571
24442
  height: crossOutLineSize,
24572
24443
  viewBox: "0 0 ".concat(crossOutLineSize, " ").concat(crossOutLineSize),
24573
- className: aphrodite.css(styles$t.crossOutLine)
24444
+ className: aphrodite.css(styles$w.crossOutLine)
24574
24445
  }, /*#__PURE__*/React__namespace.createElement("line", {
24575
24446
  // The line stretches from the bottom-left to top-right.
24576
24447
  // We don't quite go to the _very_ corner, because the cute
@@ -24585,151 +24456,124 @@ function CrossOutLine(props) {
24585
24456
  }));
24586
24457
  }
24587
24458
 
24588
- class ChoiceIcon extends React__namespace.Component {
24589
- a11yText(letter) {
24590
- const {
24591
- checked,
24592
- correct,
24593
- crossedOut,
24594
- showCorrectness
24595
- } = this.props; // There are two pieces of metadata we want to add to each a11yText:
24596
- // whether the answer was checked/crossed-out/neither, and whether the
24597
- // answer is correct/incorrect/not-yet-revealed.
24598
- //
24599
- // Translation is tricky for cross-product situations like this, so
24600
- // we've just enumerated all 9 possibilities as separate strings.
24601
-
24602
- if (showCorrectness && correct) {
24603
- if (checked) {
24604
- return i18n__namespace._("(Choice %(letter)s, Checked, Correct)", {
24605
- letter
24606
- });
24607
- }
24608
-
24609
- if (crossedOut) {
24610
- return i18n__namespace._("(Choice %(letter)s, Crossed out, Correct)", {
24611
- letter
24612
- });
24613
- }
24459
+ const styles$w = aphrodite.StyleSheet.create({
24460
+ crossOutLine: {
24461
+ // Center the icon within the container.
24462
+ position: "absolute",
24463
+ top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24464
+ left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24465
+ }
24466
+ });
24614
24467
 
24615
- return i18n__namespace._("(Choice %(letter)s, Correct)", {
24616
- letter
24617
- });
24618
- }
24468
+ function ChoiceInner(props) {
24469
+ const {
24470
+ letter,
24471
+ showCorrectness,
24472
+ correct
24473
+ } = props;
24619
24474
 
24620
- if (showCorrectness && !correct) {
24621
- if (checked) {
24622
- return i18n__namespace._("(Choice %(letter)s, Checked, Incorrect)", {
24623
- letter
24624
- });
24625
- }
24475
+ if (!showCorrectness) {
24476
+ return letter;
24477
+ }
24626
24478
 
24627
- if (crossedOut) {
24628
- return i18n__namespace._("(Choice %(letter)s, Crossed out, Incorrect)", {
24629
- letter
24630
- });
24479
+ if (correct) {
24480
+ return /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconCheck, {
24481
+ style: {
24482
+ position: "relative",
24483
+ top: -1
24631
24484
  }
24632
-
24633
- return i18n__namespace._("(Choice %(letter)s, Incorrect)", {
24634
- letter
24635
- });
24636
- }
24637
-
24638
- if (checked) {
24639
- return i18n__namespace._("(Choice %(letter)s, Checked)", {
24640
- letter
24641
- });
24642
- }
24643
-
24644
- if (crossedOut) {
24645
- return i18n__namespace._("(Choice %(letter)s, Crossed out)", {
24646
- letter
24647
- });
24648
- }
24649
-
24650
- return i18n__namespace._("(Choice %(letter)s)", {
24651
- letter
24652
- });
24485
+ }));
24653
24486
  }
24654
24487
 
24655
- render() {
24656
- const {
24657
- pos,
24658
- reviewMode,
24659
- checked,
24660
- crossedOut,
24661
- correct,
24662
- product,
24663
- showCorrectness,
24664
- pressed,
24665
- focused,
24666
- primaryProductColor,
24667
- previouslyAnswered,
24668
- transparentBackground
24669
- } = this.props;
24670
- const letter = getChoiceLetter(pos);
24488
+ return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
24489
+ } // Handle dynamic styling of the multiple choice icon. Most
24490
+ // MC icon styles are constant, but we do allow the caller
24491
+ // to specify the selected color, and thus must control styles
24492
+ // related to the selected state dynamically.
24493
+
24494
+
24495
+ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground) {
24496
+ let backgroundColor;
24497
+ let borderColor;
24498
+ let color;
24499
+
24500
+ if (!showCorrectness && pressed) {
24501
+ borderColor = primaryProductColor;
24502
+ color = primaryProductColor;
24503
+ backgroundColor = transparentBackground ? "transparent" : white$1;
24504
+ } else if (checked) {
24505
+ // Note: kaGreen is not only the default product color,
24506
+ // but also the "correctness" color
24507
+ const bg = showCorrectness && correct ? Color__default["default"].green : primaryProductColor;
24508
+ color = white$1;
24509
+ backgroundColor = bg;
24510
+ borderColor = bg;
24511
+ } else {
24512
+ borderColor = Color__default["default"].offBlack64;
24513
+ color = Color__default["default"].offBlack64;
24514
+ } // define shape
24671
24515
 
24672
- if (product === "sat") {
24673
- return /*#__PURE__*/React__namespace.createElement(SATChoiceIcon, {
24674
- letter: letter,
24675
- a11yText: this.a11yText(letter),
24676
- reviewMode: reviewMode,
24677
- checked: checked,
24678
- correct: correct,
24679
- crossedOut: crossedOut
24680
- });
24681
- }
24682
24516
 
24683
- return /*#__PURE__*/React__namespace.createElement(LibraryChoiceIcon, {
24684
- letter: letter,
24685
- a11yText: this.a11yText(letter),
24686
- reviewMode: reviewMode,
24687
- checked: checked,
24688
- crossedOut: crossedOut,
24689
- pressed: pressed,
24690
- focused: focused,
24691
- correct: correct,
24692
- showCorrectness: showCorrectness,
24693
- primaryProductColor: primaryProductColor,
24694
- previouslyAnswered: previouslyAnswered,
24695
- transparentBackground: transparentBackground
24696
- });
24517
+ let borderRadius;
24518
+
24519
+ if (multipleSelect) {
24520
+ borderRadius = 3;
24521
+ } else {
24522
+ borderRadius = LIBRARY_ICON_SIZE;
24697
24523
  }
24698
24524
 
24525
+ return {
24526
+ backgroundColor,
24527
+ borderColor,
24528
+ color,
24529
+ borderRadius
24530
+ };
24699
24531
  }
24700
24532
 
24701
- _defineProperty(ChoiceIcon, "defaultProps", {
24702
- primaryProductColor: Color__default["default"].blue
24703
- });
24533
+ function LibraryChoiceIcon(props) {
24534
+ const {
24535
+ checked,
24536
+ crossedOut,
24537
+ showCorrectness,
24538
+ correct,
24539
+ focused,
24540
+ hovered,
24541
+ multipleSelect,
24542
+ primaryProductColor,
24543
+ previouslyAnswered,
24544
+ letter,
24545
+ pressed,
24546
+ transparentBackground
24547
+ } = props;
24548
+ const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground);
24549
+ return /*#__PURE__*/React__namespace.createElement("div", {
24550
+ className: aphrodite.css(styles$x.iconWrapper)
24551
+ }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
24552
+ color: primaryProductColor,
24553
+ visible: focused || hovered,
24554
+ multipleSelect: multipleSelect
24555
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24556
+ style: dynamicStyles,
24557
+ "data-test-id": "choice-icon__library-choice-icon",
24558
+ className: aphrodite.css(styles$v.circle, showCorrectness && correct && styles$v.circleCorrect, showCorrectness && !correct && styles$v.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$v.circleIncorrectAnswered) // used in BaseRadio to check if we actually clicked on the
24559
+ // radio icon
24560
+ ,
24561
+ "data-is-radio-icon": true
24562
+ }, /*#__PURE__*/React__namespace.createElement(ChoiceInner, {
24563
+ letter: letter,
24564
+ showCorrectness: showCorrectness,
24565
+ correct: correct
24566
+ }))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24567
+ color: dynamicStyles.borderColor
24568
+ }));
24569
+ }
24704
24570
 
24705
- const styles$t = aphrodite.StyleSheet.create({
24706
- satCircle: {
24707
- display: "block",
24708
- borderRadius: SAT_ICON_SIZE,
24709
- borderStyle: "solid",
24710
- borderWidth: 2,
24711
- content: "''",
24712
- height: SAT_ICON_SIZE,
24713
- width: SAT_ICON_SIZE,
24714
- top: 1,
24715
- left: 1
24716
- },
24717
- iconWrapper: {
24718
- display: "inline-block",
24719
- position: "relative"
24720
- },
24721
- crossOutLine: {
24722
- // Center the icon within the container.
24723
- position: "absolute",
24724
- top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24725
- left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24726
- },
24727
- libraryCircle: {
24571
+ const styles$v = aphrodite.StyleSheet.create({
24572
+ circle: {
24728
24573
  // Make the circle
24729
24574
  width: LIBRARY_ICON_SIZE,
24730
24575
  height: LIBRARY_ICON_SIZE,
24731
24576
  boxSizing: "border-box",
24732
- borderRadius: LIBRARY_ICON_SIZE,
24733
24577
  borderStyle: "solid",
24734
24578
  borderWidth: 2,
24735
24579
  // The default icons have letters in them. Style those letters.
@@ -24750,18 +24594,108 @@ const styles$t = aphrodite.StyleSheet.create({
24750
24594
  // pixel so it is much better centered.
24751
24595
  lineHeight: "1px"
24752
24596
  },
24753
- libraryCircleCorrect: {
24597
+ circleCorrect: {
24754
24598
  fontSize: LIBRARY_ICON_SIZE
24755
24599
  },
24756
- libraryCircleIncorrect: {
24600
+ circleIncorrect: {
24757
24601
  fontSize: LIBRARY_ICON_SIZE,
24758
24602
  borderColor: gray68,
24759
24603
  color: gray68
24760
24604
  },
24761
- libraryCircleIncorrectAnswered: {
24605
+ circleIncorrectAnswered: {
24762
24606
  backgroundColor: Color__default["default"].red,
24763
24607
  borderColor: Color__default["default"].red,
24764
24608
  color: Color__default["default"].white
24609
+ }
24610
+ });
24611
+
24612
+ /* Component for rendering a letter icon in a SAT radio choice */
24613
+
24614
+ // TODO(amy): figure out a better scheme for specifying these
24615
+ // styles that isn't such a pain to grok. See some neat ideas
24616
+ // from MDR in https://phabricator.khanacademy.org/D35249.
24617
+ function constructSATStyles(reviewMode, correct, checked, multipleSelect) {
24618
+ let backgroundColor;
24619
+ let borderColor = satBlue;
24620
+ let color = satBlue;
24621
+
24622
+ if (reviewMode) {
24623
+ if (correct) {
24624
+ borderColor = satCorrectColor;
24625
+ color = checked ? white$1 : satCorrectColor;
24626
+ backgroundColor = checked ? satCorrectColor : white$1;
24627
+ } else if (checked) {
24628
+ borderColor = satIncorrectColor;
24629
+ color = white$1;
24630
+ backgroundColor = satIncorrectColor;
24631
+ }
24632
+ } else if (checked) {
24633
+ color = white$1;
24634
+ backgroundColor = satBlue;
24635
+ }
24636
+
24637
+ let borderRadius;
24638
+
24639
+ if (multipleSelect) {
24640
+ borderRadius = 3;
24641
+ } else {
24642
+ borderRadius = SAT_ICON_SIZE;
24643
+ }
24644
+
24645
+ return {
24646
+ color,
24647
+ backgroundColor,
24648
+ borderColor,
24649
+ borderRadius
24650
+ };
24651
+ }
24652
+
24653
+ function SATChoiceIcon(props) {
24654
+ const {
24655
+ letter,
24656
+ crossedOut,
24657
+ reviewMode,
24658
+ correct,
24659
+ checked,
24660
+ multipleSelect
24661
+ } = props;
24662
+ const {
24663
+ color,
24664
+ backgroundColor,
24665
+ borderColor,
24666
+ borderRadius
24667
+ } = constructSATStyles(reviewMode, correct, checked, multipleSelect);
24668
+ return /*#__PURE__*/React__namespace.createElement("div", {
24669
+ className: aphrodite.css(styles$x.iconWrapper)
24670
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24671
+ className: aphrodite.css(styles$u.satCircle),
24672
+ "data-test-id": "choice-icon__sat-choice-icon",
24673
+ style: {
24674
+ backgroundColor,
24675
+ borderColor,
24676
+ borderRadius
24677
+ }
24678
+ }), /*#__PURE__*/React__namespace.createElement("div", {
24679
+ style: {
24680
+ color
24681
+ },
24682
+ className: aphrodite.css(styles$u.letter)
24683
+ }, /*#__PURE__*/React__namespace.createElement("span", null, letter)), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24684
+ color: borderColor,
24685
+ sat: true
24686
+ }));
24687
+ }
24688
+
24689
+ const styles$u = aphrodite.StyleSheet.create({
24690
+ satCircle: {
24691
+ display: "block",
24692
+ borderStyle: "solid",
24693
+ borderWidth: 2,
24694
+ content: "''",
24695
+ height: SAT_ICON_SIZE,
24696
+ width: SAT_ICON_SIZE,
24697
+ top: 1,
24698
+ left: 1
24765
24699
  },
24766
24700
  letter: {
24767
24701
  // These properties make sure that this element has the exact
@@ -24781,6 +24715,55 @@ const styles$t = aphrodite.StyleSheet.create({
24781
24715
  }
24782
24716
  });
24783
24717
 
24718
+ /* Component for rendering a letter icon in radio choice */
24719
+
24720
+ function ChoiceIcon(props) {
24721
+ const {
24722
+ pos,
24723
+ reviewMode,
24724
+ checked,
24725
+ crossedOut,
24726
+ correct,
24727
+ product,
24728
+ multipleSelect,
24729
+ showCorrectness,
24730
+ focused,
24731
+ hovered,
24732
+ pressed,
24733
+ primaryProductColor = Color__default["default"].blue,
24734
+ previouslyAnswered,
24735
+ transparentBackground
24736
+ } = props;
24737
+ const letter = getChoiceLetter(pos);
24738
+
24739
+ if (product === "sat") {
24740
+ return /*#__PURE__*/React__namespace.createElement(SATChoiceIcon, {
24741
+ letter: letter,
24742
+ reviewMode: reviewMode,
24743
+ checked: checked,
24744
+ correct: correct,
24745
+ crossedOut: crossedOut,
24746
+ multipleSelect: multipleSelect
24747
+ });
24748
+ }
24749
+
24750
+ return /*#__PURE__*/React__namespace.createElement(LibraryChoiceIcon, {
24751
+ letter: letter,
24752
+ reviewMode: reviewMode,
24753
+ checked: checked,
24754
+ crossedOut: crossedOut,
24755
+ focused: focused,
24756
+ hovered: hovered,
24757
+ pressed: pressed,
24758
+ correct: correct,
24759
+ showCorrectness: showCorrectness,
24760
+ primaryProductColor: primaryProductColor,
24761
+ previouslyAnswered: previouslyAnswered,
24762
+ transparentBackground: transparentBackground,
24763
+ multipleSelect: multipleSelect
24764
+ });
24765
+ }
24766
+
24784
24767
  /**
24785
24768
  * Renders text indicating whether the choice was correct or
24786
24769
  * not and whether the choice was selected or not.
@@ -24789,63 +24772,68 @@ const styles$t = aphrodite.StyleSheet.create({
24789
24772
  * between the states more immediately clear to users.
24790
24773
  */
24791
24774
 
24792
- class OptionStatus extends React__namespace.Component {
24793
- _renderText() {
24794
- const {
24795
- checked,
24796
- correct,
24797
- crossedOut
24798
- } = this.props;
24775
+ function renderText(checked, correct, crossedOut) {
24776
+ if (correct) {
24777
+ // For correct answers, we surface checked _or_ crossedOut state,
24778
+ // because any interaction with the correct answer is noteworthy!
24779
+ if (checked) {
24780
+ return i18n__namespace._("Correct (selected)");
24781
+ }
24799
24782
 
24800
- if (correct) {
24801
- // For correct answers, we surface checked _or_ crossedOut state,
24802
- // because any interaction with the correct answer is noteworthy!
24803
- if (checked) {
24804
- return i18n__namespace._("Correct (selected)");
24805
- }
24783
+ if (crossedOut) {
24784
+ return i18n__namespace._("Correct (but you crossed it out)");
24785
+ }
24806
24786
 
24807
- if (crossedOut) {
24808
- return i18n__namespace._("Correct (but you crossed it out)");
24809
- }
24787
+ return i18n__namespace._("Correct");
24788
+ } // But, for incorrect answers, we only surface checked state,
24789
+ // because crossing out an incorrect answer is not noteworthy.
24810
24790
 
24811
- return i18n__namespace._("Correct");
24812
- } // But, for incorrect answers, we only surface checked state,
24813
- // because crossing out an incorrect answer is not noteworthy.
24814
24791
 
24792
+ if (checked) {
24793
+ return i18n__namespace._("Incorrect (selected)");
24794
+ }
24815
24795
 
24816
- if (checked) {
24817
- return i18n__namespace._("Incorrect (selected)");
24818
- }
24796
+ return i18n__namespace._("Incorrect");
24797
+ }
24798
+
24799
+ function OptionStatus(props) {
24800
+ const {
24801
+ checked,
24802
+ correct,
24803
+ crossedOut,
24804
+ previouslyAnswered,
24805
+ reviewMode,
24806
+ satStyling
24807
+ } = props; // Option status is exclued for SAT
24808
+
24809
+ if (satStyling) {
24810
+ return null;
24811
+ } // Option status is shown only in review mode, or for incorrectly
24812
+ // answered items.
24819
24813
 
24820
- return i18n__namespace._("Incorrect");
24814
+
24815
+ if (!reviewMode && !previouslyAnswered) {
24816
+ return null;
24821
24817
  }
24822
24818
 
24823
- render() {
24824
- const {
24825
- checked,
24826
- correct,
24827
- previouslyAnswered
24828
- } = this.props;
24829
- let textStyle;
24819
+ let textStyle;
24830
24820
 
24831
- if (correct) {
24832
- textStyle = styles$s.correct;
24821
+ if (correct) {
24822
+ textStyle = styles$t.correct;
24823
+ } else {
24824
+ if (checked || previouslyAnswered) {
24825
+ textStyle = styles$t.incorrectAnswered;
24833
24826
  } else {
24834
- if (checked || previouslyAnswered) {
24835
- textStyle = styles$s.incorrectAnswered;
24836
- } else {
24837
- textStyle = styles$s.incorrect;
24838
- }
24827
+ textStyle = styles$t.incorrect;
24839
24828
  }
24840
-
24841
- return /*#__PURE__*/React__namespace.createElement("div", {
24842
- className: aphrodite.css(styles$s.text, textStyle)
24843
- }, this._renderText());
24844
24829
  }
24845
24830
 
24831
+ return /*#__PURE__*/React__namespace.createElement("div", {
24832
+ className: aphrodite.css(styles$t.text, textStyle)
24833
+ }, renderText(checked, correct, crossedOut));
24846
24834
  }
24847
24835
 
24848
- const styles$s = aphrodite.StyleSheet.create({
24836
+ const styles$t = aphrodite.StyleSheet.create({
24849
24837
  text: {
24850
24838
  alignItems: "center",
24851
24839
  display: "flex",
@@ -24871,317 +24859,236 @@ const ellipsisHorizontalIcon = {
24871
24859
  width: 100,
24872
24860
  height: 27.284
24873
24861
  };
24862
+ // Note(TB): Received errors when using useUniqueIdWithMock
24863
+ // so created this workaround function. Will update when
24864
+ // useUniqueIdWithMock is available.
24865
+ // https://khanacademy.atlassian.net/browse/FEI-4861?atlOrigin=eyJpIjoiNDJlZWMwNjM1NWJhNDBkMWFjY2FmN2I0ZjcxZmQxOGUiLCJwIjoiaiJ9
24866
+ let id = 0;
24874
24867
 
24875
- class Choice extends React__namespace.Component {
24876
- constructor() {
24877
- super(...arguments);
24878
-
24879
- _defineProperty(this, "justFinishedTouch", void 0);
24880
-
24881
- _defineProperty(this, "justFinishedTimeoutID", void 0);
24882
-
24883
- _defineProperty(this, "_input", void 0);
24884
-
24885
- _defineProperty(this, "state", {
24886
- isInputFocused: false,
24887
- isInputActive: false
24888
- });
24889
-
24890
- _defineProperty(this, "inputRef", ref => {
24891
- this._input = ref;
24892
- });
24893
-
24894
- _defineProperty(this, "renderOptionStatus", () => {
24895
- const {
24896
- correct,
24897
- checked,
24898
- crossedOut,
24899
- reviewMode,
24900
- previouslyAnswered
24901
- } = this.props; // Option status is exclued for SAT
24902
-
24903
- if (this.props.apiOptions.satStyling) {
24904
- return null;
24905
- } // Option status is shown only in review mode, or for incorrectly
24906
- // answered items.
24868
+ function uniqueId() {
24869
+ return "choice-".concat(id++);
24870
+ }
24907
24871
 
24872
+ function Choice(props) {
24873
+ const {
24874
+ disabled,
24875
+ checked,
24876
+ content,
24877
+ crossedOut,
24878
+ showCorrectness,
24879
+ multipleSelect,
24880
+ onChange,
24881
+ reviewMode,
24882
+ correct,
24883
+ apiOptions,
24884
+ previouslyAnswered,
24885
+ pos,
24886
+ showRationale,
24887
+ rationale,
24888
+ forwardedRef
24889
+ } = props;
24890
+ const [isInputFocused, setIsInputFocused] = React.useState(false);
24891
+ React.useEffect(() => {
24892
+ if (isInputFocused && disabled) {
24893
+ setIsInputFocused(false);
24894
+ }
24895
+ }, [disabled, isInputFocused, setIsInputFocused]); // Call `this.props.onChange` with the given values. Any keys that are not
24896
+ // specified will be filled in with the current value. (For example, if
24897
+ // `checked` is specified but `crossedOut` is not, then `crossedOut` will
24898
+ // be filled in with `this.props.crossedOut`.)
24899
+ //
24900
+ // This enables us to use shorthand inside this component, while
24901
+ // maintaining a consistent API for the parent.
24908
24902
 
24909
- if (!reviewMode && !previouslyAnswered) {
24910
- return null;
24911
- }
24903
+ function sendChange(newValues) {
24904
+ var _newValues$checked, _newValues$crossedOut;
24912
24905
 
24913
- return /*#__PURE__*/React__namespace.createElement(OptionStatus, {
24914
- checked: checked,
24915
- correct: correct,
24916
- crossedOut: crossedOut,
24917
- previouslyAnswered: previouslyAnswered
24918
- });
24906
+ const updatedChecked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : checked;
24907
+ const updatedCrossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : crossedOut;
24908
+ onChange({
24909
+ checked: updatedChecked,
24910
+ crossedOut: updatedCrossedOut
24919
24911
  });
24912
+ } // HACK: while most of the styling for rendering SAT items is handled
24913
+ // via aphrodite, we also need to assign normal CSS classnames here to
24914
+ // special-case the coloring of MathJax formulas (see .MathJax .math in
24915
+ // stylesheets/task-package/tasks.less)
24920
24916
 
24921
- _defineProperty(this, "renderChoiceIcon", _ref => {
24922
- let {
24923
- isFocused,
24924
- isPressed
24925
- } = _ref;
24926
- const {
24927
- radioStyleVersion,
24928
- primaryProductColor
24929
- } = this.props.apiOptions.styling;
24930
- const finalStyles = typeof radioStyleVersion === "undefined" ? false : radioStyleVersion === "final";
24931
-
24932
- if (!finalStyles && !this.props.apiOptions.satStyling) {
24933
- return null;
24934
- }
24935
-
24936
- return /*#__PURE__*/React__namespace.createElement(ChoiceIcon, {
24937
- pos: this.props.pos,
24938
- correct: this.props.correct,
24939
- crossedOut: this.props.crossedOut,
24940
- pressed: isPressed,
24941
- focused: isFocused,
24942
- checked: this.props.checked,
24943
- showCorrectness: this.props.showCorrectness,
24944
- reviewMode: this.props.reviewMode,
24945
- product: this.props.apiOptions.satStyling ? "sat" : "library",
24946
- primaryProductColor: primaryProductColor,
24947
- previouslyAnswered: this.props.previouslyAnswered
24948
- });
24949
- });
24950
24917
 
24951
- _defineProperty(this, "_sendChange", newValues => {
24952
- var _newValues$checked, _newValues$crossedOut;
24918
+ const satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
24919
+ const satIncorrectChecked = apiOptions.satStyling && reviewMode && !correct && checked;
24920
+ const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!apiOptions.satStyling && styles$s.description, apiOptions.satStyling && styles$s.satDescription, satCorrectChoice && styles$s.satDescriptionCorrect, satCorrectChoice && checked && styles$s.satDescriptionCorrectChecked, satIncorrectChecked && styles$s.satDescriptionIncorrectChecked));
24921
+ const rationaleClassName = classNames__default["default"]("perseus-radio-rationale-content", aphrodite.css(styles$s.rationale, !apiOptions.satStyling && styles$s.nonSatRationale, apiOptions.satStyling && styles$s.satReviewRationale)); // We want to show the choices as dimmed out when the choices are
24922
+ // disabled. However, we don't want to do this in the SAT product and
24923
+ // we also don't want to do this when we're in review mode in the
24924
+ // content library.
24953
24925
 
24954
- const checked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : this.props.checked;
24955
- const crossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : this.props.crossedOut;
24956
- this.props.onChange({
24957
- checked,
24958
- crossedOut
24926
+ const showDimmed = !apiOptions.satStyling && !reviewMode && apiOptions.readOnly || crossedOut;
24927
+ const letter = getChoiceLetter(pos);
24928
+ const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
24929
+ const choiceId = uniqueId();
24930
+ return /*#__PURE__*/React__namespace.createElement("div", {
24931
+ style: {
24932
+ dispay: "flex",
24933
+ flexDirection: "column"
24934
+ }
24935
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24936
+ style: {
24937
+ display: "flex",
24938
+ flexDirection: "row",
24939
+ width: "100%",
24940
+ opacity: showDimmed ? 0.5 : 1.0
24941
+ }
24942
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24943
+ className: "perseus-sr-only"
24944
+ }, /*#__PURE__*/React__namespace.createElement("input", {
24945
+ type: multipleSelect ? "checkbox" : "radio",
24946
+ id: choiceId,
24947
+ checked: checked,
24948
+ onClick: () => {
24949
+ // If we're checking a crossed-out option, let's
24950
+ // also uncross it.
24951
+ sendChange({
24952
+ checked: !checked,
24953
+ crossedOut: false
24959
24954
  });
24960
- });
24961
-
24962
- _defineProperty(this, "focusInput", () => {
24963
- var _this$_input;
24964
-
24965
- (_this$_input = this._input) === null || _this$_input === void 0 ? void 0 : _this$_input.focus();
24966
- });
24967
- }
24968
-
24969
- UNSAFE_componentWillUpdate(nextProps) {
24970
- if (this.state.isInputFocused && nextProps.disabled) {
24971
- this.setState({
24972
- isInputFocused: false
24955
+ },
24956
+ onChange: () => {},
24957
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
24958
+ tabIndex: -1
24959
+ }), /*#__PURE__*/React__namespace.createElement("label", {
24960
+ htmlFor: choiceId
24961
+ }, a11yText, " \xA0 ", content)), /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
24962
+ onClick: () => {
24963
+ // If we're checking a crossed-out option, let's
24964
+ // also uncross it.
24965
+ sendChange({
24966
+ checked: !checked,
24967
+ crossedOut: false
24973
24968
  });
24974
- }
24975
- }
24969
+ },
24970
+ className: descriptionClassName,
24971
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
24972
+ style: {
24973
+ flex: 1
24974
+ },
24975
+ ref: forwardedRef,
24976
+ "aria-hidden": "true"
24977
+ }, _ref => {
24978
+ var _apiOptions$styling;
24976
24979
 
24977
- render() {
24978
- const sat = this.props.apiOptions.satStyling;
24979
- const {
24980
- reviewMode,
24981
- correct,
24982
- checked
24983
- } = this.props; // HACK: while most of the styling for rendering SAT items is handled
24984
- // via aphrodite, we also need to assign normal CSS classnames here to
24985
- // special-case the coloring of MathJax formulas (see .MathJax .math in
24986
- // stylesheets/task-package/tasks.less)
24987
-
24988
- const satCorrectChoice = sat && reviewMode && correct;
24989
- const satIncorrectChecked = sat && reviewMode && !correct && checked;
24990
- const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!sat && styles$r.description, sat && styles$r.satDescription, satCorrectChoice && styles$r.satDescriptionCorrect, satCorrectChoice && checked && styles$r.satDescriptionCorrectChecked, satIncorrectChecked && styles$r.satDescriptionIncorrectChecked));
24991
- const rationaleClassName = classNames__default["default"]("perseus-radio-rationale-content", aphrodite.css(styles$r.rationale, !sat && styles$r.nonSatRationale, sat && styles$r.satReviewRationale)); // We want to show the choices as dimmed out when the choices are
24992
- // disabled. However, we don't want to do this in the SAT product and
24993
- // we also don't want to do this when we're in review mode in the
24994
- // content library.
24995
-
24996
- const showDimmed = !sat && !reviewMode && this.props.apiOptions.readOnly || this.props.crossedOut;
24980
+ let {
24981
+ hovered,
24982
+ focused,
24983
+ pressed
24984
+ } = _ref;
24997
24985
  return /*#__PURE__*/React__namespace.createElement("div", {
24998
- style: {
24999
- dispay: "flex",
25000
- flexDirection: "column"
25001
- }
25002
- }, /*#__PURE__*/React__namespace.createElement("div", {
25003
24986
  style: {
25004
24987
  display: "flex",
25005
24988
  flexDirection: "row",
25006
- width: "100%",
25007
- opacity: showDimmed ? 0.5 : 1.0
24989
+ justifyContent: "center",
24990
+ alignContent: "center",
24991
+ paddingTop: Spacing__default["default"].xSmall_8,
24992
+ paddingBottom: Spacing__default["default"].xSmall_8
25008
24993
  }
25009
- }, /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25010
- onClick: () => {
25011
- // If we're checking a crossed-out option, let's
25012
- // also uncross it.
25013
- this._sendChange({
25014
- checked: true,
25015
- crossedOut: false
25016
- });
25017
- },
25018
- className: descriptionClassName,
25019
- disabled: this.props.disabled || this.props.apiOptions.staticRender || this.props.apiOptions.readOnly,
25020
- "aria-label": "Select Choice ".concat(getChoiceLetter(this.props.pos)),
25021
- "aria-checked": this.props.checked,
25022
- role: "checkbox",
24994
+ }, /*#__PURE__*/React__namespace.createElement(ChoiceIcon, {
24995
+ pos: pos,
24996
+ correct: correct,
24997
+ crossedOut: crossedOut,
24998
+ pressed: pressed,
24999
+ focused: focused,
25000
+ checked: checked,
25001
+ hovered: hovered,
25002
+ showCorrectness: showCorrectness,
25003
+ multipleSelect: multipleSelect,
25004
+ reviewMode: reviewMode,
25005
+ product: apiOptions.satStyling ? "sat" : "library",
25006
+ primaryProductColor: (_apiOptions$styling = apiOptions.styling) === null || _apiOptions$styling === void 0 ? void 0 : _apiOptions$styling.primaryProductColor,
25007
+ previouslyAnswered: previouslyAnswered
25008
+ }), /*#__PURE__*/React__namespace.createElement("span", {
25023
25009
  style: {
25024
- flex: 1
25010
+ paddingLeft: Spacing__default["default"].small_12,
25011
+ textAlign: "left",
25012
+ flex: 1,
25013
+ paddingTop: 4
25025
25014
  }
25026
- }, _ref2 => {
25015
+ }, /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(OptionStatus, {
25016
+ checked: checked,
25017
+ correct: correct,
25018
+ crossedOut: crossedOut,
25019
+ previouslyAnswered: previouslyAnswered,
25020
+ reviewMode: reviewMode,
25021
+ satStyling: apiOptions.satStyling
25022
+ })), /*#__PURE__*/React__namespace.createElement("div", null, content)));
25023
+ }), apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.Popover, {
25024
+ dismissEnabled: true,
25025
+ content: _ref2 => {
25027
25026
  let {
25028
- hovered,
25029
- focused,
25030
- pressed
25027
+ close
25031
25028
  } = _ref2;
25032
- return /*#__PURE__*/React__namespace.createElement("div", {
25033
- style: {
25034
- display: "flex",
25035
- flexDirection: "row",
25036
- justifyContent: "center",
25037
- alignContent: "center",
25038
- paddingTop: Spacing__default["default"].xSmall_8,
25039
- paddingBottom: Spacing__default["default"].xSmall_8
25040
- }
25041
- }, /*#__PURE__*/React__namespace.createElement("span", null, this.renderChoiceIcon({
25042
- isFocused: focused,
25043
- isPressed: pressed
25044
- })), /*#__PURE__*/React__namespace.createElement("span", {
25045
- style: {
25046
- paddingLeft: Spacing__default["default"].small_12,
25047
- textAlign: "left",
25048
- flex: 1,
25049
- paddingTop: 4
25050
- }
25051
- }, /*#__PURE__*/React__namespace.createElement("div", null, this.renderOptionStatus()), /*#__PURE__*/React__namespace.createElement("div", null, this.props.content)));
25052
- }), this.props.apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.Popover, {
25053
- dismissEnabled: true,
25054
- content: _ref3 => {
25055
- let {
25056
- close
25057
- } = _ref3;
25058
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.PopoverContent, {
25059
- title: "Cross out",
25060
- content: "Cross out option",
25061
- closeButtonVisible: true,
25062
- actions: /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
25063
- size: Spacing__default["default"].medium_16
25064
- }), /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
25065
- kind: "primary",
25066
- "aria-label": "Cross out Choice ".concat(getChoiceLetter(this.props.pos)),
25067
- disabled: this.props.apiOptions.readOnly || reviewMode,
25068
- onClick: () => {
25069
- if (!this.props.crossedOut) {
25070
- // If we're crossing
25071
- // out a checked
25072
- // option, let's also
25073
- // uncheck it.
25074
- this._sendChange({
25075
- checked: false,
25076
- crossedOut: true
25077
- });
25078
- } else {
25079
- this._sendChange({
25080
- crossedOut: false
25081
- });
25082
- }
25083
-
25084
- close();
25029
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.PopoverContent, {
25030
+ title: "Cross out",
25031
+ content: "Cross out option",
25032
+ closeButtonVisible: true,
25033
+ actions: /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
25034
+ size: Spacing__default["default"].medium_16
25035
+ }), /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
25036
+ kind: "primary",
25037
+ "aria-label": "Cross out Choice ".concat(getChoiceLetter(pos)),
25038
+ disabled: apiOptions.readOnly || reviewMode,
25039
+ onClick: () => {
25040
+ if (!crossedOut) {
25041
+ // If we're crossing
25042
+ // out a checked
25043
+ // option, let's also
25044
+ // uncheck it.
25045
+ sendChange({
25046
+ checked: false,
25047
+ crossedOut: true
25048
+ });
25049
+ } else {
25050
+ sendChange({
25051
+ crossedOut: false
25052
+ });
25085
25053
  }
25086
- }, this.props.crossedOut ? "Bring back" : "Cross out"))
25087
- });
25088
- }
25054
+
25055
+ close();
25056
+ }
25057
+ }, crossedOut ? "Bring back" : "Cross out"))
25058
+ });
25059
+ }
25060
+ }, _ref3 => {
25061
+ let {
25062
+ open
25063
+ } = _ref3;
25064
+ return /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25065
+ onClick: open,
25066
+ "aria-label": "Open menu for Choice ".concat(getChoiceLetter(pos)),
25067
+ disabled: apiOptions.staticRender
25089
25068
  }, _ref4 => {
25090
- let {
25091
- open
25092
- } = _ref4;
25093
- return /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25094
- onClick: open,
25095
- "aria-label": "Open menu for Choice ".concat(getChoiceLetter(this.props.pos)),
25096
- disabled: this.props.apiOptions.staticRender
25097
- }, _ref5 => {
25098
- return /*#__PURE__*/React__namespace.createElement(Icon, {
25099
- icon: ellipsisHorizontalIcon,
25100
- size: 3,
25101
- color: Color__default["default"].offBlack64
25102
- });
25069
+ return /*#__PURE__*/React__namespace.createElement(Icon, {
25070
+ icon: ellipsisHorizontalIcon,
25071
+ size: 3,
25072
+ color: Color__default["default"].offBlack64
25103
25073
  });
25104
- })), this.props.showRationale && /*#__PURE__*/React__namespace.createElement("div", {
25105
- className: rationaleClassName,
25106
- "data-test-id": "perseus-radio-rationale-content-".concat(this.props.pos)
25107
- }, this.props.rationale));
25108
- }
25109
-
25074
+ });
25075
+ })), showRationale && /*#__PURE__*/React__namespace.createElement("div", {
25076
+ className: rationaleClassName,
25077
+ "data-test-id": "perseus-radio-rationale-content-".concat(pos)
25078
+ }, rationale));
25110
25079
  }
25111
25080
 
25112
- _defineProperty(Choice, "propTypes", {
25113
- // TODO(kevinb) use Options.propTypes from perseus-api.jsx
25114
- // This change will also require make sure that item-renderer.jsx and
25115
- // server-item-renderer.jsx have appropriate defaults for apiOptions
25116
- // because many of the properties on Options.propTypes are required.
25117
- apiOptions: PropTypes__default["default"].shape({
25118
- satStyling: PropTypes__default["default"].bool,
25119
- isMobile: PropTypes__default["default"].bool,
25120
- styling: PropTypes__default["default"].shape({
25121
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"]),
25122
- primaryProductColor: PropTypes__default["default"].string
25123
- }),
25124
- readOnly: PropTypes__default["default"].bool
25125
- }),
25126
- checked: PropTypes__default["default"].bool,
25127
- className: PropTypes__default["default"].string,
25128
- rationale: PropTypes__default["default"].node,
25129
- content: PropTypes__default["default"].node,
25130
- correct: PropTypes__default["default"].bool,
25131
- deselectEnabled: PropTypes__default["default"].bool,
25132
- disabled: PropTypes__default["default"].bool,
25133
- editMode: PropTypes__default["default"].bool,
25134
- groupName: PropTypes__default["default"].string,
25135
- isLastChoice: PropTypes__default["default"].bool,
25136
- // Needed for border styling
25137
- // This indicates the position of the choice relative to others
25138
- // (so that we can display a nice little (A), (B), etc. next to it)
25139
- // Also used to generate an id for each input.
25140
- pos: PropTypes__default["default"].number,
25141
- reviewMode: PropTypes__default["default"].bool,
25142
- showRationale: PropTypes__default["default"].bool,
25143
- showCorrectness: PropTypes__default["default"].bool,
25144
- type: PropTypes__default["default"].string,
25145
- // Indicates whether the user has "crossed out" this choice, meaning
25146
- // that they don't think it's correct. This value does not affect
25147
- // scoring or other behavior; it's just a note for the user's
25148
- // reference.
25149
- crossedOut: PropTypes__default["default"].bool,
25150
- // Indicates that the user has previously selected this answer. These
25151
- // answers may be rendered orange in review, rather than grey if
25152
- // incorrect.
25153
- previouslyAnswered: PropTypes__default["default"].bool,
25154
- // A callback indicating that this choice has changed. Its argument is
25155
- // an object with two keys: `checked` and `crossedOut`. Each contains a
25156
- // boolean value specifying the new checked and crossed-out value of
25157
- // this choice.
25158
- onChange: PropTypes__default["default"].func,
25159
- // Callbacks for when the user presses the up arrow or down arrow
25160
- // (respectively) to navigate between choices. When called, the parent
25161
- // finds the prev/next choice, focuses the input, and checks the radio
25162
- // button if appropriate. (The reason we have custom behavior is that
25163
- // we actually _don't_ want to auto-select crossed-out choices!)
25164
- goToPrevChoice: PropTypes__default["default"].func.isRequired,
25165
- goToNextChoice: PropTypes__default["default"].func.isRequired
25166
- });
25167
-
25168
- _defineProperty(Choice, "defaultProps", {
25169
- apiOptions: {
25170
- styling: {}
25171
- },
25081
+ Choice.defaultProps = {
25082
+ disabled: false,
25172
25083
  checked: false,
25173
- classSet: {},
25084
+ onChange: newValues => {},
25174
25085
  correct: false,
25175
- disabled: false,
25176
- editMode: false,
25177
- onChange: function () {},
25178
- showRationale: false,
25179
- type: "radio",
25086
+ apiOptions: ApiOptions.defaults,
25087
+ previouslyAnswered: false,
25180
25088
  pos: 0,
25181
- previouslyAnswered: false
25182
- });
25183
-
25184
- const styles$r = aphrodite.StyleSheet.create({
25089
+ showRationale: false
25090
+ };
25091
+ const styles$s = aphrodite.StyleSheet.create({
25185
25092
  description: {
25186
25093
  display: "inline-block",
25187
25094
  width: "100%"
@@ -25221,184 +25128,83 @@ const styles$r = aphrodite.StyleSheet.create({
25221
25128
  marginLeft: 45
25222
25129
  }
25223
25130
  });
25131
+ var Choice$1 = /*#__PURE__*/React__namespace.forwardRef((props, ref) => /*#__PURE__*/React__namespace.createElement(Choice, _extends({}, props, {
25132
+ forwardedRef: ref
25133
+ })));
25224
25134
 
25225
- class ChoiceNoneAbove extends React__namespace.Component {
25226
- constructor() {
25227
- super(...arguments);
25228
-
25229
- _defineProperty(this, "_choice", void 0);
25230
-
25231
- _defineProperty(this, "focusInput", () => {
25232
- this._choice.focusInput();
25233
- });
25234
- }
25235
-
25236
- render() {
25237
- const choiceProps = { ...this.props,
25238
- className: classNames__default["default"](this.props.className, "none-of-above"),
25239
- content: this.props.showContent ? this.props.content :
25240
- /*#__PURE__*/
25241
- // We use a Renderer here because that is how
25242
- // `this.props.content` is wrapped otherwise.
25243
- // We pass in a key here so that we avoid a semi-spurious
25244
- // react warning when we render this in the same place
25245
- // as the previous choice content renderer.
25246
- // Note this destroys state, but since all we're doing
25247
- // is outputting "None of the above", that is okay.
25248
- React__namespace.createElement(Renderer, {
25249
- key: "noneOfTheAboveRenderer",
25250
- content: i18n__namespace._("None of the above")
25251
- }),
25252
- ref: el => this._choice = el
25253
- };
25254
- return /*#__PURE__*/React__namespace.createElement(Choice, choiceProps);
25255
- }
25256
-
25135
+ function ChoiceNoneAbove(props) {
25136
+ const {
25137
+ showContent,
25138
+ content,
25139
+ forwardedRef,
25140
+ ...rest
25141
+ } = props;
25142
+ const choiceProps = { ...rest,
25143
+ content: showContent ? content :
25144
+ /*#__PURE__*/
25145
+ // We use a Renderer here because that is how
25146
+ // `this.props.content` is wrapped otherwise.
25147
+ // We pass in a key here so that we avoid a semi-spurious
25148
+ // react warning when we render this in the same place
25149
+ // as the previous choice content renderer.
25150
+ // Note this destroys state, but since all we're doing
25151
+ // is outputting "None of the above", that is okay.
25152
+ //
25153
+ // todo(matthewc): this seems like way overkill
25154
+ // just to render a string
25155
+ React__namespace.createElement(Renderer, {
25156
+ key: "noneOfTheAboveRenderer",
25157
+ content: i18n__namespace._("None of the above")
25158
+ })
25159
+ };
25160
+ return /*#__PURE__*/React__namespace.createElement(Choice$1, _extends({}, choiceProps, {
25161
+ ref: forwardedRef
25162
+ }));
25257
25163
  }
25258
25164
 
25259
- _defineProperty(ChoiceNoneAbove, "propTypes", {
25260
- className: PropTypes__default["default"].string,
25261
- content: PropTypes__default["default"].node,
25262
- showContent: PropTypes__default["default"].bool
25263
- });
25264
-
25265
- _defineProperty(ChoiceNoneAbove, "defaultProps", {
25165
+ ChoiceNoneAbove.defaultProps = {
25266
25166
  showContent: true
25267
- });
25167
+ };
25168
+ var ChoiceNoneAbove$1 = /*#__PURE__*/React__namespace.forwardRef((props, ref) => /*#__PURE__*/React__namespace.createElement(ChoiceNoneAbove, _extends({}, props, {
25169
+ forwardedRef: ref
25170
+ })));
25268
25171
 
25269
25172
  const {
25270
25173
  captureScratchpadTouchStart: captureScratchpadTouchStart$2
25271
- } = Util;
25272
- const ChoicesType = PropTypes__default["default"].arrayOf(PropTypes__default["default"].shape({
25273
- // Indicates whether this choice is checked.
25274
- checked: PropTypes__default["default"].bool,
25275
- // Indicates whether the user has "crossed out" this choice, meaning
25276
- // that they don't think it's correct. This value does not affect
25277
- // scoring or other behavior; it's just a note for the user's
25278
- // reference.
25279
- crossedOut: PropTypes__default["default"].bool,
25280
- content: PropTypes__default["default"].node,
25281
- rationale: PropTypes__default["default"].node,
25282
- hasRationale: PropTypes__default["default"].bool,
25283
- showRationale: PropTypes__default["default"].bool,
25284
- showCorrectness: PropTypes__default["default"].bool,
25285
- correct: PropTypes__default["default"].bool,
25286
- originalIndex: PropTypes__default["default"].number,
25287
- isNoneOfTheAbove: PropTypes__default["default"].bool
25288
- }));
25289
- const radioBorderColor = radioBorderColor$2;
25290
-
25291
- class BaseRadio extends React__namespace.Component {
25292
- constructor() {
25293
- super(...arguments);
25294
-
25295
- _defineProperty(this, "state", {
25296
- // TODO(mdr): This keeps the ID stable across re-renders on the
25297
- // same machine, but, at time of writing, the server's state
25298
- // isn't rehydrated to the client during SSR, so the server and
25299
- // client will generate different IDs and cause a mismatch
25300
- // during SSR :(
25301
- radioGroupName: ___default["default"].uniqueId("perseus_radio_")
25302
- });
25303
-
25304
- _defineProperty(this, "updateChoice", (choiceIndex, newValues) => {
25305
- // Get the baseline `checked` values. If we're checking a new answer
25306
- // and multiple-select is not on, we should clear all choices to be
25307
- // unchecked. Otherwise, we should copy the old checked values.
25308
- let newCheckedList;
25309
-
25310
- if (newValues.checked && !this.props.multipleSelect) {
25311
- newCheckedList = this.props.choices.map(_ => false);
25312
- } else {
25313
- newCheckedList = this.props.choices.map(c => c.checked);
25314
- } // Get the baseline `crossedOut` values.
25315
-
25174
+ } = Util; // exported for tests
25316
25175
 
25317
- const newCrossedOutList = this.props.choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25318
-
25319
- newCheckedList[choiceIndex] = newValues.checked;
25320
- newCrossedOutList[choiceIndex] = newValues.crossedOut;
25321
- this.props.onChange({
25322
- checked: newCheckedList,
25323
- crossedOut: newCrossedOutList
25176
+ function getInstructionsText(multipleSelect, countChoices, numCorrect) {
25177
+ if (multipleSelect) {
25178
+ if (countChoices) {
25179
+ return i18n__namespace._("Choose %(numCorrect)s answers:", {
25180
+ numCorrect: numCorrect
25324
25181
  });
25325
- });
25326
-
25327
- _defineProperty(this, "focus", i => {
25328
- // $FlowFixMe[incompatible-use]
25329
- // $FlowFixMe[prop-missing]
25330
- ReactDOM__default["default"].findDOMNode(this.refs["radio" + (i || 0)]).focus(); // eslint-disable-line react/no-string-refs
25331
-
25332
- return true;
25333
- });
25334
-
25335
- _defineProperty(this, "getInstructionsText", () => {
25336
- if (this.props.multipleSelect) {
25337
- if (this.props.countChoices) {
25338
- return i18n__namespace._("Choose %(numCorrect)s answers:", {
25339
- numCorrect: this.props.numCorrect
25340
- });
25341
- }
25342
-
25343
- return i18n__namespace._("Choose all answers that apply:");
25344
- }
25345
-
25346
- return i18n__namespace._("Choose 1 answer:");
25347
- });
25348
-
25349
- _defineProperty(this, "deselectEnabled", () => {
25350
- // We want to force enable deselect on mobile.
25351
- return this.props.apiOptions.isMobile || this.props.deselectEnabled;
25352
- });
25353
-
25354
- _defineProperty(this, "goToChoice", newChoiceIndex => {
25355
- const numChoices = this.props.choices.length; // Wrap the newChoiceIndex around the start/end, if necessary.
25356
-
25357
- if (newChoiceIndex < 0) {
25358
- newChoiceIndex += numChoices;
25359
- }
25360
-
25361
- if (newChoiceIndex >= numChoices) {
25362
- newChoiceIndex -= numChoices;
25363
- } // Focus the new choice's input element.
25364
- // eslint-disable-next-line react/no-string-refs
25365
-
25366
-
25367
- const choiceInstance = this.refs["radio".concat(newChoiceIndex)];
25368
-
25369
- if (!choiceInstance) {
25370
- Log.error("found no choice at index ".concat(newChoiceIndex, ", even after ") + "wrapping it to be within bounds", Errors.Internal, {
25371
- loggedMetadata: {
25372
- choices: JSON.stringify(this.props.choices)
25373
- }
25374
- });
25375
- return;
25376
- }
25377
-
25378
- choiceInstance.focusInput(); // We aren't going to change any choices' crossed-out state, but we
25379
- // _are_ going to deselect all choices.
25380
-
25381
- const newCrossedOutList = this.props.choices.map(c => c.crossedOut);
25382
- const newCheckedList = this.props.choices.map(c => false); // And, if the new choice isn't crossed out, we'll select it.
25383
-
25384
- if (!this.props.choices[newChoiceIndex].crossedOut) {
25385
- newCheckedList[newChoiceIndex] = true;
25386
- }
25182
+ }
25387
25183
 
25388
- this.props.onChange({
25389
- checked: newCheckedList,
25390
- crossedOut: newCrossedOutList
25391
- });
25392
- });
25184
+ return i18n__namespace._("Choose all answers that apply:");
25393
25185
  }
25394
25186
 
25395
- componentDidUpdate(prevProps) {
25396
- const {
25397
- apiOptions,
25398
- choices,
25399
- isLastUsedWidget,
25400
- reviewModeRubric
25401
- } = this.props; // Switching into review mode can sometimes cause the selected answer
25187
+ return i18n__namespace._("Choose 1 answer:");
25188
+ }
25189
+
25190
+ function BaseRadio(props) {
25191
+ const {
25192
+ apiOptions,
25193
+ reviewModeRubric,
25194
+ choices,
25195
+ editMode,
25196
+ multipleSelect,
25197
+ labelWrap,
25198
+ countChoices,
25199
+ numCorrect,
25200
+ isLastUsedWidget,
25201
+ registerFocusFunction
25202
+ } = props; // useEffect doesn't have previous props
25203
+
25204
+ const prevReviewModeRubric = React.useRef();
25205
+ const choiceRefs = React.useRef([]);
25206
+ React.useEffect(() => {
25207
+ // Switching into review mode can sometimes cause the selected answer
25402
25208
  // to scroll out of view - for example, when we reveal all those tall
25403
25209
  // rationales. This can be disorienting for the user.
25404
25210
  //
@@ -25411,17 +25217,25 @@ class BaseRadio extends React__namespace.Component {
25411
25217
  // the most recently used widget (because, if the user has since
25412
25218
  // touched something else, they're probably not trying to keep their
25413
25219
  // eye on this widget anymore).
25414
-
25415
- if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevProps.reviewModeRubric) {
25220
+ if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevReviewModeRubric.current) {
25416
25221
  const checkedIndex = choices.findIndex(c => c.checked);
25417
25222
 
25418
25223
  if (checkedIndex >= 0) {
25419
- const checkedNode = ReactDOM__default["default"].findDOMNode( // eslint-disable-next-line react/no-string-refs
25420
- this.refs["radio" + checkedIndex]);
25421
- scrollElementIntoView(checkedNode);
25224
+ const ref = choiceRefs.current[checkedIndex]; // note(matthew): we know this is only getting passed
25225
+ // to a WB Clickable button, so we force it to be of
25226
+ // type HTMLButtonElement
25227
+
25228
+ const anyNode = ReactDOM__default["default"].findDOMNode(ref.current);
25229
+ const buttonNode = anyNode;
25230
+
25231
+ if (buttonNode) {
25232
+ scrollElementIntoView(buttonNode);
25233
+ }
25422
25234
  }
25423
25235
  }
25424
- } // When a particular choice's `onChange` handler is called, indicating a
25236
+
25237
+ prevReviewModeRubric.current = reviewModeRubric;
25238
+ }, [apiOptions, choices, isLastUsedWidget, reviewModeRubric]); // When a particular choice's `onChange` handler is called, indicating a
25425
25239
  // change in a single choice's values, we need to call our `onChange`
25426
25240
  // handler in order to notify our parent. However, our API with our parent
25427
25241
  // is that we always provide *all* values for *all* choices, even if just
@@ -25430,181 +25244,197 @@ class BaseRadio extends React__namespace.Component {
25430
25244
  // will usually cause the old answer to become unchecked.)
25431
25245
  //
25432
25246
  // So, given the new values for a particular choice, compute the new values
25433
- // for all choices, and pass them to `this.props.onChange`.
25247
+ // for all choices, and pass them to `onChange`.
25434
25248
  //
25435
25249
  // `newValues` is an object with two keys: `checked` and `crossedOut`. Each
25436
25250
  // contains a boolean value specifying the new checked and crossed-out
25437
25251
  // value of this choice.
25438
25252
 
25253
+ function updateChoice(choiceIndex, newValues) {
25254
+ const {
25255
+ multipleSelect,
25256
+ choices,
25257
+ onChange
25258
+ } = props; // Get the baseline `checked` values. If we're checking a new answer
25259
+ // and multiple-select is not on, we should clear all choices to be
25260
+ // unchecked. Otherwise, we should copy the old checked values.
25439
25261
 
25440
- render() {
25441
- const inputType = this.props.multipleSelect ? "checkbox" : "radio";
25442
- const rubric = this.props.reviewModeRubric;
25443
- const reviewMode = !!rubric;
25444
- const styles = BaseRadio.styles;
25445
- const sat = this.props.apiOptions.satStyling;
25446
- const isMobile = this.props.apiOptions.isMobile;
25447
- const choices = this.props.choices;
25448
- const firstChoiceHighlighted = choices[0].highlighted;
25449
- const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25450
- const className = classNames__default["default"]("perseus-widget-radio", !this.props.editMode && "perseus-rendered-radio", aphrodite.css(styles.radio, // SAT doesn't use the "responsive styling" as it conflicts
25451
- // with their custom theming.
25452
- !sat && styles.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles.radioContainerLastHighlighted, sat && styles.satRadio));
25453
- const instructionsClassName = classNames__default["default"]("instructions", aphrodite.css(styles.instructions, isMobile && styles.instructionsMobile));
25454
- const instructions = this.getInstructionsText();
25455
- const shouldShowInstructions = !sat;
25456
- const responsiveClassName = aphrodite.css(styles.responsiveFieldset);
25457
- const fieldset = /*#__PURE__*/React__namespace.createElement("fieldset", {
25458
- className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25459
- }, /*#__PURE__*/React__namespace.createElement("legend", {
25460
- className: "perseus-sr-only"
25461
- }, instructions), shouldShowInstructions && /*#__PURE__*/React__namespace.createElement("div", {
25462
- className: instructionsClassName
25463
- }, instructions), /*#__PURE__*/React__namespace.createElement("ul", {
25464
- className: className,
25465
- style: {
25466
- listStyle: "none"
25467
- }
25468
- }, this.props.choices.map(function (choice, i) {
25469
- let Element = Choice;
25470
- const elementProps = {
25471
- ref: "radio".concat(i),
25472
- apiOptions: this.props.apiOptions,
25473
- checked: choice.checked,
25474
- crossedOut: choice.crossedOut,
25475
- previouslyAnswered: choice.previouslyAnswered,
25476
- reviewMode,
25477
- correct: choice.correct,
25478
- rationale: choice.rationale,
25479
- content: choice.content,
25480
- disabled: this.props.apiOptions.readOnly || choice.disabled,
25481
- editMode: this.props.editMode,
25482
- groupName: this.state.radioGroupName,
25483
- isLastChoice: i === this.props.choices.length - 1,
25484
- showCorrectness: reviewMode || !!choice.showCorrectness,
25485
- showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25486
- type: inputType,
25487
- pos: i,
25488
- deselectEnabled: this.deselectEnabled(),
25489
- onChange: newValues => {
25490
- this.updateChoice(i, newValues);
25491
- },
25492
- goToPrevChoice: () => this.goToChoice(i - 1),
25493
- goToNextChoice: () => this.goToChoice(i + 1)
25494
- };
25262
+ let newCheckedList;
25495
25263
 
25496
- if (choice.isNoneOfTheAbove) {
25497
- Element = ChoiceNoneAbove;
25264
+ if (newValues.checked && !multipleSelect) {
25265
+ newCheckedList = choices.map(_ => false);
25266
+ } else {
25267
+ newCheckedList = choices.map(c => c.checked);
25268
+ } // Get the baseline `crossedOut` values.
25498
25269
 
25499
- ___default["default"].extend(elementProps, {
25500
- showContent: choice.revealNoneOfTheAbove
25501
- });
25502
- }
25503
25270
 
25504
- const nextChoice = this.props.choices[i + 1];
25505
- const nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25506
-
25507
- const aphroditeClassName = checked => {
25508
- // Whether or not to show correctness borders
25509
- // for this choice and the next choice.
25510
- const satShowCorrectness = sat && reviewMode && checked;
25511
- const satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25512
- return aphrodite.css(sharedStyles.aboveScratchpad, styles.item, !sat && styles.responsiveItem, !sat && checked && styles.selectedItem, !sat && checked && choice.highlighted && styles.aboveBackdrop, !sat && checked && choice.highlighted && this.props.apiOptions.isMobile && styles.aboveBackdropMobile, !sat && nextChoiceHighlighted && this.props.apiOptions.isMobile && styles.nextHighlighted, sat && styles.satRadioOption, satShowCorrectness && !choice.correct && styles.satRadioOptionIncorrect, satShowCorrectness && choice.correct && styles.satRadioOptionCorrect, satShowCorrectnessNext && !nextChoice.correct && styles.satRadioOptionNextIncorrect, satShowCorrectnessNext && nextChoice.correct && styles.satRadioOptionNextCorrect, sat && rubric && styles.satReviewRadioOption);
25513
- }; // HACK(abdulrahman): Preloads the selection-state
25514
- // css because of a bug that causes iOS to lag
25515
- // when selecting the button for the first time.
25516
-
25517
-
25518
- aphroditeClassName(true);
25519
- const className = classNames__default["default"](aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25520
- // classNames
25521
- ClassNames.RADIO.OPTION, choice.checked && ClassNames.RADIO.SELECTED, reviewMode && rubric.choices[i].correct && ClassNames.CORRECT, reviewMode && !rubric.choices[i].correct && ClassNames.INCORRECT); // In edit mode, the Choice renders a Div in order to
25522
- // allow for the contentEditable area to be selected
25523
- // (label forces any clicks inside to select the input
25524
- // element) We have to add some extra behavior to make
25525
- // sure that we can still check the choice.
25526
-
25527
- let listElem = null;
25528
- let clickHandler = null;
25529
-
25530
- if (this.props.editMode) {
25531
- clickHandler = e => {
25532
- // Traverse the parent nodes of the clicked
25533
- // element.
25534
- let elem = e.target;
25535
-
25536
- while (elem && elem !== listElem) {
25537
- // If the clicked element is inside of the
25538
- // radio icon, then we want to trigger the
25539
- // check by flipping the choice of the icon.
25540
- if (elem.getAttribute("data-is-radio-icon")) {
25541
- this.updateChoice(i, {
25542
- checked: !choice.checked
25543
- });
25544
- return;
25545
- }
25271
+ const newCrossedOutList = choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25272
+
25273
+ newCheckedList[choiceIndex] = newValues.checked;
25274
+ newCrossedOutList[choiceIndex] = newValues.crossedOut;
25275
+ onChange({
25276
+ checked: newCheckedList,
25277
+ crossedOut: newCrossedOutList
25278
+ });
25279
+ } // register a callback with the parent that allows
25280
+ // the parent to focus an individual choice
25281
+
25282
+
25283
+ registerFocusFunction === null || registerFocusFunction === void 0 ? void 0 : registerFocusFunction(choiceIndex => {
25284
+ const ref = choiceRefs.current[choiceIndex || 0]; // note(matthew): we know this is only getting passed
25285
+ // to a WB Clickable button, so we force it to be of
25286
+ // type HTMLButtonElement
25287
+
25288
+ const anyNode = ReactDOM__default["default"].findDOMNode(ref.current);
25289
+ const buttonNode = anyNode;
25290
+
25291
+ if (buttonNode) {
25292
+ buttonNode.focus();
25293
+ } else {
25294
+ return false;
25295
+ }
25296
+
25297
+ return true;
25298
+ }); // some commonly used shorthands
25299
+
25300
+ const reviewMode = !!reviewModeRubric;
25301
+ const sat = apiOptions.satStyling;
25302
+ const isMobile = apiOptions.isMobile;
25303
+ const firstChoiceHighlighted = choices[0].highlighted;
25304
+ const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25305
+ const className = classNames__default["default"]("perseus-widget-radio", !editMode && "perseus-rendered-radio", aphrodite.css(styles$r.radio, // SAT doesn't use the "responsive styling" as it conflicts
25306
+ // with their custom theming.
25307
+ !sat && styles$r.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles$r.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles$r.radioContainerLastHighlighted, sat && styles$r.satRadio));
25308
+ const instructionsClassName = classNames__default["default"]("instructions", aphrodite.css(styles$r.instructions, isMobile && styles$r.instructionsMobile));
25309
+ const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
25310
+ const shouldShowInstructions = !sat;
25311
+ const responsiveClassName = aphrodite.css(styles$r.responsiveFieldset);
25312
+ const fieldset = /*#__PURE__*/React__namespace.createElement("fieldset", {
25313
+ className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25314
+ }, /*#__PURE__*/React__namespace.createElement("legend", {
25315
+ className: "perseus-sr-only"
25316
+ }, instructions), shouldShowInstructions && /*#__PURE__*/React__namespace.createElement("div", {
25317
+ className: instructionsClassName,
25318
+ "aria-hidden": "true"
25319
+ }, instructions), /*#__PURE__*/React__namespace.createElement("ul", {
25320
+ className: className,
25321
+ style: {
25322
+ listStyle: "none"
25323
+ }
25324
+ }, choices.map(function (choice, i) {
25325
+ let Element = Choice$1;
25326
+ const ref = /*#__PURE__*/React__namespace.createRef();
25327
+ choiceRefs.current[i] = ref;
25328
+ const elementProps = {
25329
+ apiOptions: apiOptions,
25330
+ multipleSelect: multipleSelect,
25331
+ checked: choice.checked,
25332
+ crossedOut: choice.crossedOut,
25333
+ previouslyAnswered: choice.previouslyAnswered,
25334
+ reviewMode,
25335
+ correct: choice.correct,
25336
+ rationale: choice.rationale,
25337
+ content: choice.content,
25338
+ disabled: apiOptions.readOnly || choice.disabled,
25339
+ showCorrectness: reviewMode || !!choice.showCorrectness,
25340
+ showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25341
+ pos: i,
25342
+ onChange: newValues => {
25343
+ updateChoice(i, newValues);
25344
+ },
25345
+ ref
25346
+ };
25347
+
25348
+ if (choice.isNoneOfTheAbove) {
25349
+ Element = ChoiceNoneAbove$1;
25546
25350
 
25547
- elem = elem.parentNode;
25351
+ ___default["default"].extend(elementProps, {
25352
+ showContent: choice.revealNoneOfTheAbove
25353
+ });
25354
+ }
25355
+
25356
+ const nextChoice = choices[i + 1];
25357
+ const nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25358
+
25359
+ const aphroditeClassName = checked => {
25360
+ // Whether or not to show correctness borders
25361
+ // for this choice and the next choice.
25362
+ const satShowCorrectness = sat && reviewMode && checked;
25363
+ const satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25364
+ return aphrodite.css(sharedStyles.aboveScratchpad, styles$r.item, !sat && styles$r.responsiveItem, !sat && checked && styles$r.selectedItem, !sat && checked && choice.highlighted && styles$r.aboveBackdrop, !sat && checked && choice.highlighted && apiOptions.isMobile && styles$r.aboveBackdropMobile, !sat && nextChoiceHighlighted && apiOptions.isMobile && styles$r.nextHighlighted, sat && styles$r.satRadioOption, satShowCorrectness && !choice.correct && styles$r.satRadioOptionIncorrect, satShowCorrectness && choice.correct && styles$r.satRadioOptionCorrect, satShowCorrectnessNext && !nextChoice.correct && styles$r.satRadioOptionNextIncorrect, satShowCorrectnessNext && nextChoice.correct && styles$r.satRadioOptionNextCorrect, sat && reviewModeRubric && styles$r.satReviewRadioOption);
25365
+ }; // HACK(abdulrahman): Preloads the selection-state
25366
+ // css because of a bug that causes iOS to lag
25367
+ // when selecting the button for the first time.
25368
+
25369
+
25370
+ aphroditeClassName(true);
25371
+ let correctnessClass; // reviewMode is only true if there's a rubric
25372
+ // but Flow doesn't understand that
25373
+
25374
+ if (reviewMode && reviewModeRubric) {
25375
+ correctnessClass = reviewModeRubric.choices[i].correct ? ClassNames.CORRECT : ClassNames.INCORRECT;
25376
+ }
25377
+
25378
+ const className = classNames__default["default"](aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25379
+ // classNames
25380
+ ClassNames.RADIO.OPTION, choice.checked && ClassNames.RADIO.SELECTED, correctnessClass); // In edit mode, the Choice renders a Div in order to
25381
+ // allow for the contentEditable area to be selected
25382
+ // (label forces any clicks inside to select the input
25383
+ // element) We have to add some extra behavior to make
25384
+ // sure that we can still check the choice.
25385
+
25386
+ let listElem = null;
25387
+ let clickHandler = null;
25388
+
25389
+ if (editMode) {
25390
+ clickHandler = e => {
25391
+ // Traverse the parent nodes of the clicked
25392
+ // element.
25393
+ let elem = e.target;
25394
+
25395
+ while (elem && elem !== listElem) {
25396
+ // If the clicked element is inside of the
25397
+ // radio icon, then we want to trigger the
25398
+ // check by flipping the choice of the icon.
25399
+ if (elem.getAttribute("data-is-radio-icon")) {
25400
+ this.updateChoice(i, {
25401
+ checked: !choice.checked,
25402
+ crossedOut: choice.crossedOut
25403
+ });
25404
+ return;
25548
25405
  }
25549
- };
25550
- } // TODO(mattdr): Index isn't a *good* choice of key
25551
- // here; is there a better one? Can we use choice
25552
- // content somehow? Would changing our choice of key
25553
- // somehow break something happening inside a choice's
25554
- // child Renderers, by changing when we mount/unmount?
25555
25406
 
25407
+ elem = elem.parentNode;
25408
+ }
25409
+ };
25410
+ } // TODO(mattdr): Index isn't a *good* choice of key
25411
+ // here; is there a better one? Can we use choice
25412
+ // content somehow? Would changing our choice of key
25413
+ // somehow break something happening inside a choice's
25414
+ // child Renderers, by changing when we mount/unmount?
25556
25415
 
25557
- return /*#__PURE__*/React__namespace.createElement("li", {
25558
- key: i,
25559
- ref: e => listElem = e,
25560
- className: className,
25561
- onClick: clickHandler,
25562
- onTouchStart: !this.props.labelWrap ? null : captureScratchpadTouchStart$2
25563
- }, /*#__PURE__*/React__namespace.createElement(Element, elementProps));
25564
- }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25565
- // an issue especially on phones.
25566
- // This is disabled in SAT, since it conflicts with their theming.
25567
25416
 
25568
- return /*#__PURE__*/React__namespace.createElement("div", {
25569
- className: aphrodite.css(!sat && styles.responsiveContainer)
25570
- }, fieldset);
25571
- }
25417
+ return /*#__PURE__*/React__namespace.createElement("li", {
25418
+ key: i,
25419
+ ref: e => listElem = e,
25420
+ className: className,
25421
+ onClick: clickHandler,
25422
+ onTouchStart: !labelWrap ? null : captureScratchpadTouchStart$2
25423
+ }, /*#__PURE__*/React__namespace.createElement(Element, elementProps));
25424
+ }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25425
+ // an issue especially on phones.
25426
+ // This is disabled in SAT, since it conflicts with their theming.
25572
25427
 
25428
+ return /*#__PURE__*/React__namespace.createElement("div", {
25429
+ className: aphrodite.css(!sat && styles$r.responsiveContainer)
25430
+ }, fieldset);
25573
25431
  }
25574
25432
 
25575
- _defineProperty(BaseRadio, "propTypes", {
25576
- apiOptions: PropTypes__default["default"].shape({
25577
- readOnly: PropTypes__default["default"].bool,
25578
- satStyling: PropTypes__default["default"].bool,
25579
- isMobile: PropTypes__default["default"].bool,
25580
- styling: PropTypes__default["default"].shape({
25581
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"])
25582
- }),
25583
- canScrollPage: PropTypes__default["default"].bool
25584
- }),
25585
- choices: ChoicesType,
25586
- deselectEnabled: PropTypes__default["default"].bool,
25587
- editMode: PropTypes__default["default"].bool,
25588
- labelWrap: PropTypes__default["default"].bool,
25589
- countChoices: PropTypes__default["default"].bool,
25590
- numCorrect: PropTypes__default["default"].number,
25591
- multipleSelect: PropTypes__default["default"].bool,
25592
- reviewModeRubric: PropTypes__default["default"].shape({
25593
- choices: ChoicesType
25594
- }),
25595
- // A callback indicating that this choice has changed. Its argument is
25596
- // an object with two keys: `checked` and `crossedOut`. Each contains
25597
- // an array of boolean values, specifying the new checked and
25598
- // crossed-out value of each choice.
25599
- onChange: PropTypes__default["default"].func,
25600
- // Whether this widget was the most recently used widget in this
25601
- // Renderer. Determines whether we'll auto-scroll the page upon
25602
- // entering review mode.
25603
- isLastUsedWidget: PropTypes__default["default"].bool
25604
- });
25605
-
25606
- _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25607
- // eslint-disable-next-line react-native/no-unused-styles
25433
+ BaseRadio.defaultProps = {
25434
+ editMode: false,
25435
+ multipleSelect: false
25436
+ };
25437
+ const styles$r = aphrodite.StyleSheet.create({
25608
25438
  instructions: {
25609
25439
  display: "block",
25610
25440
  color: gray17,
@@ -25614,7 +25444,6 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25614
25444
  fontWeight: "bold",
25615
25445
  marginBottom: 16
25616
25446
  },
25617
- // eslint-disable-next-line react-native/no-unused-styles
25618
25447
  instructionsMobile: {
25619
25448
  fontSize: 18,
25620
25449
  [mediaQueries.smOrSmaller]: {
@@ -25628,36 +25457,30 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25628
25457
  fontSize: 20
25629
25458
  }
25630
25459
  },
25631
- // eslint-disable-next-line react-native/no-unused-styles
25632
25460
  radio: {
25633
25461
  // Avoid centering
25634
25462
  width: "100%"
25635
25463
  },
25636
- // eslint-disable-next-line react-native/no-unused-styles
25637
25464
  responsiveRadioContainer: {
25638
- borderBottom: "1px solid ".concat(radioBorderColor),
25639
- borderTop: "1px solid ".concat(radioBorderColor),
25465
+ borderBottom: "1px solid ".concat(radioBorderColor$1),
25466
+ borderTop: "1px solid ".concat(radioBorderColor$1),
25640
25467
  width: "auto",
25641
25468
  [mediaQueries.smOrSmaller]: {
25642
25469
  marginLeft: negativePhoneMargin,
25643
25470
  marginRight: negativePhoneMargin
25644
25471
  }
25645
25472
  },
25646
- // eslint-disable-next-line react-native/no-unused-styles
25647
25473
  radioContainerFirstHighlighted: {
25648
25474
  borderTop: "1px solid rgba(0, 0, 0, 0)"
25649
25475
  },
25650
- // eslint-disable-next-line react-native/no-unused-styles
25651
25476
  radioContainerLastHighlighted: {
25652
25477
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25653
25478
  },
25654
- // eslint-disable-next-line react-native/no-unused-styles
25655
25479
  satRadio: {
25656
25480
  background: "none",
25657
25481
  marginLeft: 0,
25658
25482
  userSelect: "none"
25659
25483
  },
25660
- // eslint-disable-next-line react-native/no-unused-styles
25661
25484
  satRadioOption: {
25662
25485
  margin: 0,
25663
25486
  padding: 0,
@@ -25666,57 +25489,40 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25666
25489
  borderTop: "1px solid #ccc"
25667
25490
  }
25668
25491
  },
25669
- // eslint-disable-next-line react-native/no-unused-styles
25670
25492
  satRadioOptionCorrect: {
25671
25493
  borderBottomColor: satCorrectBorderColor,
25672
25494
  ":first-child": {
25673
25495
  borderTopColor: satCorrectBorderColor
25674
25496
  }
25675
25497
  },
25676
- // eslint-disable-next-line react-native/no-unused-styles
25677
25498
  satRadioOptionIncorrect: {
25678
25499
  borderBottomColor: satIncorrectBorderColor,
25679
25500
  ":first-child": {
25680
25501
  borderTopColor: satIncorrectBorderColor
25681
25502
  }
25682
25503
  },
25683
- // eslint-disable-next-line react-native/no-unused-styles
25684
25504
  satRadioOptionNextCorrect: {
25685
25505
  borderBottomColor: satCorrectBorderColor
25686
25506
  },
25687
- // eslint-disable-next-line react-native/no-unused-styles
25688
25507
  satRadioOptionNextIncorrect: {
25689
25508
  borderBottomColor: satIncorrectBorderColor
25690
25509
  },
25691
- // eslint-disable-next-line react-native/no-unused-styles
25692
25510
  satReviewRadioOption: {
25693
25511
  pointerEvents: "none"
25694
25512
  },
25695
- // eslint-disable-next-line react-native/no-unused-styles
25696
25513
  item: {
25697
25514
  marginLeft: 20
25698
25515
  },
25699
- // eslint-disable-next-line react-native/no-unused-styles
25700
- inlineItem: {
25701
- display: "inline-block",
25702
- paddingLeft: 20,
25703
- verticalAlign: "middle" // See http://stackoverflow.com/q/8120466 for explanation of
25704
- // why vertical align property is needed
25705
-
25706
- },
25707
- // eslint-disable-next-line react-native/no-unused-styles
25708
25516
  responsiveItem: {
25709
25517
  marginLeft: 0,
25710
25518
  padding: 0,
25711
25519
  ":not(:last-child)": {
25712
- borderBottom: "1px solid ".concat(radioBorderColor)
25520
+ borderBottom: "1px solid ".concat(radioBorderColor$1)
25713
25521
  }
25714
25522
  },
25715
- // eslint-disable-next-line react-native/no-unused-styles
25716
25523
  selectedItem: {
25717
25524
  background: "white"
25718
25525
  },
25719
- // eslint-disable-next-line react-native/no-unused-styles
25720
25526
  aboveBackdrop: {
25721
25527
  position: "relative",
25722
25528
  // HACK(emily): We want selected choices to show up above our
@@ -25725,20 +25531,17 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25725
25531
  // coordinated between here and webapp. :(
25726
25532
  zIndex: 1062
25727
25533
  },
25728
- // eslint-disable-next-line react-native/no-unused-styles
25729
25534
  aboveBackdropMobile: {
25730
25535
  boxShadow: "0 0 4px 0 rgba(0, 0, 0, 0.2)," + "0 0 2px 0 rgba(0, 0, 0, 0.1)",
25731
25536
  ":not(:last-child)": {
25732
25537
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25733
25538
  }
25734
25539
  },
25735
- // eslint-disable-next-line react-native/no-unused-styles
25736
25540
  nextHighlighted: {
25737
25541
  ":not(:last-child)": {
25738
25542
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25739
25543
  }
25740
25544
  },
25741
- // eslint-disable-next-line react-native/no-unused-styles
25742
25545
  responsiveContainer: {
25743
25546
  overflow: "auto",
25744
25547
  marginLeft: negativePhoneMargin,
@@ -25746,22 +25549,22 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25746
25549
  paddingLeft: phoneMargin // paddingRight is handled by responsiveFieldset
25747
25550
 
25748
25551
  },
25749
- // eslint-disable-next-line react-native/no-unused-styles
25750
25552
  responsiveFieldset: {
25751
25553
  paddingRight: phoneMargin
25752
25554
  }
25753
- }));
25754
-
25755
- _defineProperty(BaseRadio, "defaultProps", {
25756
- editMode: false
25757
25555
  });
25758
25556
 
25759
25557
  class Radio$1 extends React__namespace.Component {
25760
25558
  constructor() {
25559
+ var _this;
25560
+
25761
25561
  super(...arguments);
25562
+ _this = this;
25563
+
25564
+ _defineProperty(this, "focusFunction", void 0);
25762
25565
 
25763
- _defineProperty(this, "_renderRenderer", content => {
25764
- content = content || "";
25566
+ _defineProperty(this, "_renderRenderer", function () {
25567
+ let content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
25765
25568
  let nextPassageRefId = 1;
25766
25569
  const widgets = {};
25767
25570
  const modContent = content.replace(/\{\{passage-ref (\d+) (\d+)(?: "([^"]*)")?\}\}/g, (match, passageNum, refNum, summaryText) => {
@@ -25793,19 +25596,14 @@ class Radio$1 extends React__namespace.Component {
25793
25596
  key: "choiceContentRenderer",
25794
25597
  content: modContent,
25795
25598
  widgets: widgets,
25796
- findExternalWidgets: this.props.findWidgets,
25599
+ findExternalWidgets: _this.props.findWidgets,
25797
25600
  alwaysUpdate: true,
25798
- linterContext: { ...this.props.linterContext,
25601
+ linterContext: { ..._this.props.linterContext,
25799
25602
  blockHighlight: true
25800
25603
  }
25801
25604
  });
25802
25605
  });
25803
25606
 
25804
- _defineProperty(this, "focus", i => {
25805
- // eslint-disable-next-line react/no-string-refs
25806
- return this.refs.baseRadio.focus(i);
25807
- });
25808
-
25809
25607
  _defineProperty(this, "updateChoices", newValueLists => {
25810
25608
  const {
25811
25609
  choiceStates,
@@ -25816,25 +25614,18 @@ class Radio$1 extends React__namespace.Component {
25816
25614
  // should clone the old `choiceStates` objects, in preparation to
25817
25615
  // mutate them.
25818
25616
 
25819
- let newChoiceStates;
25820
-
25821
- if (choiceStates) {
25822
- newChoiceStates = choiceStates.map(state => ({ ...state
25823
- }));
25824
- } else {
25825
- newChoiceStates = choices.map(() => ({
25826
- selected: false,
25827
- crossedOut: false,
25828
- highlighted: false,
25829
- rationaleShown: false,
25830
- correctnessShown: false,
25831
- previouslyAnswered: false,
25832
- readOnly: false
25833
- }));
25834
- } // Mutate the new `choiceState` objects, according to the new `checked`
25617
+ const newChoiceStates = choiceStates ? choiceStates.map(state => ({ ...state
25618
+ })) : choices.map(() => ({
25619
+ selected: false,
25620
+ crossedOut: false,
25621
+ highlighted: false,
25622
+ rationaleShown: false,
25623
+ correctnessShown: false,
25624
+ previouslyAnswered: false,
25625
+ readOnly: false
25626
+ })); // Mutate the new `choiceState` objects, according to the new `checked`
25835
25627
  // and `crossedOut` values provided in `newValueLists`.
25836
25628
 
25837
-
25838
25629
  newChoiceStates.forEach((choiceState, i) => {
25839
25630
  choiceState.selected = newValueLists.checked[i];
25840
25631
  choiceState.crossedOut = newValueLists.crossedOut[i];
@@ -25898,14 +25689,47 @@ class Radio$1 extends React__namespace.Component {
25898
25689
  });
25899
25690
  }
25900
25691
 
25692
+ // TODO(LP-10672): I think this might be unused right now. I can't find anywhere
25693
+ // that we pass a value to `.focus()` and it seems to have been used for
25694
+ // adding hints when editing.
25695
+ // See: https://github.com/Khan/perseus/blame/e18582b4b69959270b90e237ef1813899711ddfa/src/widgets/radio.js#L169
25696
+ focus(choiceIndex) {
25697
+ if (this.focusFunction) {
25698
+ return this.focusFunction(choiceIndex);
25699
+ }
25700
+
25701
+ return false;
25702
+ } // lets BaseRadio regiser a focus callback so widget
25703
+ // can focus an individual choice
25704
+
25705
+
25706
+ registerFocusFunction(fun) {
25707
+ this.focusFunction = fun;
25708
+ } // When `BaseRadio`'s `onChange` handler is called, indicating a change in
25709
+ // our choices' state, we need to call our `onChange` handler in order to
25710
+ // persist those changes in the item's Perseus state.
25711
+ //
25712
+ // So, given the new values for each choice, construct the new
25713
+ // `choiceStates` objects, and pass them to `this.props.onChange`.
25714
+ //
25715
+ // `newValueLists` is an object with two keys: `checked` and `crossedOut`.
25716
+ // Each contains an array of boolean values, specifying the new checked and
25717
+ // crossed-out value of each choice.
25718
+ //
25719
+ // NOTE(mdr): This method expects to be auto-bound. If this component is
25720
+ // converted to an ES6 class, take care to auto-bind this method!
25721
+
25722
+
25901
25723
  render() {
25902
- let choices = this.props.choices;
25724
+ const {
25725
+ choices
25726
+ } = this.props;
25903
25727
  let choiceStates;
25904
25728
 
25905
25729
  if (this.props.static) {
25906
- choiceStates = ___default["default"].map(choices, val => ({
25907
- selected: val.correct,
25908
- crossedOut: val.crossedOut,
25730
+ choiceStates = choices.map(choice => ({
25731
+ selected: !!choice.correct,
25732
+ crossedOut: false,
25909
25733
  readOnly: true,
25910
25734
  highlighted: false,
25911
25735
  rationaleShown: true,
@@ -25913,12 +25737,12 @@ class Radio$1 extends React__namespace.Component {
25913
25737
  previouslyAnswered: false
25914
25738
  }));
25915
25739
  } else if (this.props.choiceStates) {
25916
- choiceStates = this.props.choiceStates; // $FlowFixMe[prop-missing]
25740
+ choiceStates = this.props.choiceStates;
25917
25741
  } else if (this.props.values) {
25918
25742
  // Support legacy choiceStates implementation
25919
25743
 
25920
25744
  /* istanbul ignore next - props.values is deprecated */
25921
- choiceStates = ___default["default"].map(this.props.values, val => ({
25745
+ choiceStates = this.props.values.map(val => ({
25922
25746
  selected: val,
25923
25747
  crossedOut: false,
25924
25748
  readOnly: false,
@@ -25928,7 +25752,7 @@ class Radio$1 extends React__namespace.Component {
25928
25752
  previouslyAnswered: false
25929
25753
  }));
25930
25754
  } else {
25931
- choiceStates = ___default["default"].map(choices, () => ({
25755
+ choiceStates = choices.map(() => ({
25932
25756
  selected: false,
25933
25757
  crossedOut: false,
25934
25758
  readOnly: false,
@@ -25939,7 +25763,7 @@ class Radio$1 extends React__namespace.Component {
25939
25763
  }));
25940
25764
  }
25941
25765
 
25942
- choices = ___default["default"].map(choices, (choice, i) => {
25766
+ const choicesProp = choices.map((choice, i) => {
25943
25767
  const content = choice.isNoneOfTheAbove && !choice.content ? // we use i18n._ instead of $_ here because the content
25944
25768
  // sent to a renderer needs to be a string, not a react
25945
25769
  // node (/renderable/fragment).
@@ -25967,37 +25791,36 @@ class Radio$1 extends React__namespace.Component {
25967
25791
  // TODO(emily): Come up with a more comprehensive way to solve
25968
25792
  // this sort of "serialized state breaks when internal
25969
25793
  // structure changes" problem.
25970
- correct: typeof choice.correct === "undefined" ? !!reviewChoice && reviewChoice.correct : choice.correct,
25794
+ correct: choice.correct === undefined ? !!reviewChoice && !!reviewChoice.correct : choice.correct,
25971
25795
  disabled: readOnly,
25972
25796
  hasRationale: !!choice.clue,
25973
25797
  rationale: this._renderRenderer(choice.clue),
25974
25798
  showRationale: rationaleShown,
25975
25799
  showCorrectness: correctnessShown,
25976
- isNoneOfTheAbove: choice.isNoneOfTheAbove,
25977
- revealNoneOfTheAbove: this.props.questionCompleted && selected,
25800
+ isNoneOfTheAbove: !!choice.isNoneOfTheAbove,
25801
+ revealNoneOfTheAbove: !!(this.props.questionCompleted && selected),
25978
25802
  crossedOut,
25979
25803
  highlighted,
25980
25804
  previouslyAnswered: previouslyAnswered
25981
25805
  };
25982
25806
  });
25983
- return /*#__PURE__*/React__namespace.createElement(BaseRadio // eslint-disable-next-line react/no-string-refs
25984
- , {
25985
- ref: "baseRadio",
25807
+ return /*#__PURE__*/React__namespace.createElement(BaseRadio, {
25986
25808
  labelWrap: true,
25987
25809
  multipleSelect: this.props.multipleSelect,
25988
25810
  countChoices: this.props.countChoices,
25989
25811
  numCorrect: this.props.numCorrect,
25990
- choices: choices,
25812
+ choices: choicesProp,
25991
25813
  onChange: this.updateChoices,
25992
25814
  reviewModeRubric: this.props.reviewModeRubric,
25993
25815
  deselectEnabled: this.props.deselectEnabled,
25994
25816
  apiOptions: this.props.apiOptions,
25995
- isLastUsedWidget: this.props.isLastUsedWidget
25817
+ isLastUsedWidget: this.props.isLastUsedWidget,
25818
+ registerFocusFunction: i => this.registerFocusFunction(i)
25996
25819
  });
25997
25820
  }
25998
25821
 
25999
25822
  static validate(userInput, rubric) {
26000
- const numSelected = ___default["default"].reduce(userInput.choicesSelected, (sum, selected) => {
25823
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26001
25824
  return sum + (selected ? 1 : 0);
26002
25825
  }, 0);
26003
25826
 
@@ -26006,10 +25829,9 @@ class Radio$1 extends React__namespace.Component {
26006
25829
  type: "invalid",
26007
25830
  message: null
26008
25831
  };
26009
- } // $FlowFixMe[invalid-compare]
26010
-
25832
+ }
26011
25833
 
26012
- if (userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
25834
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
26013
25835
  return {
26014
25836
  type: "invalid",
26015
25837
  message: i18n__namespace._("Please choose the correct number of answers.")
@@ -26022,14 +25844,12 @@ class Radio$1 extends React__namespace.Component {
26022
25844
  message: i18n__namespace._("'None of the above' may not be selected " + "when other answers are selected.")
26023
25845
  };
26024
25846
  }
26025
- /* jshint -W018 */
26026
25847
 
26027
-
26028
- const correct = ___default["default"].all(userInput.choicesSelected, (selected, i) => {
25848
+ const correct = userInput.choicesSelected.every((selected, i) => {
26029
25849
  let isCorrect;
26030
25850
 
26031
25851
  if (userInput.noneOfTheAboveIndex === i) {
26032
- isCorrect = ___default["default"].all(rubric.choices, (choice, j) => {
25852
+ isCorrect = rubric.choices.every((choice, j) => {
26033
25853
  return i === j || !choice.correct;
26034
25854
  });
26035
25855
  } else {
@@ -26038,9 +25858,6 @@ class Radio$1 extends React__namespace.Component {
26038
25858
 
26039
25859
  return isCorrect === selected;
26040
25860
  });
26041
- /* jshint +W018 */
26042
-
26043
-
26044
25861
  return {
26045
25862
  type: "points",
26046
25863
  earned: correct ? 1 : 0,
@@ -26062,9 +25879,7 @@ class Radio$1 extends React__namespace.Component {
26062
25879
  const numCorrect = props.numCorrect;
26063
25880
 
26064
25881
  for (let i = 0; i < choicesSelected.length; i++) {
26065
- // $FlowFixMe[prop-missing]
26066
- const index = props.choices[i].originalIndex; // $FlowFixMe[incompatible-use]
26067
-
25882
+ const index = props.choices[i].originalIndex;
26068
25883
  choicesSelected[index] = choiceStates[i].selected;
26069
25884
 
26070
25885
  if (props.choices[i].isNoneOfTheAbove) {
@@ -26085,31 +25900,35 @@ class Radio$1 extends React__namespace.Component {
26085
25900
  }; // Support legacy choiceState implementation
26086
25901
  }
26087
25902
  /* istanbul ignore if - props.values is deprecated */
26088
- // $FlowFixMe[prop-missing]
26089
25903
 
26090
25904
 
26091
- if (props.values) {
25905
+ const {
25906
+ values
25907
+ } = props;
25908
+
25909
+ if (values) {
26092
25910
  let noneOfTheAboveIndex = null;
26093
25911
  let noneOfTheAboveSelected = false;
26094
- const values = props.values.slice();
25912
+ const choicesSelected = [...values];
26095
25913
  const countChoices = props.countChoices;
26096
25914
  const numCorrect = props.numCorrect;
25915
+ const valuesLength = values.length;
26097
25916
 
26098
- for (let i = 0; i < props.values.length; i++) {
25917
+ for (let i = 0; i < valuesLength; i++) {
26099
25918
  const index = props.choices[i].originalIndex;
26100
- values[index] = props.values[i];
25919
+ choicesSelected[index] = values[i];
26101
25920
 
26102
25921
  if (props.choices[i].isNoneOfTheAbove) {
26103
25922
  noneOfTheAboveIndex = index;
26104
25923
 
26105
- if (values[i]) {
25924
+ if (choicesSelected[i]) {
26106
25925
  noneOfTheAboveSelected = true;
26107
25926
  }
26108
25927
  }
26109
25928
  }
26110
25929
 
26111
25930
  return {
26112
- choicesSelected: values,
25931
+ choicesSelected,
26113
25932
  noneOfTheAboveIndex,
26114
25933
  noneOfTheAboveSelected,
26115
25934
  countChoices,
@@ -26119,15 +25938,14 @@ class Radio$1 extends React__namespace.Component {
26119
25938
 
26120
25939
 
26121
25940
  return {
26122
- choicesSelected: ___default["default"].map(props.choices, () => false)
25941
+ choicesSelected: props.choices.map(() => false)
26123
25942
  };
26124
25943
  }
26125
25944
 
26126
25945
  }
26127
25946
 
26128
25947
  _defineProperty(Radio$1, "defaultProps", {
26129
- choices: [{}],
26130
- displayCount: null,
25948
+ choices: [],
26131
25949
  multipleSelect: false,
26132
25950
  countChoices: false,
26133
25951
  deselectEnabled: false,
@@ -26158,14 +25976,14 @@ const _choiceTransform = (widgetOptions, problemNum) => {
26158
25976
 
26159
25977
  const _addNoneOfAbove = function (choices) {
26160
25978
  let noneOfTheAbove = null;
26161
-
26162
- const newChoices = ___default["default"].reject(choices, function (choice, index) {
25979
+ const newChoices = choices.filter((choice, index) => {
26163
25980
  if (choice.isNoneOfTheAbove) {
26164
25981
  noneOfTheAbove = choice;
26165
- return true;
25982
+ return false;
26166
25983
  }
26167
- }); // Place the "None of the above" options last
26168
25984
 
25985
+ return true;
25986
+ }); // Place the "None of the above" options last
26169
25987
 
26170
25988
  if (noneOfTheAbove) {
26171
25989
  newChoices.push(noneOfTheAbove);
@@ -26185,12 +26003,11 @@ const _choiceTransform = (widgetOptions, problemNum) => {
26185
26003
  }; // Add meta-information to choices
26186
26004
 
26187
26005
 
26188
- let choices = widgetOptions.choices.slice();
26189
- choices = ___default["default"].map(choices, (choice, i) => {
26190
- return ___default["default"].extend({}, choice, {
26006
+ const choices = widgetOptions.choices.map((choice, i) => {
26007
+ return { ...choice,
26191
26008
  originalIndex: i,
26192
26009
  correct: Boolean(choice.correct)
26193
- });
26010
+ };
26194
26011
  }); // Apply all the transforms. Note that the order we call these is
26195
26012
  // important!
26196
26013
  // 3) finally add "None of the above" to the bottom
@@ -26775,7 +26592,7 @@ class Definition extends React__namespace.Component {
26775
26592
  }, /*#__PURE__*/React__namespace.createElement("span", {
26776
26593
  className: "perseus-widget-definition"
26777
26594
  }, /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
26778
- size: "small",
26595
+ size: "medium",
26779
26596
  kind: "tertiary",
26780
26597
  onClick: () => {
26781
26598
  this.props.trackInteraction();
@@ -28984,6 +28801,8 @@ class Group extends React__namespace.Component {
28984
28801
  super(...arguments);
28985
28802
  _this = this;
28986
28803
 
28804
+ _defineProperty(this, "rendererRef", void 0);
28805
+
28987
28806
  _defineProperty(this, "change", function () {
28988
28807
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
28989
28808
  args[_key] = arguments[_key];
@@ -28993,56 +28812,66 @@ class Group extends React__namespace.Component {
28993
28812
  });
28994
28813
 
28995
28814
  _defineProperty(this, "getUserInput", () => {
28996
- // eslint-disable-next-line react/no-string-refs
28997
- return this.refs.renderer.getUserInput();
28815
+ var _this$rendererRef;
28816
+
28817
+ return (_this$rendererRef = this.rendererRef) === null || _this$rendererRef === void 0 ? void 0 : _this$rendererRef.getUserInput();
28998
28818
  });
28999
28819
 
29000
28820
  _defineProperty(this, "getSerializedState", () => {
29001
- // eslint-disable-next-line react/no-string-refs
29002
- return this.refs.renderer.getSerializedState();
28821
+ var _this$rendererRef2;
28822
+
28823
+ return (_this$rendererRef2 = this.rendererRef) === null || _this$rendererRef2 === void 0 ? void 0 : _this$rendererRef2.getSerializedState();
29003
28824
  });
29004
28825
 
29005
28826
  _defineProperty(this, "restoreSerializedState", (state, callback) => {
29006
- // eslint-disable-next-line react/no-string-refs
29007
- this.refs.renderer.restoreSerializedState(state, callback); // Tell our renderer that we have no props to change
28827
+ var _this$rendererRef3;
28828
+
28829
+ (_this$rendererRef3 = this.rendererRef) === null || _this$rendererRef3 === void 0 ? void 0 : _this$rendererRef3.restoreSerializedState(state, callback); // Tell our renderer that we have no props to change
29008
28830
  // (all our changes were in state):
29009
28831
 
29010
28832
  return null;
29011
28833
  });
29012
28834
 
29013
28835
  _defineProperty(this, "simpleValidate", rubric => {
29014
- // eslint-disable-next-line react/no-string-refs
29015
- return this.refs.renderer.score();
28836
+ var _this$rendererRef4;
28837
+
28838
+ return (_this$rendererRef4 = this.rendererRef) === null || _this$rendererRef4 === void 0 ? void 0 : _this$rendererRef4.score();
29016
28839
  });
29017
28840
 
29018
28841
  _defineProperty(this, "getInputPaths", () => {
29019
- // eslint-disable-next-line react/no-string-refs
29020
- return this.refs.renderer.getInputPaths();
28842
+ var _this$rendererRef5;
28843
+
28844
+ return (_this$rendererRef5 = this.rendererRef) === null || _this$rendererRef5 === void 0 ? void 0 : _this$rendererRef5.getInputPaths();
29021
28845
  });
29022
28846
 
29023
- _defineProperty(this, "setInputValue", (path, newValue, cb) => {
29024
- // eslint-disable-next-line react/no-string-refs
29025
- return this.refs.renderer.setInputValue(path, newValue, cb);
28847
+ _defineProperty(this, "setInputValue", (path, newValue, callback) => {
28848
+ var _this$rendererRef6;
28849
+
28850
+ return (_this$rendererRef6 = this.rendererRef) === null || _this$rendererRef6 === void 0 ? void 0 : _this$rendererRef6.setInputValue(path, newValue, callback);
29026
28851
  });
29027
28852
 
29028
28853
  _defineProperty(this, "focus", () => {
29029
- // eslint-disable-next-line react/no-string-refs
29030
- return this.refs.renderer.focus();
28854
+ var _this$rendererRef7;
28855
+
28856
+ return (_this$rendererRef7 = this.rendererRef) === null || _this$rendererRef7 === void 0 ? void 0 : _this$rendererRef7.focus();
29031
28857
  });
29032
28858
 
29033
28859
  _defineProperty(this, "focusInputPath", path => {
29034
- // eslint-disable-next-line react/no-string-refs
29035
- this.refs.renderer.focusPath(path);
28860
+ var _this$rendererRef8;
28861
+
28862
+ (_this$rendererRef8 = this.rendererRef) === null || _this$rendererRef8 === void 0 ? void 0 : _this$rendererRef8.focusPath(path);
29036
28863
  });
29037
28864
 
29038
28865
  _defineProperty(this, "blurInputPath", path => {
29039
- // eslint-disable-next-line react/no-string-refs
29040
- this.refs.renderer.blurPath(path);
28866
+ var _this$rendererRef9;
28867
+
28868
+ (_this$rendererRef9 = this.rendererRef) === null || _this$rendererRef9 === void 0 ? void 0 : _this$rendererRef9.blurPath(path);
29041
28869
  });
29042
28870
 
29043
28871
  _defineProperty(this, "showRationalesForCurrentlySelectedChoices", () => {
29044
- // eslint-disable-next-line react/no-string-refs
29045
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
28872
+ var _this$rendererRef10;
28873
+
28874
+ (_this$rendererRef10 = this.rendererRef) === null || _this$rendererRef10 === void 0 ? void 0 : _this$rendererRef10.showRationalesForCurrentlySelectedChoices();
29046
28875
  });
29047
28876
  }
29048
28877
 
@@ -29054,7 +28883,10 @@ class Group extends React__namespace.Component {
29054
28883
  }
29055
28884
 
29056
28885
  render() {
29057
- const apiOptions = ___default["default"].extend({}, ApiOptions.defaults, this.props.apiOptions, {
28886
+ var _this$rendererRef11;
28887
+
28888
+ const apiOptions = { ...ApiOptions.defaults,
28889
+ ...this.props.apiOptions,
29058
28890
  // Api Rewriting to support correct onFocus/onBlur
29059
28891
  // events for the mobile API
29060
28892
  onFocusChange: (newFocus, oldFocus) => {
@@ -29066,7 +28898,7 @@ class Group extends React__namespace.Component {
29066
28898
  this.props.onFocus(newFocus);
29067
28899
  }
29068
28900
  }
29069
- }); // Allow a problem number annotation to be added.
28901
+ }; // Allow a problem number annotation to be added.
29070
28902
  // This is cyclical and should probably be reconsidered. In order to
29071
28903
  // render the annotation ("Question 3 of 10"), we call findWidgets to
29072
28904
  // figure out our index in the list of all fellow group widgets. On
@@ -29075,9 +28907,8 @@ class Group extends React__namespace.Component {
29075
28907
  // componentDidMount so that we can number ourselves properly. But,
29076
28908
  // really we should have a more unidirectional flow. TODO(marcia): fix.
29077
28909
 
29078
-
29079
- const number = ___default["default"].indexOf(this.props.findWidgets("group"), this);
29080
-
28910
+ const groupWidgets = this.props.findWidgets("group");
28911
+ const number = groupWidgets.indexOf(this);
29081
28912
  const problemNumComponent = this.props.apiOptions.groupAnnotator(number, this.props.widgetId); // This is a little strange because the id of the widget that actually
29082
28913
  // changed is going to be lost in favor of the group widget's id. The
29083
28914
  // widgets prop also wasn't actually changed, and this only serves to
@@ -29085,15 +28916,12 @@ class Group extends React__namespace.Component {
29085
28916
  // has occurred.
29086
28917
 
29087
28918
  const onInteractWithWidget = id => {
29088
- // eslint-disable-next-line react/no-string-refs
29089
- if (this.refs.renderer) {
29090
- // eslint-disable-next-line react/no-string-refs
29091
- this.change("widgets", this.refs.renderer.props.widgets);
28919
+ if (this.rendererRef) {
28920
+ this.change("widgets", this.rendererRef.props.widgets);
29092
28921
  }
29093
- }; // eslint-disable-next-line react/no-string-refs
29094
-
28922
+ };
29095
28923
 
29096
- const score = this.refs.renderer && this.refs.renderer.score();
28924
+ const score = (_this$rendererRef11 = this.rendererRef) === null || _this$rendererRef11 === void 0 ? void 0 : _this$rendererRef11.score();
29097
28925
  const isValid = score && score.type !== "invalid";
29098
28926
  const isInvalid = score && score.type === "invalid"; // TODO(mdr): Widgets inside this Renderer are not discoverable through
29099
28927
  // the parent Renderer's `findWidgets` function.
@@ -29104,15 +28932,17 @@ class Group extends React__namespace.Component {
29104
28932
  "perseus-group-valid-answer": isValid,
29105
28933
  "perseus-group-invalid-answer": isInvalid
29106
28934
  })
29107
- }, problemNumComponent, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, this.props, {
29108
- // eslint-disable-next-line react/no-string-refs
29109
- ref: "renderer",
28935
+ }, problemNumComponent, /*#__PURE__*/React__namespace.createElement(Renderer, {
28936
+ content: this.props.content,
28937
+ widgets: this.props.widgets,
28938
+ images: this.props.images,
28939
+ ref: ref => this.rendererRef = ref,
29110
28940
  apiOptions: apiOptions,
29111
28941
  findExternalWidgets: this.props.findWidgets,
29112
28942
  reviewMode: !!this.props.reviewModeRubric,
29113
28943
  onInteractWithWidget: onInteractWithWidget,
29114
28944
  linterContext: this.props.linterContext
29115
- })), this.props.icon && /*#__PURE__*/React__namespace.createElement("div", {
28945
+ }), this.props.icon && /*#__PURE__*/React__namespace.createElement("div", {
29116
28946
  className: "group-icon"
29117
28947
  }, this.props.icon));
29118
28948
  }
@@ -29127,7 +28957,9 @@ _defineProperty(Group, "defaultProps", {
29127
28957
  });
29128
28958
 
29129
28959
  const traverseChildWidgets$1 = function (props, traverseRenderer) {
29130
- return ___default["default"].extend({}, props, traverseRenderer(props));
28960
+ return { ...props,
28961
+ ...traverseRenderer(props)
28962
+ };
29131
28963
  };
29132
28964
 
29133
28965
  var Group$1 = {
@@ -40994,43 +40826,23 @@ const styles$9 = aphrodite.StyleSheet.create({
40994
40826
  }
40995
40827
  });
40996
40828
 
40997
- const getLineHeightForNode = ($line1, $line2) => {
40998
- return $line2.offset().top - $line1.offset().top;
40829
+ const getLineHeightForNode = (line1, line2) => {
40830
+ return $__default["default"](line2).offset().top - $__default["default"](line1).offset().top;
40999
40831
  };
41000
40832
 
41001
- const START_REF_PREFIX = "start-ref-";
41002
- const END_REF_PREFIX = "end-ref-";
41003
- const REF_STYLE = {
41004
- display: "inline-block",
41005
- width: 0,
41006
- visibility: "hidden"
41007
- };
41008
- const LABEL_OUTER_STYLE = {
41009
- // for some reason we need these to keep the nbsp from wrapping when the
41010
- // inner circle/square is display: inline-block
41011
- display: "inline",
41012
- whiteSpace: "nowrap"
41013
- };
41014
- const SQUARE_LABEL_STYLE = {
41015
- display: "inline-block",
41016
- color: "rgb(255, 255, 255)",
41017
- backgroundColor: "rgb(90, 90, 90)",
41018
- paddingLeft: 10,
41019
- paddingRight: 10,
41020
- userSelect: "none",
41021
- WebkitUserSelect: "none"
41022
- };
41023
- const CIRCLE_LABEL_STYLE = {
41024
- display: "inline-block",
41025
- color: "rgb(255, 255, 255)",
41026
- backgroundColor: "rgb(90, 90, 90)",
41027
- userSelect: "none",
41028
- WebkitUserSelect: "none",
41029
- width: 22,
41030
- height: 22,
41031
- borderRadius: "50%",
41032
- textAlign: "center"
41033
- };
40833
+ function getInitialParseState() {
40834
+ return {
40835
+ currentRef: [],
40836
+ useRefs: true,
40837
+ lastRef: 0,
40838
+ firstSentenceRef: null,
40839
+ firstQuestionRef: null,
40840
+ lastFootnote: {
40841
+ id: 0,
40842
+ text: ""
40843
+ }
40844
+ };
40845
+ }
41034
40846
 
41035
40847
  class RefStart extends React__namespace.Component {
41036
40848
  constructor() {
@@ -41049,10 +40861,6 @@ class RefStart extends React__namespace.Component {
41049
40861
 
41050
40862
  }
41051
40863
 
41052
- _defineProperty(RefStart, "propTypes", {
41053
- refContent: PropTypes__default["default"].node.isRequired
41054
- });
41055
-
41056
40864
  class RefEnd extends React__namespace.Component {
41057
40865
  render() {
41058
40866
  return /*#__PURE__*/React__namespace.createElement("span", {
@@ -41063,20 +40871,11 @@ class RefEnd extends React__namespace.Component {
41063
40871
  }
41064
40872
 
41065
40873
  const rules = {
41066
- // $FlowFixMe[prop-missing]
41067
- // $FlowFixMe[incompatible-use]
41068
40874
  newline: SimpleMarkdown__default["default"].defaultRules.newline,
41069
- // $FlowFixMe[prop-missing]
41070
- // $FlowFixMe[incompatible-use]
41071
40875
  paragraph: SimpleMarkdown__default["default"].defaultRules.paragraph,
41072
- // $FlowFixMe[prop-missing]
41073
- // $FlowFixMe[incompatible-use]
41074
40876
  escape: SimpleMarkdown__default["default"].defaultRules.escape,
41075
40877
  passageFootnote: {
41076
- // $FlowFixMe[prop-missing]
41077
- // $FlowFixMe[incompatible-use]
41078
40878
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.1,
41079
- // $FlowFixMe[prop-missing]
41080
40879
  match: SimpleMarkdown__default["default"].inlineRegex(/^\^/),
41081
40880
  parse: (capture, parse, state) => {
41082
40881
  // if no footnotes have been seen, we're id 1. otherwise,
@@ -41105,10 +40904,8 @@ const rules = {
41105
40904
  }
41106
40905
  },
41107
40906
  refStart: {
41108
- // $FlowFixMe[prop-missing]
41109
- // $FlowFixMe[incompatible-use]
41110
40907
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.2,
41111
- match: function (source, state) {
40908
+ match: function (source) {
41112
40909
  const capture = /^\{\{/.exec(source);
41113
40910
 
41114
40911
  if (capture) {
@@ -41139,12 +40936,14 @@ const rules = {
41139
40936
  }
41140
40937
 
41141
40938
  closeIndex++;
41142
- }
41143
-
41144
- const refText = source.slice(2, closeIndex); // A "magic" capture that matches the opening {{
40939
+ } // A "magic" capture that matches the opening {{
41145
40940
  // but captures the full ref text internally :D
40941
+ // unfortunately the "magic" makes Flow upset,
40942
+ // so there's this questionable workaround
40943
+
41146
40944
 
41147
- return [capture[0], refText];
40945
+ capture[1] = source.slice(2, closeIndex);
40946
+ return capture;
41148
40947
  }
41149
40948
 
41150
40949
  return null;
@@ -41167,33 +40966,34 @@ const rules = {
41167
40966
  // to make those refs into react refs on the
41168
40967
  // passage, for instance!
41169
40968
  useRefs: false
41170
- }, INITIAL_PARSE_STATE));
40969
+ }, getInitialParseState()));
41171
40970
  return {
41172
40971
  ref: ref,
41173
40972
  refContent: refContent
41174
40973
  };
41175
40974
  },
41176
40975
  react: (node, output, state) => {
41177
- if (node.ref == null) {
40976
+ const ref = node.ref;
40977
+
40978
+ if (ref == null) {
41178
40979
  return null;
41179
40980
  } // We don't pass state here because this is parsed
41180
40981
  // and output out-of-band. We don't want to affect
41181
40982
  // our state by the double-output here :).
41182
40983
 
41183
40984
 
41184
- const refContent = output(node.refContent, {});
40985
+ const refContent = output(node.refContent, {}); // note(matthewc) the refs created here become the refs
40986
+ // pulled from `this.refs` in passage.jsx
40987
+
41185
40988
  return /*#__PURE__*/React__namespace.createElement(RefStart, {
41186
- ref: START_REF_PREFIX + node.ref,
41187
- key: START_REF_PREFIX + node.ref,
40989
+ ref: START_REF_PREFIX + ref,
40990
+ key: START_REF_PREFIX + ref,
41188
40991
  refContent: refContent
41189
40992
  });
41190
40993
  }
41191
40994
  },
41192
40995
  refEnd: {
41193
- // $FlowFixMe[prop-missing]
41194
- // $FlowFixMe[incompatible-use]
41195
40996
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.3,
41196
- // $FlowFixMe[prop-missing]
41197
40997
  match: SimpleMarkdown__default["default"].inlineRegex(/^\}\}/),
41198
40998
  parse: (capture, parse, state) => {
41199
40999
  if (!state.useRefs) {
@@ -41209,6 +41009,8 @@ const rules = {
41209
41009
  },
41210
41010
  react: (node, output, state) => {
41211
41011
  if (node.ref != null) {
41012
+ // note(matthewc) the refs created here become the refs
41013
+ // pulled from `this.refs` in passage.jsx
41212
41014
  return /*#__PURE__*/React__namespace.createElement(RefEnd, {
41213
41015
  ref: END_REF_PREFIX + node.ref,
41214
41016
  key: END_REF_PREFIX + node.ref
@@ -41222,10 +41024,7 @@ const rules = {
41222
41024
  }
41223
41025
  },
41224
41026
  squareLabel: {
41225
- // $FlowFixMe[prop-missing]
41226
- // $FlowFixMe[incompatible-use]
41227
41027
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.4,
41228
- // $FlowFixMe[prop-missing]
41229
41028
  match: SimpleMarkdown__default["default"].inlineRegex(/^\[\[(\w+)\]\]( *)/),
41230
41029
  parse: (capture, parse, state) => {
41231
41030
  if (!state.firstQuestionRef) {
@@ -41254,10 +41053,7 @@ const rules = {
41254
41053
  }
41255
41054
  },
41256
41055
  circleLabel: {
41257
- // $FlowFixMe[prop-missing]
41258
- // $FlowFixMe[incompatible-use]
41259
41056
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.5,
41260
- // $FlowFixMe[prop-missing]
41261
41057
  match: SimpleMarkdown__default["default"].inlineRegex(/^\(\((\w+)\)\)( *)/),
41262
41058
  parse: (capture, parse, state) => {
41263
41059
  return {
@@ -41282,10 +41078,7 @@ const rules = {
41282
41078
  }
41283
41079
  },
41284
41080
  squareBracketRef: {
41285
- // $FlowFixMe[prop-missing]
41286
- // $FlowFixMe[incompatible-use]
41287
41081
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.6,
41288
- // $FlowFixMe[prop-missing]
41289
41082
  match: SimpleMarkdown__default["default"].inlineRegex(/^\[(\d+)\]( *)/),
41290
41083
  parse: (capture, parse, state) => {
41291
41084
  if (!state.firstSentenceRef) {
@@ -41311,10 +41104,7 @@ const rules = {
41311
41104
  }
41312
41105
  },
41313
41106
  highlight: {
41314
- // $FlowFixMe[prop-missing]
41315
- // $FlowFixMe[incompatible-use]
41316
41107
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.7,
41317
- // $FlowFixMe[prop-missing]
41318
41108
  match: SimpleMarkdown__default["default"].inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
41319
41109
  parse: (capture, parse, state) => {
41320
41110
  return {
@@ -41329,10 +41119,7 @@ const rules = {
41329
41119
  }
41330
41120
  },
41331
41121
  reviewHighlight: {
41332
- // $FlowFixMe[prop-missing]
41333
- // $FlowFixMe[incompatible-use]
41334
41122
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.7,
41335
- // $FlowFixMe[prop-missing]
41336
41123
  match: SimpleMarkdown__default["default"].inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
41337
41124
  parse: (capture, parse, state) => {
41338
41125
  return {
@@ -41346,21 +41133,11 @@ const rules = {
41346
41133
  }, node.content)];
41347
41134
  }
41348
41135
  },
41349
- // $FlowFixMe[prop-missing]
41350
- // $FlowFixMe[incompatible-use]
41351
41136
  strong: SimpleMarkdown__default["default"].defaultRules.strong,
41352
- // $FlowFixMe[prop-missing]
41353
- // $FlowFixMe[incompatible-use]
41354
41137
  u: SimpleMarkdown__default["default"].defaultRules.u,
41355
- // $FlowFixMe[prop-missing]
41356
- // $FlowFixMe[incompatible-use]
41357
41138
  em: SimpleMarkdown__default["default"].defaultRules.em,
41358
- // $FlowFixMe[prop-missing]
41359
- // $FlowFixMe[incompatible-use]
41360
41139
  del: SimpleMarkdown__default["default"].defaultRules.del,
41361
- text: { // $FlowFixMe[prop-missing]
41362
- // $FlowFixMe[incompatible-use]
41363
- ...SimpleMarkdown__default["default"].defaultRules.text,
41140
+ text: { ...SimpleMarkdown__default["default"].defaultRules.text,
41364
41141
 
41365
41142
  react(node, output, state) {
41366
41143
  return /*#__PURE__*/React__namespace.createElement("span", {
@@ -41370,32 +41147,54 @@ const rules = {
41370
41147
 
41371
41148
  }
41372
41149
  };
41373
- const INITIAL_PARSE_STATE = {
41374
- currentRef: [],
41375
- useRefs: true,
41376
- lastRef: 0,
41377
- lastFootnote: {
41378
- id: 0,
41379
- text: ""
41380
- }
41381
- }; // $FlowFixMe[prop-missing]
41382
-
41150
+ const START_REF_PREFIX = "start-ref-";
41151
+ const END_REF_PREFIX = "end-ref-";
41152
+ const REF_STYLE = {
41153
+ display: "inline-block",
41154
+ width: 0,
41155
+ visibility: "hidden"
41156
+ };
41157
+ const LABEL_OUTER_STYLE = {
41158
+ // for some reason we need these to keep the nbsp from wrapping when the
41159
+ // inner circle/square is display: inline-block
41160
+ display: "inline",
41161
+ whiteSpace: "nowrap"
41162
+ };
41163
+ const SQUARE_LABEL_STYLE = {
41164
+ display: "inline-block",
41165
+ color: "rgb(255, 255, 255)",
41166
+ backgroundColor: "rgb(90, 90, 90)",
41167
+ paddingLeft: 10,
41168
+ paddingRight: 10,
41169
+ userSelect: "none",
41170
+ WebkitUserSelect: "none"
41171
+ };
41172
+ const CIRCLE_LABEL_STYLE = {
41173
+ display: "inline-block",
41174
+ color: "rgb(255, 255, 255)",
41175
+ backgroundColor: "rgb(90, 90, 90)",
41176
+ userSelect: "none",
41177
+ WebkitUserSelect: "none",
41178
+ width: 22,
41179
+ height: 22,
41180
+ borderRadius: "50%",
41181
+ textAlign: "center"
41182
+ };
41383
41183
  const builtParser = SimpleMarkdown__default["default"].parserFor(rules);
41384
41184
 
41385
41185
  const parse = (source, state) => {
41386
41186
  state = state || {};
41387
41187
  const paragraphedSource = source + "\n\n";
41388
- return builtParser(paragraphedSource, ___default["default"].extend(state, INITIAL_PARSE_STATE));
41188
+ return builtParser(paragraphedSource, ___default["default"].extend(state, getInitialParseState()));
41389
41189
  };
41390
41190
 
41191
+ const output = SimpleMarkdown__default["default"].reactFor(SimpleMarkdown__default["default"].ruleOutput(rules, "react"));
41391
41192
  var PassageMarkdown = {
41392
- parse: parse,
41393
- // $FlowFixMe[prop-missing]
41394
- output: SimpleMarkdown__default["default"].reactFor( // $FlowFixMe[prop-missing]
41395
- SimpleMarkdown__default["default"].ruleOutput(rules, "react")),
41396
- START_REF_PREFIX: START_REF_PREFIX,
41397
- END_REF_PREFIX: END_REF_PREFIX,
41398
- _rulesForTesting: rules
41193
+ parse,
41194
+ output,
41195
+ START_REF_PREFIX,
41196
+ END_REF_PREFIX,
41197
+ getInitialParseState
41399
41198
  };
41400
41199
 
41401
41200
  // A fake paragraph to measure the line height of the passage. In CSS we always
@@ -41408,9 +41207,9 @@ class LineHeightMeasurer extends React__namespace.Component {
41408
41207
 
41409
41208
  _defineProperty(this, "_cachedLineHeight", void 0);
41410
41209
 
41411
- _defineProperty(this, "$body", void 0);
41210
+ _defineProperty(this, "body", void 0);
41412
41211
 
41413
- _defineProperty(this, "$end", void 0);
41212
+ _defineProperty(this, "end", void 0);
41414
41213
  }
41415
41214
 
41416
41215
  measureLineHeight() {
@@ -41422,23 +41221,30 @@ class LineHeightMeasurer extends React__namespace.Component {
41422
41221
  }
41423
41222
 
41424
41223
  forceMeasureLineHeight() {
41425
- // Add some text which magically fills an entire line.
41426
- this.$body.text(" \u0080"); // Now, the line height is the difference between the top of the
41224
+ const body = this.body;
41225
+ const end = this.end;
41226
+
41227
+ if (!body || !end) {
41228
+ return;
41229
+ } // Add some text which magically fills an entire line.
41230
+
41231
+
41232
+ body.textContent = " \u0080"; // Now, the line height is the difference between the top of the
41427
41233
  // second line and the top of the first line.
41428
41234
 
41429
- this._cachedLineHeight = getLineHeightForNode(this.$body, this.$end); // Clear out the first line so it doesn't overlap the passage.
41235
+ this._cachedLineHeight = getLineHeightForNode(body, end); // Clear out the first line so it doesn't overlap the passage.
41430
41236
 
41431
- this.$body.text("");
41237
+ body.textContent = "";
41432
41238
  }
41433
41239
 
41434
41240
  render() {
41435
41241
  return /*#__PURE__*/React__namespace.createElement("div", {
41436
41242
  className: aphrodite.css(styles$8.measurer)
41437
41243
  }, /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement("div", {
41438
- ref: e => this.$body = $__default["default"](e),
41244
+ ref: ref => this.body = ref,
41439
41245
  className: "paragraph"
41440
41246
  }), /*#__PURE__*/React__namespace.createElement("div", {
41441
- ref: e => this.$end = $__default["default"](e)
41247
+ ref: ref => this.end = ref
41442
41248
  })));
41443
41249
  }
41444
41250
 
@@ -41457,9 +41263,11 @@ class Passage extends React__namespace.Component {
41457
41263
  constructor() {
41458
41264
  super(...arguments);
41459
41265
 
41460
- _defineProperty(this, "_onResize", void 0);
41266
+ _defineProperty(this, "_contentRef", void 0);
41461
41267
 
41462
- _defineProperty(this, "_lineHeightMeasurer", void 0);
41268
+ _defineProperty(this, "_lineHeightMeasurerRef", void 0);
41269
+
41270
+ _defineProperty(this, "_onResize", void 0);
41463
41271
 
41464
41272
  _defineProperty(this, "_stylesAppiedTimer", void 0);
41465
41273
 
@@ -41480,6 +41288,8 @@ class Passage extends React__namespace.Component {
41480
41288
  this._updateState();
41481
41289
 
41482
41290
  this._onResize = ___default["default"].throttle(() => {
41291
+ var _this$_lineHeightMeas;
41292
+
41483
41293
  // If we're rendering JIPT text, we won't have line numbers or a
41484
41294
  // line height measurer, so skip handling this resize.
41485
41295
  if (this.shouldRenderJipt()) {
@@ -41490,7 +41300,7 @@ class Passage extends React__namespace.Component {
41490
41300
  // listen for resize events.
41491
41301
 
41492
41302
 
41493
- this._lineHeightMeasurer.forceMeasureLineHeight();
41303
+ (_this$_lineHeightMeas = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas === void 0 ? void 0 : _this$_lineHeightMeas.forceMeasureLineHeight();
41494
41304
 
41495
41305
  this._updateState();
41496
41306
  }, 500);
@@ -41553,9 +41363,8 @@ class Passage extends React__namespace.Component {
41553
41363
  }
41554
41364
 
41555
41365
  _measureLines() {
41556
- // eslint-disable-next-line react/no-string-refs
41557
- const $renderer = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.content));
41558
- const contentsHeight = $renderer.height();
41366
+ const renderer = ReactDOM__default["default"].findDOMNode(this._contentRef);
41367
+ const contentsHeight = $__default["default"](renderer).height();
41559
41368
 
41560
41369
  const lineHeight = this._getLineHeight();
41561
41370
 
@@ -41582,7 +41391,9 @@ class Passage extends React__namespace.Component {
41582
41391
  }
41583
41392
 
41584
41393
  _getLineHeight() {
41585
- return this._lineHeightMeasurer.measureLineHeight();
41394
+ var _this$_lineHeightMeas2;
41395
+
41396
+ return ((_this$_lineHeightMeas2 = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas2 === void 0 ? void 0 : _this$_lineHeightMeas2.measureLineHeight()) || 0;
41586
41397
  }
41587
41398
 
41588
41399
  getLineCount() {
@@ -41598,12 +41409,16 @@ class Passage extends React__namespace.Component {
41598
41409
  * These are functions to support the passage refs inter-widgets feature
41599
41410
  * where other widgets can fetch the line numbers of a reference inside of
41600
41411
  * a passage.
41412
+ *
41413
+ * todo(matthewc): The refs are created by PassageMarkdown's refStart and refEnd,
41414
+ * somehow bubbling up to Passage's `this.refs`. This runs against
41415
+ * current best practices for refs by using string refs, but also
41416
+ * by breaking our expectation of explicit data flow.
41601
41417
  */
41602
41418
 
41603
41419
 
41604
41420
  _getStartRefLineNumber(referenceNumber) {
41605
- const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41606
-
41421
+ const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41607
41422
  const ref = this.refs[refRef];
41608
41423
 
41609
41424
  if (!ref) {
@@ -41626,8 +41441,7 @@ class Passage extends React__namespace.Component {
41626
41441
  }
41627
41442
 
41628
41443
  _getEndRefLineNumber(referenceNumber) {
41629
- const refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41630
-
41444
+ const refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber;
41631
41445
  const ref = this.refs[refRef];
41632
41446
 
41633
41447
  if (!ref) {
@@ -41664,9 +41478,8 @@ class Passage extends React__namespace.Component {
41664
41478
  }
41665
41479
 
41666
41480
  _convertPosToLineNumber(absoluteVPos) {
41667
- // eslint-disable-next-line react/no-string-refs
41668
- const $content = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.content));
41669
- const relativeVPos = absoluteVPos - $content.offset().top;
41481
+ const content = ReactDOM__default["default"].findDOMNode(this._contentRef);
41482
+ const relativeVPos = absoluteVPos - $__default["default"](content).offset().top;
41670
41483
 
41671
41484
  const lineHeight = this._getLineHeight();
41672
41485
 
@@ -41675,8 +41488,7 @@ class Passage extends React__namespace.Component {
41675
41488
  }
41676
41489
 
41677
41490
  _getRefContent(referenceNumber) {
41678
- const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41679
-
41491
+ const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41680
41492
  const ref = this.refs[refRef];
41681
41493
 
41682
41494
  if (!ref) {
@@ -41782,12 +41594,9 @@ class Passage extends React__namespace.Component {
41782
41594
  onSerializedHighlightsUpdate: this._handleSerializedHighlightsUpdate,
41783
41595
  serializedHighlights: this.props.highlights
41784
41596
  }, /*#__PURE__*/React__namespace.createElement("div", {
41785
- ref: "content"
41786
- }, /*#__PURE__*/React__namespace.createElement(LineHeightMeasurer // TODO(mdr): We found a new Flow error when upgrading:
41787
- // "e (null) This type is incompatible with this._lineHeightMeasurer (LineHeightMeasurer)"
41788
- // $FlowFixMe[incompatible-type](0.52.0->0.53.0)
41789
- , {
41790
- ref: e => this._lineHeightMeasurer = e
41597
+ ref: ref => this._contentRef = ref
41598
+ }, /*#__PURE__*/React__namespace.createElement(LineHeightMeasurer, {
41599
+ ref: ref => this._lineHeightMeasurerRef = ref
41791
41600
  }), PassageMarkdown.output(parsed)));
41792
41601
  }
41793
41602
 
@@ -41830,10 +41639,7 @@ class Passage extends React__namespace.Component {
41830
41639
  });
41831
41640
  }
41832
41641
 
41833
- const parseState = {
41834
- firstSentenceRef: null,
41835
- firstQuestionRef: null
41836
- }; // Replace the vertical double quote characters quoting text with
41642
+ const parseState = PassageMarkdown.getInitialParseState(); // Replace the vertical double quote characters quoting text with
41837
41643
  // an unicode left and right double quote characters. This would
41838
41644
  // avoid rendering right or left unicode double quotes on both
41839
41645
  // ends of a sentence wrapped by double quotes.