@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/es/index.js CHANGED
@@ -5,6 +5,7 @@ import { Rule, linterContextDefault, linterContextProps } from '@khanacademy/per
5
5
  import * as i18n from '@khanacademy/wonder-blocks-i18n';
6
6
  import classNames from 'classnames';
7
7
  import * as React from 'react';
8
+ import { useState, useEffect, useRef } from 'react';
8
9
  import _, { debounce } from 'underscore';
9
10
  import PropTypes from 'prop-types';
10
11
  import $ from 'jquery';
@@ -31,7 +32,7 @@ import Link from '@khanacademy/wonder-blocks-link';
31
32
 
32
33
  var version = {
33
34
  apiVersion: {
34
- major: 11,
35
+ major: 12,
35
36
  minor: 0
36
37
  },
37
38
  itemDataVersion: {
@@ -493,38 +494,9 @@ var ApiOptions = {
493
494
  // values. For now, though, you must either specify all fields
494
495
  // of `styling`, or omit the `styling` option entirely.
495
496
  styling: PropTypes.shape({
496
- // Which version of radio widget styles to use in non-SAT
497
- // contexts.
498
- //
499
- // "legacy" was the version of the widget display after XOM but
500
- // before we started adding MCR styles. It doesn't have support
501
- // for rationales. It has since been removed.
502
- //
503
- // "intermediate" is a design which adds several new additions
504
- // to the "legacy" styles such as:
505
- // 1. Using the XOM "desktop" styles (with a visible check icon
506
- // and lines in between choices) on mobile devices.
507
- // 2. Designs for rationales
508
- // 3. Using the single-select styles for multi-select styles
509
- //
510
- // "final" is a design which will build off of the
511
- // "intermediate" designs and adds some improved designs as well
512
- // as:
513
- // 1. a/b/c/d/etc. letters inside of the prompt check box
514
- // 2. New iconography and styles to indicate choice correctness
515
- //
516
- // The "legacy" and "intermediate" designs will be A/B tested
517
- // against each other to ensure that its changes don't cause
518
- // problems due to the new designs. Once the "intermediate"
519
- // designs are finished, they will be A/B tested against the
520
- // "final" designs.
521
- //
522
- // If no flag is provided, "legacy" styles will be shown.
523
- //
524
- // TODO(emily): Remove this by Aug 1, 2017, at which point all
525
- // callsites should have been switched to using the "final"
526
- // designs.
527
- radioStyleVersion: PropTypes.oneOf(["intermediate", "final"])
497
+ // TODO(Nicole): Remove this after landing wonderblock-ifying
498
+ // the radio widget
499
+ primaryProductColor: PropTypes.string
528
500
  }),
529
501
  // The color used for the hint progress indicator (eg. 1 / 3)
530
502
  hintProgressColor: PropTypes.string,
@@ -575,9 +547,7 @@ var ApiOptions = {
575
547
  },
576
548
  setDrawingAreaAvailable: function setDrawingAreaAvailable() {},
577
549
  useDraftEditor: false,
578
- styling: {
579
- radioStyleVersion: "final"
580
- },
550
+ styling: {},
581
551
  canScrollPage: false,
582
552
  inModal: false,
583
553
  crossOutEnabled: false,
@@ -1506,8 +1476,7 @@ var KhanAnswerTypes = {
1506
1476
  */
1507
1477
  number: {
1508
1478
  convertToPredicate: function convertToPredicate(correctAnswer, options) {
1509
- // TODO(alpert): Don't think this $.trim is necessary
1510
- var correctFloat = parseFloat($.trim(correctAnswer));
1479
+ var correctFloat = parseFloat(correctAnswer);
1511
1480
  return [function (guess, maxError) {
1512
1481
  return Math.abs(guess - correctFloat) < maxError;
1513
1482
  }, $.extend({}, options, {
@@ -1701,16 +1670,10 @@ function inputPathsEqual(a, b) {
1701
1670
  return a.length === b.length && a.every((item, index) => {
1702
1671
  return b[index] === item;
1703
1672
  });
1704
- } // Keep `rWidgetRule` in sync with `WIDGET_RE`
1705
- // in content_internal/perseus_traversal.py
1706
- // sync-start:widget-id-regex 1271666319 services/content-editing/perseus/translation/content_traversal.go
1707
- // sync-start:widget-id-regex 680240484 services/static/javascript/perseus-merged-editor-package/editor.jsx
1708
- // sync-start:widget-id-regex 1400368396 content_internal/perseus_traversal.py
1709
-
1673
+ }
1710
1674
 
1711
1675
  var rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
1712
- var rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/; // sync-end:widget-id-regex
1713
-
1676
+ var rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
1714
1677
  var rWidgetParts = new RegExp(rWidgetRule.source + "$");
1715
1678
  var snowman = "\u2603";
1716
1679
  var noScore = {
@@ -3143,11 +3106,10 @@ var warningColor = "#f86700";
3143
3106
  var warningColorHover = "#df5c00";
3144
3107
  var warningColorActive = "#c75300";
3145
3108
  var publishBlockingErrorColor = "#be2612";
3146
- var radioBorderColor$2 = "#BABEC2";
3147
- var checkedColor$1 = "#71B307"; // sync-start:article-max-width 986452807 services/static/javascript/perseus-all-package/styles/variables.less
3148
-
3109
+ var radioBorderColor$1 = "#BABEC2";
3110
+ var checkedColor$1 = "#71B307";
3149
3111
  var articleMaxWidthInPx = 688;
3150
- var articleMaxWidthTableInPx = 512; // sync-end:article-max-width
3112
+ var articleMaxWidthTableInPx = 512;
3151
3113
 
3152
3114
  var constants$1 = /*#__PURE__*/Object.freeze({
3153
3115
  __proto__: null,
@@ -3212,7 +3174,7 @@ var constants$1 = /*#__PURE__*/Object.freeze({
3212
3174
  warningColorHover: warningColorHover,
3213
3175
  warningColorActive: warningColorActive,
3214
3176
  publishBlockingErrorColor: publishBlockingErrorColor,
3215
- radioBorderColor: radioBorderColor$2,
3177
+ radioBorderColor: radioBorderColor$1,
3216
3178
  checkedColor: checkedColor$1,
3217
3179
  articleMaxWidthInPx: articleMaxWidthInPx,
3218
3180
  articleMaxWidthTableInPx: articleMaxWidthTableInPx
@@ -17996,17 +17958,17 @@ class Lint extends React.Component {
17996
17958
  var warningTextStyle;
17997
17959
 
17998
17960
  if (this.props.severity === 1) {
17999
- severityStyle = styles$w.indicatorError;
17961
+ severityStyle = styles$A.indicatorError;
18000
17962
  warningText = "Error";
18001
- warningTextStyle = styles$w.publishBlockingError;
17963
+ warningTextStyle = styles$A.publishBlockingError;
18002
17964
  } else if (this.props.severity === 2) {
18003
- severityStyle = styles$w.indicatorWarning;
17965
+ severityStyle = styles$A.indicatorWarning;
18004
17966
  warningText = "Warning";
18005
- warningTextStyle = styles$w.warning;
17967
+ warningTextStyle = styles$A.warning;
18006
17968
  } else {
18007
- severityStyle = styles$w.indicatorGuideline;
17969
+ severityStyle = styles$A.indicatorGuideline;
18008
17970
  warningText = "Recommendation";
18009
- warningTextStyle = styles$w.warning;
17971
+ warningTextStyle = styles$A.warning;
18010
17972
  }
18011
17973
 
18012
17974
  return /*#__PURE__*/React.createElement("a", {
@@ -18014,16 +17976,16 @@ class Lint extends React.Component {
18014
17976
  target: "lint-help-window",
18015
17977
  className: css(style)
18016
17978
  }, /*#__PURE__*/React.createElement("span", {
18017
- className: css(styles$w.indicator, severityStyle)
17979
+ className: css(styles$A.indicator, severityStyle)
18018
17980
  }, this.props.severity === 1 && /*#__PURE__*/React.createElement(InlineIcon, exclamationIcon)), /*#__PURE__*/React.createElement("div", {
18019
- className: css(styles$w.tooltip, tooltipAbove && styles$w.tooltipAbove)
17981
+ className: css(styles$A.tooltip, tooltipAbove && styles$A.tooltipAbove)
18020
17982
  }, this.props.message.split("\n\n").map((m, i) => /*#__PURE__*/React.createElement("p", {
18021
17983
  key: i,
18022
- className: css(styles$w.tooltipParagraph)
17984
+ className: css(styles$A.tooltipParagraph)
18023
17985
  }, /*#__PURE__*/React.createElement("span", {
18024
17986
  className: css(warningTextStyle)
18025
17987
  }, warningText, ":", " "), m)), /*#__PURE__*/React.createElement("div", {
18026
- className: css(styles$w.tail, tooltipAbove && styles$w.tailAbove)
17988
+ className: css(styles$A.tail, tooltipAbove && styles$A.tailAbove)
18027
17989
  })));
18028
17990
  });
18029
17991
  }
@@ -18071,19 +18033,19 @@ class Lint extends React.Component {
18071
18033
 
18072
18034
  if (blockHighlight) {
18073
18035
  return /*#__PURE__*/React.createElement("span", {
18074
- className: css(styles$w.lintContainer, styles$w.lintContainerBlock)
18075
- }, this.renderLink(styles$w.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
18036
+ className: css(styles$A.lintContainer, styles$A.lintContainerBlock)
18037
+ }, this.renderLink(styles$A.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
18076
18038
  }
18077
18039
 
18078
18040
  if (inline) {
18079
18041
  return /*#__PURE__*/React.createElement("span", {
18080
- className: css(styles$w.lintContainer)
18081
- }, this.renderLink(styles$w.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
18042
+ className: css(styles$A.lintContainer)
18043
+ }, this.renderLink(styles$A.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
18082
18044
  }
18083
18045
 
18084
18046
  return /*#__PURE__*/React.createElement("div", {
18085
- className: css(styles$w.lintContainer)
18086
- }, this.renderLink(styles$w.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
18047
+ className: css(styles$A.lintContainer)
18048
+ }, this.renderLink(styles$A.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
18087
18049
  }
18088
18050
 
18089
18051
  } // $FlowFixMe[prop-missing]
@@ -18108,7 +18070,7 @@ _defineProperty(Lint, "propTypes", {
18108
18070
  severity: PropTypes.number
18109
18071
  });
18110
18072
 
18111
- var styles$w = StyleSheet.create({
18073
+ var styles$A = StyleSheet.create({
18112
18074
  // This is the class of the outermost element.
18113
18075
  // We use relative positioning so that the lint indicator can be
18114
18076
  // positioned absolutely relative to the position of the linty container.
@@ -19998,7 +19960,7 @@ class Renderer extends React.Component {
19998
19960
  paddingRight: margin
19999
19961
  };
20000
19962
 
20001
- var mobileInnerStyle = _objectSpread2(_objectSpread2({}, innerStyle), styles$v.mobileZoomableParentFix);
19963
+ var mobileInnerStyle = _objectSpread2(_objectSpread2({}, innerStyle), styles$z.mobileZoomableParentFix);
20002
19964
 
20003
19965
  return /*#__PURE__*/React.createElement("div", {
20004
19966
  key: state.key,
@@ -20136,7 +20098,7 @@ class Renderer extends React.Component {
20136
20098
  paddingRight: 0
20137
20099
  };
20138
20100
 
20139
- var _mobileInnerStyle = _objectSpread2(_objectSpread2({}, _innerStyle), styles$v.mobileZoomableParentFix);
20101
+ var _mobileInnerStyle = _objectSpread2(_objectSpread2({}, _innerStyle), styles$z.mobileZoomableParentFix);
20140
20102
 
20141
20103
  var wrappedOutput = /*#__PURE__*/React.createElement("div", {
20142
20104
  style: _objectSpread2(_objectSpread2({}, _mobileInnerStyle), {}, {
@@ -20815,7 +20777,7 @@ _defineProperty(Renderer, "defaultProps", {
20815
20777
  linterContext: PerseusLinter.linterContextDefault
20816
20778
  });
20817
20779
 
20818
- var styles$v = {
20780
+ var styles$z = {
20819
20781
  mobileZoomableParentFix: {
20820
20782
  // NOTE(abdul): There is an issue where transform animations will
20821
20783
  // cause the Zoomable component to disappear when running on the
@@ -22991,36 +22953,37 @@ _defineProperty(PossibleAnswers, "propTypes", {
22991
22953
  answers: PropTypes.arrayOf(PropTypes.string)
22992
22954
  });
22993
22955
 
22994
- var _excluded$2 = ["keypadElement", "onFocus", "value"];
22995
- var SimpleKeypadInput = createReactClass({
22996
- displayName: "SimpleKeypadInput",
22997
- propTypes: {
22998
- keypadElement: keypadElementPropType,
22999
- onFocus: PropTypes.func,
23000
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
23001
- },
23002
- componentDidMount: function componentDidMount() {
22956
+ var _excluded$3 = ["keypadElement", "onFocus", "value"];
22957
+ class SimpleKeypadInput extends React.Component {
22958
+ constructor() {
22959
+ super(...arguments);
22960
+
22961
+ _defineProperty(this, "_isMounted", false);
22962
+ }
22963
+
22964
+ componentDidMount() {
23003
22965
  // TODO(scottgrant): This is a hack to remove the deprecated call to
23004
22966
  // this.isMounted() but is still considered an anti-pattern.
23005
22967
  this._isMounted = true;
23006
- },
23007
- componentWillUnmount: function componentWillUnmount() {
22968
+ }
22969
+
22970
+ componentWillUnmount() {
23008
22971
  this._isMounted = false;
23009
- },
22972
+ }
23010
22973
 
23011
22974
  focus() {
23012
22975
  // $FlowFixMe[object-this-reference]
23013
22976
  this.refs.input.focus(); // eslint-disable-line react/no-string-refs
23014
- },
22977
+ }
23015
22978
 
23016
22979
  blur() {
23017
22980
  // $FlowFixMe[object-this-reference]
23018
22981
  this.refs.input.blur(); // eslint-disable-line react/no-string-refs
23019
- },
22982
+ }
23020
22983
 
23021
- getValue: function getValue() {
22984
+ getValue() {
23022
22985
  return this.props.value;
23023
- },
22986
+ }
23024
22987
 
23025
22988
  render() {
23026
22989
  // $FlowFixMe[object-this-reference]
@@ -23036,7 +22999,7 @@ var SimpleKeypadInput = createReactClass({
23036
22999
  onFocus: _onFocus,
23037
23000
  value
23038
23001
  } = _this$props,
23039
- rest = _objectWithoutProperties(_this$props, _excluded$2);
23002
+ rest = _objectWithoutProperties(_this$props, _excluded$3);
23040
23003
 
23041
23004
  return /*#__PURE__*/React.createElement(KeypadInput // eslint-disable-next-line react/no-string-refs
23042
23005
  , _extends({
@@ -23059,7 +23022,12 @@ var SimpleKeypadInput = createReactClass({
23059
23022
  }, rest));
23060
23023
  }
23061
23024
 
23062
- });
23025
+ }
23026
+ SimpleKeypadInput.propTypes = {
23027
+ keypadElement: keypadElementPropType,
23028
+ onFocus: PropTypes.func,
23029
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
23030
+ };
23063
23031
 
23064
23032
  var ParseTex$1 = TexWrangler.parseTex;
23065
23033
  var answerTypes = {
@@ -23314,9 +23282,13 @@ class InputNumber extends React.Component {
23314
23282
 
23315
23283
  if (rubric.answerType == null) {
23316
23284
  rubric.answerType = "number";
23317
- }
23285
+ } // note(matthewc): this will get immediately parsed again by
23286
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
23287
+ // expected here
23288
+
23318
23289
 
23319
- var val = KhanAnswerTypes.number.createValidatorFunctional(rubric.value, {
23290
+ var stringValue = "".concat(rubric.value);
23291
+ var val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
23320
23292
  simplify: rubric.simplify,
23321
23293
  inexact: rubric.inexact || undefined,
23322
23294
  maxError: rubric.maxError,
@@ -23442,6 +23414,8 @@ class NumericInput extends React.Component {
23442
23414
  constructor() {
23443
23415
  super(...arguments);
23444
23416
 
23417
+ _defineProperty(this, "inputRef", void 0);
23418
+
23445
23419
  _defineProperty(this, "state", {
23446
23420
  // keeps track of the other set of values when switching
23447
23421
  // between 0 and finite solutions
@@ -23460,9 +23434,8 @@ class NumericInput extends React.Component {
23460
23434
  correct = score.type === "points" && score.earned === score.total;
23461
23435
 
23462
23436
  if (!correct) {
23463
- var correctAnswers = _.filter(rubric.answers, answer => answer.status === "correct");
23464
-
23465
- var answerStrings = _.map(correctAnswers, answer => {
23437
+ var correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23438
+ var answerStrings = correctAnswers.map(answer => {
23466
23439
  // Figure out how this answer is supposed to be
23467
23440
  // displayed
23468
23441
  var format = "decimal";
@@ -23483,7 +23456,6 @@ class NumericInput extends React.Component {
23483
23456
 
23484
23457
  return answerString;
23485
23458
  });
23486
-
23487
23459
  answerBlurb = /*#__PURE__*/React.createElement(PossibleAnswers, {
23488
23460
  answers: answerStrings
23489
23461
  });
@@ -23497,7 +23469,7 @@ class NumericInput extends React.Component {
23497
23469
  var _this$props$answerFor;
23498
23470
 
23499
23471
  // if the set of specified forms are empty, allow all forms
23500
- var forms = ((_this$props$answerFor = this.props.answerForms) === null || _this$props$answerFor === void 0 ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : _.map(_.keys(formExamples), name => {
23472
+ var 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 => {
23501
23473
  return {
23502
23474
  name: name,
23503
23475
  simplify: "required"
@@ -23523,8 +23495,7 @@ class NumericInput extends React.Component {
23523
23495
 
23524
23496
  var answerFormNames = _.uniq((_this$props$answerFor3 = this.props.answerForms) === null || _this$props$answerFor3 === void 0 ? void 0 : _this$props$answerFor3.map(form => form.name));
23525
23497
 
23526
- var allFormsAccepted = answerFormNames.length >= _.size(formExamples);
23527
-
23498
+ var allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
23528
23499
  return !noFormsAccepted && !allFormsAccepted;
23529
23500
  });
23530
23501
 
@@ -23533,20 +23504,22 @@ class NumericInput extends React.Component {
23533
23504
  });
23534
23505
 
23535
23506
  _defineProperty(this, "focus", () => {
23536
- // eslint-disable-next-line react/no-string-refs
23537
- this.refs.input.focus();
23507
+ var _this$inputRef;
23508
+
23509
+ (_this$inputRef = this.inputRef) === null || _this$inputRef === void 0 ? void 0 : _this$inputRef.focus();
23538
23510
  return true;
23539
23511
  });
23540
23512
 
23541
- _defineProperty(this, "focusInputPath", inputPath => {
23542
- /* istanbul ignore next */
23543
- // eslint-disable-next-line react/no-string-refs
23544
- this.refs.input.focus();
23513
+ _defineProperty(this, "focusInputPath", () => {
23514
+ var _this$inputRef2;
23515
+
23516
+ (_this$inputRef2 = this.inputRef) === null || _this$inputRef2 === void 0 ? void 0 : _this$inputRef2.focus();
23545
23517
  });
23546
23518
 
23547
- _defineProperty(this, "blurInputPath", inputPath => {
23548
- // eslint-disable-next-line react/no-string-refs
23549
- this.refs.input.blur();
23519
+ _defineProperty(this, "blurInputPath", () => {
23520
+ var _this$inputRef3;
23521
+
23522
+ (_this$inputRef3 = this.inputRef) === null || _this$inputRef3 === void 0 ? void 0 : _this$inputRef3.blur();
23550
23523
  });
23551
23524
 
23552
23525
  _defineProperty(this, "getInputPaths", () => {
@@ -23614,9 +23587,8 @@ class NumericInput extends React.Component {
23614
23587
  }
23615
23588
 
23616
23589
  static getOneCorrectAnswerFromRubric(rubric) {
23617
- var correctAnswers = _.filter(rubric.answers, answer => answer.status === "correct");
23618
-
23619
- var answerStrings = _.map(correctAnswers, answer => {
23590
+ var correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23591
+ var answerStrings = correctAnswers.map(answer => {
23620
23592
  // Figure out how this answer is supposed to be
23621
23593
  // displayed
23622
23594
  var format = "decimal";
@@ -23646,31 +23618,30 @@ class NumericInput extends React.Component {
23646
23618
  }
23647
23619
 
23648
23620
  static validate(useInput, rubric) {
23649
- var allAnswerForms = _.pluck(answerFormButtons, "value");
23650
-
23651
- var createValidator = answer => KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
23652
- message: answer.message,
23653
- simplify: answer.status === "correct" ? answer.simplify : "optional",
23654
- inexact: true,
23655
- // TODO(merlob) backfill / delete
23656
- maxError: answer.maxError,
23657
- forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23658
- }); // We may have received TeX; try to parse it before grading.
23621
+ var allAnswerForms = answerFormButtons.map(e => e["value"]);
23622
+
23623
+ var createValidator = answer => {
23624
+ var stringAnswer = "".concat(answer.value);
23625
+ return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
23626
+ message: answer.message,
23627
+ simplify: answer.status === "correct" ? answer.simplify : "optional",
23628
+ inexact: true,
23629
+ // TODO(merlob) backfill / delete
23630
+ maxError: answer.maxError,
23631
+ forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23632
+ });
23633
+ }; // We may have received TeX; try to parse it before grading.
23659
23634
  // If `currentValue` is not TeX, this should be a no-op.
23660
23635
 
23661
23636
 
23662
23637
  var currentValue = ParseTex(useInput.currentValue);
23663
-
23664
- var correctAnswers = _.where(rubric.answers, {
23665
- status: "correct"
23666
- });
23667
-
23638
+ var correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23668
23639
  var normalizedAnswerExpected = correctAnswers.every(answer => Math.abs(answer.value) <= 1); // Look through all correct answers for one that matches either
23669
23640
  // precisely or approximately and return the appropriate message:
23670
23641
  // - if precise, return the message that the answer came with
23671
23642
  // - if it needs to be simplified, etc., show that message
23672
23643
 
23673
- var result = _.find(_.map(correctAnswers, answer => {
23644
+ var result = correctAnswers.map(answer => {
23674
23645
  // The coefficient is an attribute of the widget
23675
23646
  var localValue = currentValue;
23676
23647
 
@@ -23684,22 +23655,17 @@ class NumericInput extends React.Component {
23684
23655
 
23685
23656
  var validate = createValidator(answer);
23686
23657
  return validate(maybeParsePercentInput(localValue, normalizedAnswerExpected));
23687
- }), match => match.correct || match.empty);
23658
+ }).find(match => match.correct || match.empty);
23688
23659
 
23689
23660
  if (!result) {
23690
23661
  // Otherwise, if the guess is not correct
23691
- var otherAnswers = [].concat(_.where(rubric.answers, {
23692
- status: "ungraded"
23693
- }), _.where(rubric.answers, {
23694
- status: "wrong"
23695
- })); // Look through all other answers and if one matches either
23662
+ var 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
23696
23663
  // precisely or approximately return the answer's message
23697
23664
 
23698
- var match = _.find(otherAnswers, answer => {
23665
+ var match = otherAnswers.find(answer => {
23699
23666
  var validate = createValidator(answer);
23700
23667
  return validate(maybeParsePercentInput(currentValue, normalizedAnswerExpected)).correct;
23701
23668
  });
23702
-
23703
23669
  result = {
23704
23670
  empty: match ? match.status === "ungraded" : false,
23705
23671
  correct: match ? match.status === "correct" : false,
@@ -23746,9 +23712,8 @@ class NumericInput extends React.Component {
23746
23712
 
23747
23713
  if (this.props.apiOptions.customKeypad) {
23748
23714
  // TODO(charlie): Support "Review Mode".
23749
- return maybeRightAlignKeypadInput( /*#__PURE__*/React.createElement(SimpleKeypadInput // eslint-disable-next-line react/no-string-refs
23750
- , {
23751
- ref: "input",
23715
+ return maybeRightAlignKeypadInput( /*#__PURE__*/React.createElement(SimpleKeypadInput, {
23716
+ ref: _ref => this.inputRef = _ref,
23752
23717
  value: this.props.currentValue,
23753
23718
  keypadElement: this.props.keypadElement,
23754
23719
  onChange: this.handleChange,
@@ -23757,9 +23722,8 @@ class NumericInput extends React.Component {
23757
23722
  }));
23758
23723
  }
23759
23724
 
23760
- var input = /*#__PURE__*/React.createElement(InputWithExamples // eslint-disable-next-line react/no-string-refs
23761
- , {
23762
- ref: "input",
23725
+ var input = /*#__PURE__*/React.createElement(InputWithExamples, {
23726
+ ref: _ref2 => this.inputRef = _ref2,
23763
23727
  value: this.props.currentValue,
23764
23728
  onChange: this.handleChange,
23765
23729
  className: classNames(classes),
@@ -23826,7 +23790,7 @@ var unionAnswerForms = function unionAnswerForms(answerFormsList) {
23826
23790
  // two elements are equal, and returns a list of unique elements. This is
23827
23791
  // just a helper function here, but works generally.
23828
23792
  var uniqueBy = function uniqueBy(list, iteratee) {
23829
- return _.reduce(list, (uniqueList, element) => {
23793
+ return list.reduce((uniqueList, element) => {
23830
23794
  // For each element, decide whether it's already in the list of
23831
23795
  // unique items.
23832
23796
  var inList = _.find(uniqueList, iteratee.bind(null, element));
@@ -23840,13 +23804,13 @@ var unionAnswerForms = function unionAnswerForms(answerFormsList) {
23840
23804
  }; // Pull out all of the forms from the different lists.
23841
23805
 
23842
23806
 
23843
- var allForms = _.flatten(answerFormsList); // Pull out the unique forms using uniqueBy.
23844
-
23807
+ var allForms = answerFormsList.flat(); // Pull out the unique forms using uniqueBy.
23845
23808
 
23846
23809
  var uniqueForms = uniqueBy(allForms, _.isEqual); // Sort them by the order they appear in the `formExamples` list.
23847
23810
 
23811
+ var formExampleKeys = Object.keys(formExamples);
23848
23812
  return _.sortBy(uniqueForms, form => {
23849
- return _.keys(formExamples).indexOf(form.name);
23813
+ return formExampleKeys.indexOf(form.name);
23850
23814
  });
23851
23815
  };
23852
23816
  // This function checks if the user inputted a percent value, parsing
@@ -23855,7 +23819,7 @@ var unionAnswerForms = function unionAnswerForms(answerFormsList) {
23855
23819
  // can accept several input forms with or without "%", the decision
23856
23820
  // to parse based on the presence of "%" in the input, is so that we
23857
23821
  // don't accidently scale the user typed value before grading, CP-930.
23858
- var maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23822
+ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
23859
23823
  // If the input value is not a string ending with "%", then there's
23860
23824
  // nothing more to do. The value will be graded as inputted by user.
23861
23825
  if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
@@ -23880,13 +23844,13 @@ var maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23880
23844
 
23881
23845
 
23882
23846
  return value;
23883
- };
23847
+ }
23884
23848
 
23885
23849
  var propsTransform = function propsTransform(widgetOptions) {
23886
23850
  var rendererProps = _.extend(_.omit(widgetOptions, "answers"), {
23887
23851
  answerForms: unionAnswerForms( // Pull out the name of each form and whether that form has
23888
23852
  // required simplification.
23889
- _.map(widgetOptions.answers, answer => {
23853
+ widgetOptions.answers.map(answer => {
23890
23854
  return _.map(answer.answerForms, form => {
23891
23855
  return {
23892
23856
  simplify: answer.simplify,
@@ -24122,7 +24086,7 @@ var mediaQueries = {
24122
24086
  var {
24123
24087
  zIndexAboveScratchpad,
24124
24088
  zIndexInteractiveComponent,
24125
- radioBorderColor: radioBorderColor$1,
24089
+ radioBorderColor,
24126
24090
  checkedColor,
24127
24091
  circleSize,
24128
24092
  radioMarginWidth
@@ -24177,7 +24141,7 @@ var sharedStyles = StyleSheet.create({
24177
24141
  },
24178
24142
  backgroundColor: "#fff",
24179
24143
  border: "2px solid #fff",
24180
- boxShadow: "0 0px 0px 1px ".concat(radioBorderColor$1),
24144
+ boxShadow: "0 0px 0px 1px ".concat(radioBorderColor),
24181
24145
  outline: "none",
24182
24146
  boxSizing: "border-box",
24183
24147
  flexShrink: 0,
@@ -24430,35 +24394,65 @@ _defineProperty(Icon, "defaultProps", {
24430
24394
  color: "currentColor"
24431
24395
  });
24432
24396
 
24433
- class FocusRing extends React.Component {
24434
- render() {
24435
- var borderColor = this.props.visible ? this.props.color : "transparent";
24436
- var style = {
24437
- borderColor: borderColor
24438
- };
24439
- return /*#__PURE__*/React.createElement("span", {
24440
- className: css(styles$u.ring),
24441
- style: style
24442
- }, this.props.children);
24397
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
24398
+ // There are two pieces of metadata we want to add to each a11yText:
24399
+ // whether the answer was checked/crossed-out/neither, and whether the
24400
+ // answer is correct/incorrect/not-yet-revealed.
24401
+ //
24402
+ // Translation is tricky for cross-product situations like this, so
24403
+ // we've just enumerated all 9 possibilities as separate strings.
24404
+ if (showCorrectness && correct) {
24405
+ if (checked) {
24406
+ return i18n._("(Choice %(letter)s, Checked, Correct)", {
24407
+ letter
24408
+ });
24409
+ }
24410
+
24411
+ if (crossedOut) {
24412
+ return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
24413
+ letter
24414
+ });
24415
+ }
24416
+
24417
+ return i18n._("(Choice %(letter)s, Correct)", {
24418
+ letter
24419
+ });
24443
24420
  }
24444
24421
 
24445
- }
24422
+ if (showCorrectness && !correct) {
24423
+ if (checked) {
24424
+ return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
24425
+ letter
24426
+ });
24427
+ }
24446
24428
 
24447
- _defineProperty(FocusRing, "defaultProps", {
24448
- visible: true,
24449
- color: kaGreen$1
24450
- });
24429
+ if (crossedOut) {
24430
+ return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
24431
+ letter
24432
+ });
24433
+ }
24451
24434
 
24452
- var styles$u = StyleSheet.create({
24453
- ring: {
24454
- margin: "auto",
24455
- display: "inline-block",
24456
- borderRadius: "50%",
24457
- borderWidth: 2,
24458
- padding: 2,
24459
- borderStyle: "solid"
24435
+ return i18n._("(Choice %(letter)s, Incorrect)", {
24436
+ letter
24437
+ });
24460
24438
  }
24461
- });
24439
+
24440
+ if (checked) {
24441
+ return i18n._("(Choice %(letter)s, Checked)", {
24442
+ letter
24443
+ });
24444
+ }
24445
+
24446
+ if (crossedOut) {
24447
+ return i18n._("(Choice %(letter)s, Crossed out)", {
24448
+ letter
24449
+ });
24450
+ }
24451
+
24452
+ return i18n._("(Choice %(letter)s)", {
24453
+ letter
24454
+ });
24455
+ }
24462
24456
 
24463
24457
  /**
24464
24458
  * Given a choice's position in the radio widget, return the corresponding
@@ -24490,179 +24484,57 @@ function getChoiceLetter(pos) {
24490
24484
  return " ";
24491
24485
  }
24492
24486
 
24493
- var SAT_ICON_SIZE = 25;
24494
-
24495
- class SATChoiceIcon extends React.Component {
24496
- // TODO(amy): figure out a better scheme for specifying these
24497
- // styles that isn't such a pain to grok. See some neat ideas
24498
- // from MDR in https://phabricator.khanacademy.org/D35249.
24499
- constructStyles(reviewMode, correct, checked) {
24500
- var backgroundColor;
24501
- var borderColor = satBlue;
24502
- var color = satBlue;
24503
-
24504
- if (reviewMode) {
24505
- if (correct) {
24506
- borderColor = satCorrectColor;
24507
- color = checked ? white$1 : satCorrectColor;
24508
- backgroundColor = checked ? satCorrectColor : white$1;
24509
- } else if (checked) {
24510
- borderColor = satIncorrectColor;
24511
- color = white$1;
24512
- backgroundColor = satIncorrectColor;
24513
- }
24514
- } else if (checked) {
24515
- color = white$1;
24516
- backgroundColor = satBlue;
24517
- }
24518
-
24519
- return {
24520
- color,
24521
- backgroundColor,
24522
- borderColor
24523
- };
24524
- }
24525
-
24526
- render() {
24527
- var {
24528
- letter,
24529
- a11yText,
24530
- reviewMode,
24531
- checked,
24532
- correct,
24533
- crossedOut
24534
- } = this.props;
24535
- var {
24536
- color,
24537
- backgroundColor,
24538
- borderColor
24539
- } = this.constructStyles(reviewMode, correct, checked);
24540
- return /*#__PURE__*/React.createElement("div", {
24541
- className: css(styles$t.iconWrapper)
24542
- }, /*#__PURE__*/React.createElement("div", {
24543
- className: css(styles$t.satCircle),
24544
- style: {
24545
- backgroundColor,
24546
- borderColor
24547
- }
24548
- }), /*#__PURE__*/React.createElement("div", {
24549
- style: {
24550
- color
24551
- },
24552
- className: css(styles$t.letter)
24553
- }, /*#__PURE__*/React.createElement("span", {
24554
- className: "perseus-sr-only"
24555
- }, a11yText), /*#__PURE__*/React.createElement("span", {
24556
- "aria-hidden": "true"
24557
- }, letter)), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
24558
- color: borderColor,
24559
- sat: true
24560
- }));
24561
- }
24487
+ /**
24488
+ * Renders a circular selection ring around the child.
24489
+ */
24562
24490
 
24491
+ function FocusRing(props) {
24492
+ var {
24493
+ visible,
24494
+ color,
24495
+ children,
24496
+ multipleSelect
24497
+ } = props;
24498
+ var borderColor = visible ? color : "transparent";
24499
+ var borderRadius = multipleSelect ? 5 : "50%";
24500
+ var style = {
24501
+ borderColor,
24502
+ borderRadius
24503
+ };
24504
+ return /*#__PURE__*/React.createElement("span", {
24505
+ "data-test-id": "focus-ring",
24506
+ className: css(styles$y.ring),
24507
+ style: style
24508
+ }, children);
24563
24509
  }
24564
24510
 
24565
- var LIBRARY_ICON_SIZE = 24;
24566
-
24567
- class LibraryChoiceIcon extends React.Component {
24568
- getChoiceInner() {
24569
- var {
24570
- letter,
24571
- showCorrectness,
24572
- correct
24573
- } = this.props;
24574
-
24575
- if (!showCorrectness) {
24576
- return letter;
24577
- }
24578
-
24579
- if (correct) {
24580
- return /*#__PURE__*/React.createElement(InlineIcon, _extends({}, iconCheck, {
24581
- style: {
24582
- position: "relative",
24583
- top: -1
24584
- }
24585
- }));
24586
- }
24587
-
24588
- return /*#__PURE__*/React.createElement(InlineIcon, iconMinus);
24589
- } // Handle dynamic styling of the multiple choice icon. Most
24590
- // MC icon styles are constant, but we do allow the caller
24591
- // to specify the selected color, and thus must control styles
24592
- // related to the selected state dynamically.
24593
-
24594
-
24595
- getDynamicStyles() {
24596
- var {
24597
- checked,
24598
- showCorrectness,
24599
- pressed,
24600
- primaryProductColor,
24601
- correct,
24602
- transparentBackground
24603
- } = this.props;
24604
-
24605
- if (!showCorrectness && pressed) {
24606
- return {
24607
- borderColor: primaryProductColor,
24608
- color: primaryProductColor,
24609
- backgroundColor: transparentBackground ? "transparent" : white$1
24610
- };
24611
- }
24612
-
24613
- if (checked) {
24614
- // Note: kaGreen is not only the default product color,
24615
- // but also the "correctness" color
24616
- var bg = showCorrectness && correct ? Color.green : primaryProductColor;
24617
- return {
24618
- color: white$1,
24619
- backgroundColor: bg,
24620
- borderColor: bg
24621
- };
24622
- }
24623
-
24624
- return {
24625
- borderColor: Color.offBlack64,
24626
- color: Color.offBlack64
24627
- };
24511
+ FocusRing.defaultProps = {
24512
+ visible: true,
24513
+ color: kaGreen$1,
24514
+ multipleSelect: false
24515
+ };
24516
+ var styles$y = StyleSheet.create({
24517
+ ring: {
24518
+ margin: "auto",
24519
+ display: "inline-block",
24520
+ borderWidth: 2,
24521
+ padding: 2,
24522
+ borderStyle: "solid"
24628
24523
  }
24524
+ });
24629
24525
 
24630
- render() {
24631
- var {
24632
- a11yText,
24633
- checked,
24634
- crossedOut,
24635
- showCorrectness,
24636
- correct,
24637
- focused,
24638
- primaryProductColor,
24639
- previouslyAnswered
24640
- } = this.props;
24641
- var dynamicStyles = this.getDynamicStyles();
24642
- return /*#__PURE__*/React.createElement("div", {
24643
- className: css(styles$t.iconWrapper)
24644
- }, /*#__PURE__*/React.createElement(FocusRing, {
24645
- color: primaryProductColor,
24646
- visible: focused
24647
- }, /*#__PURE__*/React.createElement("div", {
24648
- style: dynamicStyles,
24649
- className: 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
24650
- // radio icon
24651
- ,
24652
- "data-is-radio-icon": true
24653
- }, /*#__PURE__*/React.createElement("div", {
24654
- className: "perseus-sr-only"
24655
- }, a11yText), /*#__PURE__*/React.createElement("div", {
24656
- "aria-hidden": true
24657
- }, this.getChoiceInner()))), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
24658
- color: dynamicStyles.borderColor
24659
- }));
24526
+ var SAT_ICON_SIZE = 25;
24527
+ var LIBRARY_ICON_SIZE = 24;
24528
+ var styles$x = StyleSheet.create({
24529
+ iconWrapper: {
24530
+ display: "inline-block",
24531
+ position: "relative"
24660
24532
  }
24533
+ });
24661
24534
 
24662
- } // The "cross-out line" SVG is a bit bigger than the library icon, to provide
24535
+ /* Component for rendering a letter icon in radio choice */
24663
24536
  // extra space for the cute rounded line cap.
24664
24537
 
24665
-
24666
24538
  var CROSS_OUT_LINE_PADDING = 2;
24667
24539
  var CROSS_OUT_LINE_SIZE = LIBRARY_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
24668
24540
  var SAT_CROSS_OUT_LINE_SIZE = SAT_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
@@ -24677,7 +24549,7 @@ function CrossOutLine(props) {
24677
24549
  width: crossOutLineSize,
24678
24550
  height: crossOutLineSize,
24679
24551
  viewBox: "0 0 ".concat(crossOutLineSize, " ").concat(crossOutLineSize),
24680
- className: css(styles$t.crossOutLine)
24552
+ className: css(styles$w.crossOutLine)
24681
24553
  }, /*#__PURE__*/React.createElement("line", {
24682
24554
  // The line stretches from the bottom-left to top-right.
24683
24555
  // We don't quite go to the _very_ corner, because the cute
@@ -24692,151 +24564,124 @@ function CrossOutLine(props) {
24692
24564
  }));
24693
24565
  }
24694
24566
 
24695
- class ChoiceIcon extends React.Component {
24696
- a11yText(letter) {
24697
- var {
24698
- checked,
24699
- correct,
24700
- crossedOut,
24701
- showCorrectness
24702
- } = this.props; // There are two pieces of metadata we want to add to each a11yText:
24703
- // whether the answer was checked/crossed-out/neither, and whether the
24704
- // answer is correct/incorrect/not-yet-revealed.
24705
- //
24706
- // Translation is tricky for cross-product situations like this, so
24707
- // we've just enumerated all 9 possibilities as separate strings.
24708
-
24709
- if (showCorrectness && correct) {
24710
- if (checked) {
24711
- return i18n._("(Choice %(letter)s, Checked, Correct)", {
24712
- letter
24713
- });
24714
- }
24715
-
24716
- if (crossedOut) {
24717
- return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
24718
- letter
24719
- });
24720
- }
24567
+ var styles$w = StyleSheet.create({
24568
+ crossOutLine: {
24569
+ // Center the icon within the container.
24570
+ position: "absolute",
24571
+ top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24572
+ left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24573
+ }
24574
+ });
24721
24575
 
24722
- return i18n._("(Choice %(letter)s, Correct)", {
24723
- letter
24724
- });
24725
- }
24576
+ function ChoiceInner(props) {
24577
+ var {
24578
+ letter,
24579
+ showCorrectness,
24580
+ correct
24581
+ } = props;
24726
24582
 
24727
- if (showCorrectness && !correct) {
24728
- if (checked) {
24729
- return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
24730
- letter
24731
- });
24732
- }
24583
+ if (!showCorrectness) {
24584
+ return letter;
24585
+ }
24733
24586
 
24734
- if (crossedOut) {
24735
- return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
24736
- letter
24737
- });
24587
+ if (correct) {
24588
+ return /*#__PURE__*/React.createElement(InlineIcon, _extends({}, iconCheck, {
24589
+ style: {
24590
+ position: "relative",
24591
+ top: -1
24738
24592
  }
24739
-
24740
- return i18n._("(Choice %(letter)s, Incorrect)", {
24741
- letter
24742
- });
24743
- }
24744
-
24745
- if (checked) {
24746
- return i18n._("(Choice %(letter)s, Checked)", {
24747
- letter
24748
- });
24749
- }
24750
-
24751
- if (crossedOut) {
24752
- return i18n._("(Choice %(letter)s, Crossed out)", {
24753
- letter
24754
- });
24755
- }
24756
-
24757
- return i18n._("(Choice %(letter)s)", {
24758
- letter
24759
- });
24593
+ }));
24760
24594
  }
24761
24595
 
24762
- render() {
24763
- var {
24764
- pos,
24765
- reviewMode,
24766
- checked,
24767
- crossedOut,
24768
- correct,
24769
- product,
24770
- showCorrectness,
24771
- pressed,
24772
- focused,
24773
- primaryProductColor,
24774
- previouslyAnswered,
24775
- transparentBackground
24776
- } = this.props;
24777
- var letter = getChoiceLetter(pos);
24596
+ return /*#__PURE__*/React.createElement(InlineIcon, iconMinus);
24597
+ } // Handle dynamic styling of the multiple choice icon. Most
24598
+ // MC icon styles are constant, but we do allow the caller
24599
+ // to specify the selected color, and thus must control styles
24600
+ // related to the selected state dynamically.
24601
+
24602
+
24603
+ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground) {
24604
+ var backgroundColor;
24605
+ var borderColor;
24606
+ var color;
24607
+
24608
+ if (!showCorrectness && pressed) {
24609
+ borderColor = primaryProductColor;
24610
+ color = primaryProductColor;
24611
+ backgroundColor = transparentBackground ? "transparent" : white$1;
24612
+ } else if (checked) {
24613
+ // Note: kaGreen is not only the default product color,
24614
+ // but also the "correctness" color
24615
+ var bg = showCorrectness && correct ? Color.green : primaryProductColor;
24616
+ color = white$1;
24617
+ backgroundColor = bg;
24618
+ borderColor = bg;
24619
+ } else {
24620
+ borderColor = Color.offBlack64;
24621
+ color = Color.offBlack64;
24622
+ } // define shape
24778
24623
 
24779
- if (product === "sat") {
24780
- return /*#__PURE__*/React.createElement(SATChoiceIcon, {
24781
- letter: letter,
24782
- a11yText: this.a11yText(letter),
24783
- reviewMode: reviewMode,
24784
- checked: checked,
24785
- correct: correct,
24786
- crossedOut: crossedOut
24787
- });
24788
- }
24789
24624
 
24790
- return /*#__PURE__*/React.createElement(LibraryChoiceIcon, {
24791
- letter: letter,
24792
- a11yText: this.a11yText(letter),
24793
- reviewMode: reviewMode,
24794
- checked: checked,
24795
- crossedOut: crossedOut,
24796
- pressed: pressed,
24797
- focused: focused,
24798
- correct: correct,
24799
- showCorrectness: showCorrectness,
24800
- primaryProductColor: primaryProductColor,
24801
- previouslyAnswered: previouslyAnswered,
24802
- transparentBackground: transparentBackground
24803
- });
24625
+ var borderRadius;
24626
+
24627
+ if (multipleSelect) {
24628
+ borderRadius = 3;
24629
+ } else {
24630
+ borderRadius = LIBRARY_ICON_SIZE;
24804
24631
  }
24805
24632
 
24633
+ return {
24634
+ backgroundColor,
24635
+ borderColor,
24636
+ color,
24637
+ borderRadius
24638
+ };
24806
24639
  }
24807
24640
 
24808
- _defineProperty(ChoiceIcon, "defaultProps", {
24809
- primaryProductColor: Color.blue
24810
- });
24641
+ function LibraryChoiceIcon(props) {
24642
+ var {
24643
+ checked,
24644
+ crossedOut,
24645
+ showCorrectness,
24646
+ correct,
24647
+ focused,
24648
+ hovered,
24649
+ multipleSelect,
24650
+ primaryProductColor,
24651
+ previouslyAnswered,
24652
+ letter,
24653
+ pressed,
24654
+ transparentBackground
24655
+ } = props;
24656
+ var dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground);
24657
+ return /*#__PURE__*/React.createElement("div", {
24658
+ className: css(styles$x.iconWrapper)
24659
+ }, /*#__PURE__*/React.createElement(FocusRing, {
24660
+ color: primaryProductColor,
24661
+ visible: focused || hovered,
24662
+ multipleSelect: multipleSelect
24663
+ }, /*#__PURE__*/React.createElement("div", {
24664
+ style: dynamicStyles,
24665
+ "data-test-id": "choice-icon__library-choice-icon",
24666
+ className: 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
24667
+ // radio icon
24668
+ ,
24669
+ "data-is-radio-icon": true
24670
+ }, /*#__PURE__*/React.createElement(ChoiceInner, {
24671
+ letter: letter,
24672
+ showCorrectness: showCorrectness,
24673
+ correct: correct
24674
+ }))), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
24675
+ color: dynamicStyles.borderColor
24676
+ }));
24677
+ }
24811
24678
 
24812
- var styles$t = StyleSheet.create({
24813
- satCircle: {
24814
- display: "block",
24815
- borderRadius: SAT_ICON_SIZE,
24816
- borderStyle: "solid",
24817
- borderWidth: 2,
24818
- content: "''",
24819
- height: SAT_ICON_SIZE,
24820
- width: SAT_ICON_SIZE,
24821
- top: 1,
24822
- left: 1
24823
- },
24824
- iconWrapper: {
24825
- display: "inline-block",
24826
- position: "relative"
24827
- },
24828
- crossOutLine: {
24829
- // Center the icon within the container.
24830
- position: "absolute",
24831
- top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24832
- left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24833
- },
24834
- libraryCircle: {
24679
+ var styles$v = StyleSheet.create({
24680
+ circle: {
24835
24681
  // Make the circle
24836
24682
  width: LIBRARY_ICON_SIZE,
24837
24683
  height: LIBRARY_ICON_SIZE,
24838
24684
  boxSizing: "border-box",
24839
- borderRadius: LIBRARY_ICON_SIZE,
24840
24685
  borderStyle: "solid",
24841
24686
  borderWidth: 2,
24842
24687
  // The default icons have letters in them. Style those letters.
@@ -24857,18 +24702,108 @@ var styles$t = StyleSheet.create({
24857
24702
  // pixel so it is much better centered.
24858
24703
  lineHeight: "1px"
24859
24704
  },
24860
- libraryCircleCorrect: {
24705
+ circleCorrect: {
24861
24706
  fontSize: LIBRARY_ICON_SIZE
24862
24707
  },
24863
- libraryCircleIncorrect: {
24708
+ circleIncorrect: {
24864
24709
  fontSize: LIBRARY_ICON_SIZE,
24865
24710
  borderColor: gray68,
24866
24711
  color: gray68
24867
24712
  },
24868
- libraryCircleIncorrectAnswered: {
24713
+ circleIncorrectAnswered: {
24869
24714
  backgroundColor: Color.red,
24870
24715
  borderColor: Color.red,
24871
24716
  color: Color.white
24717
+ }
24718
+ });
24719
+
24720
+ /* Component for rendering a letter icon in a SAT radio choice */
24721
+
24722
+ // TODO(amy): figure out a better scheme for specifying these
24723
+ // styles that isn't such a pain to grok. See some neat ideas
24724
+ // from MDR in https://phabricator.khanacademy.org/D35249.
24725
+ function constructSATStyles(reviewMode, correct, checked, multipleSelect) {
24726
+ var backgroundColor;
24727
+ var borderColor = satBlue;
24728
+ var color = satBlue;
24729
+
24730
+ if (reviewMode) {
24731
+ if (correct) {
24732
+ borderColor = satCorrectColor;
24733
+ color = checked ? white$1 : satCorrectColor;
24734
+ backgroundColor = checked ? satCorrectColor : white$1;
24735
+ } else if (checked) {
24736
+ borderColor = satIncorrectColor;
24737
+ color = white$1;
24738
+ backgroundColor = satIncorrectColor;
24739
+ }
24740
+ } else if (checked) {
24741
+ color = white$1;
24742
+ backgroundColor = satBlue;
24743
+ }
24744
+
24745
+ var borderRadius;
24746
+
24747
+ if (multipleSelect) {
24748
+ borderRadius = 3;
24749
+ } else {
24750
+ borderRadius = SAT_ICON_SIZE;
24751
+ }
24752
+
24753
+ return {
24754
+ color,
24755
+ backgroundColor,
24756
+ borderColor,
24757
+ borderRadius
24758
+ };
24759
+ }
24760
+
24761
+ function SATChoiceIcon(props) {
24762
+ var {
24763
+ letter,
24764
+ crossedOut,
24765
+ reviewMode,
24766
+ correct,
24767
+ checked,
24768
+ multipleSelect
24769
+ } = props;
24770
+ var {
24771
+ color,
24772
+ backgroundColor,
24773
+ borderColor,
24774
+ borderRadius
24775
+ } = constructSATStyles(reviewMode, correct, checked, multipleSelect);
24776
+ return /*#__PURE__*/React.createElement("div", {
24777
+ className: css(styles$x.iconWrapper)
24778
+ }, /*#__PURE__*/React.createElement("div", {
24779
+ className: css(styles$u.satCircle),
24780
+ "data-test-id": "choice-icon__sat-choice-icon",
24781
+ style: {
24782
+ backgroundColor,
24783
+ borderColor,
24784
+ borderRadius
24785
+ }
24786
+ }), /*#__PURE__*/React.createElement("div", {
24787
+ style: {
24788
+ color
24789
+ },
24790
+ className: css(styles$u.letter)
24791
+ }, /*#__PURE__*/React.createElement("span", null, letter)), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
24792
+ color: borderColor,
24793
+ sat: true
24794
+ }));
24795
+ }
24796
+
24797
+ var styles$u = StyleSheet.create({
24798
+ satCircle: {
24799
+ display: "block",
24800
+ borderStyle: "solid",
24801
+ borderWidth: 2,
24802
+ content: "''",
24803
+ height: SAT_ICON_SIZE,
24804
+ width: SAT_ICON_SIZE,
24805
+ top: 1,
24806
+ left: 1
24872
24807
  },
24873
24808
  letter: {
24874
24809
  // These properties make sure that this element has the exact
@@ -24888,6 +24823,55 @@ var styles$t = StyleSheet.create({
24888
24823
  }
24889
24824
  });
24890
24825
 
24826
+ /* Component for rendering a letter icon in radio choice */
24827
+
24828
+ function ChoiceIcon(props) {
24829
+ var {
24830
+ pos,
24831
+ reviewMode,
24832
+ checked,
24833
+ crossedOut,
24834
+ correct,
24835
+ product,
24836
+ multipleSelect,
24837
+ showCorrectness,
24838
+ focused,
24839
+ hovered,
24840
+ pressed,
24841
+ primaryProductColor = Color.blue,
24842
+ previouslyAnswered,
24843
+ transparentBackground
24844
+ } = props;
24845
+ var letter = getChoiceLetter(pos);
24846
+
24847
+ if (product === "sat") {
24848
+ return /*#__PURE__*/React.createElement(SATChoiceIcon, {
24849
+ letter: letter,
24850
+ reviewMode: reviewMode,
24851
+ checked: checked,
24852
+ correct: correct,
24853
+ crossedOut: crossedOut,
24854
+ multipleSelect: multipleSelect
24855
+ });
24856
+ }
24857
+
24858
+ return /*#__PURE__*/React.createElement(LibraryChoiceIcon, {
24859
+ letter: letter,
24860
+ reviewMode: reviewMode,
24861
+ checked: checked,
24862
+ crossedOut: crossedOut,
24863
+ focused: focused,
24864
+ hovered: hovered,
24865
+ pressed: pressed,
24866
+ correct: correct,
24867
+ showCorrectness: showCorrectness,
24868
+ primaryProductColor: primaryProductColor,
24869
+ previouslyAnswered: previouslyAnswered,
24870
+ transparentBackground: transparentBackground,
24871
+ multipleSelect: multipleSelect
24872
+ });
24873
+ }
24874
+
24891
24875
  /**
24892
24876
  * Renders text indicating whether the choice was correct or
24893
24877
  * not and whether the choice was selected or not.
@@ -24896,63 +24880,68 @@ var styles$t = StyleSheet.create({
24896
24880
  * between the states more immediately clear to users.
24897
24881
  */
24898
24882
 
24899
- class OptionStatus extends React.Component {
24900
- _renderText() {
24901
- var {
24902
- checked,
24903
- correct,
24904
- crossedOut
24905
- } = this.props;
24883
+ function renderText(checked, correct, crossedOut) {
24884
+ if (correct) {
24885
+ // For correct answers, we surface checked _or_ crossedOut state,
24886
+ // because any interaction with the correct answer is noteworthy!
24887
+ if (checked) {
24888
+ return i18n._("Correct (selected)");
24889
+ }
24906
24890
 
24907
- if (correct) {
24908
- // For correct answers, we surface checked _or_ crossedOut state,
24909
- // because any interaction with the correct answer is noteworthy!
24910
- if (checked) {
24911
- return i18n._("Correct (selected)");
24912
- }
24891
+ if (crossedOut) {
24892
+ return i18n._("Correct (but you crossed it out)");
24893
+ }
24894
+
24895
+ return i18n._("Correct");
24896
+ } // But, for incorrect answers, we only surface checked state,
24897
+ // because crossing out an incorrect answer is not noteworthy.
24913
24898
 
24914
- if (crossedOut) {
24915
- return i18n._("Correct (but you crossed it out)");
24916
- }
24917
24899
 
24918
- return i18n._("Correct");
24919
- } // But, for incorrect answers, we only surface checked state,
24920
- // because crossing out an incorrect answer is not noteworthy.
24900
+ if (checked) {
24901
+ return i18n._("Incorrect (selected)");
24902
+ }
24921
24903
 
24904
+ return i18n._("Incorrect");
24905
+ }
24906
+
24907
+ function OptionStatus(props) {
24908
+ var {
24909
+ checked,
24910
+ correct,
24911
+ crossedOut,
24912
+ previouslyAnswered,
24913
+ reviewMode,
24914
+ satStyling
24915
+ } = props; // Option status is exclued for SAT
24916
+
24917
+ if (satStyling) {
24918
+ return null;
24919
+ } // Option status is shown only in review mode, or for incorrectly
24920
+ // answered items.
24922
24921
 
24923
- if (checked) {
24924
- return i18n._("Incorrect (selected)");
24925
- }
24926
24922
 
24927
- return i18n._("Incorrect");
24923
+ if (!reviewMode && !previouslyAnswered) {
24924
+ return null;
24928
24925
  }
24929
24926
 
24930
- render() {
24931
- var {
24932
- checked,
24933
- correct,
24934
- previouslyAnswered
24935
- } = this.props;
24936
- var textStyle;
24927
+ var textStyle;
24937
24928
 
24938
- if (correct) {
24939
- textStyle = styles$s.correct;
24929
+ if (correct) {
24930
+ textStyle = styles$t.correct;
24931
+ } else {
24932
+ if (checked || previouslyAnswered) {
24933
+ textStyle = styles$t.incorrectAnswered;
24940
24934
  } else {
24941
- if (checked || previouslyAnswered) {
24942
- textStyle = styles$s.incorrectAnswered;
24943
- } else {
24944
- textStyle = styles$s.incorrect;
24945
- }
24935
+ textStyle = styles$t.incorrect;
24946
24936
  }
24947
-
24948
- return /*#__PURE__*/React.createElement("div", {
24949
- className: css(styles$s.text, textStyle)
24950
- }, this._renderText());
24951
24937
  }
24952
24938
 
24939
+ return /*#__PURE__*/React.createElement("div", {
24940
+ className: css(styles$t.text, textStyle)
24941
+ }, renderText(checked, correct, crossedOut));
24953
24942
  }
24954
24943
 
24955
- var styles$s = StyleSheet.create({
24944
+ var styles$t = StyleSheet.create({
24956
24945
  text: {
24957
24946
  alignItems: "center",
24958
24947
  display: "flex",
@@ -24978,317 +24967,236 @@ var ellipsisHorizontalIcon = {
24978
24967
  width: 100,
24979
24968
  height: 27.284
24980
24969
  };
24970
+ // Note(TB): Received errors when using useUniqueIdWithMock
24971
+ // so created this workaround function. Will update when
24972
+ // useUniqueIdWithMock is available.
24973
+ // https://khanacademy.atlassian.net/browse/FEI-4861?atlOrigin=eyJpIjoiNDJlZWMwNjM1NWJhNDBkMWFjY2FmN2I0ZjcxZmQxOGUiLCJwIjoiaiJ9
24974
+ var id = 0;
24981
24975
 
24982
- class Choice extends React.Component {
24983
- constructor() {
24984
- super(...arguments);
24985
-
24986
- _defineProperty(this, "justFinishedTouch", void 0);
24987
-
24988
- _defineProperty(this, "justFinishedTimeoutID", void 0);
24989
-
24990
- _defineProperty(this, "_input", void 0);
24991
-
24992
- _defineProperty(this, "state", {
24993
- isInputFocused: false,
24994
- isInputActive: false
24995
- });
24996
-
24997
- _defineProperty(this, "inputRef", ref => {
24998
- this._input = ref;
24999
- });
25000
-
25001
- _defineProperty(this, "renderOptionStatus", () => {
25002
- var {
25003
- correct,
25004
- checked,
25005
- crossedOut,
25006
- reviewMode,
25007
- previouslyAnswered
25008
- } = this.props; // Option status is exclued for SAT
25009
-
25010
- if (this.props.apiOptions.satStyling) {
25011
- return null;
25012
- } // Option status is shown only in review mode, or for incorrectly
25013
- // answered items.
24976
+ function uniqueId() {
24977
+ return "choice-".concat(id++);
24978
+ }
25014
24979
 
24980
+ function Choice(props) {
24981
+ var {
24982
+ disabled,
24983
+ checked,
24984
+ content,
24985
+ crossedOut,
24986
+ showCorrectness,
24987
+ multipleSelect,
24988
+ onChange,
24989
+ reviewMode,
24990
+ correct,
24991
+ apiOptions,
24992
+ previouslyAnswered,
24993
+ pos,
24994
+ showRationale,
24995
+ rationale,
24996
+ forwardedRef
24997
+ } = props;
24998
+ var [isInputFocused, setIsInputFocused] = useState(false);
24999
+ useEffect(() => {
25000
+ if (isInputFocused && disabled) {
25001
+ setIsInputFocused(false);
25002
+ }
25003
+ }, [disabled, isInputFocused, setIsInputFocused]); // Call `this.props.onChange` with the given values. Any keys that are not
25004
+ // specified will be filled in with the current value. (For example, if
25005
+ // `checked` is specified but `crossedOut` is not, then `crossedOut` will
25006
+ // be filled in with `this.props.crossedOut`.)
25007
+ //
25008
+ // This enables us to use shorthand inside this component, while
25009
+ // maintaining a consistent API for the parent.
25015
25010
 
25016
- if (!reviewMode && !previouslyAnswered) {
25017
- return null;
25018
- }
25011
+ function sendChange(newValues) {
25012
+ var _newValues$checked, _newValues$crossedOut;
25019
25013
 
25020
- return /*#__PURE__*/React.createElement(OptionStatus, {
25021
- checked: checked,
25022
- correct: correct,
25023
- crossedOut: crossedOut,
25024
- previouslyAnswered: previouslyAnswered
25025
- });
25014
+ var updatedChecked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : checked;
25015
+ var updatedCrossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : crossedOut;
25016
+ onChange({
25017
+ checked: updatedChecked,
25018
+ crossedOut: updatedCrossedOut
25026
25019
  });
25020
+ } // HACK: while most of the styling for rendering SAT items is handled
25021
+ // via aphrodite, we also need to assign normal CSS classnames here to
25022
+ // special-case the coloring of MathJax formulas (see .MathJax .math in
25023
+ // stylesheets/task-package/tasks.less)
25027
25024
 
25028
- _defineProperty(this, "renderChoiceIcon", _ref => {
25029
- var {
25030
- isFocused,
25031
- isPressed
25032
- } = _ref;
25033
- var {
25034
- radioStyleVersion,
25035
- primaryProductColor
25036
- } = this.props.apiOptions.styling;
25037
- var finalStyles = typeof radioStyleVersion === "undefined" ? false : radioStyleVersion === "final";
25038
-
25039
- if (!finalStyles && !this.props.apiOptions.satStyling) {
25040
- return null;
25041
- }
25042
25025
 
25043
- return /*#__PURE__*/React.createElement(ChoiceIcon, {
25044
- pos: this.props.pos,
25045
- correct: this.props.correct,
25046
- crossedOut: this.props.crossedOut,
25047
- pressed: isPressed,
25048
- focused: isFocused,
25049
- checked: this.props.checked,
25050
- showCorrectness: this.props.showCorrectness,
25051
- reviewMode: this.props.reviewMode,
25052
- product: this.props.apiOptions.satStyling ? "sat" : "library",
25053
- primaryProductColor: primaryProductColor,
25054
- previouslyAnswered: this.props.previouslyAnswered
25055
- });
25056
- });
25057
-
25058
- _defineProperty(this, "_sendChange", newValues => {
25059
- var _newValues$checked, _newValues$crossedOut;
25026
+ var satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
25027
+ var satIncorrectChecked = apiOptions.satStyling && reviewMode && !correct && checked;
25028
+ var descriptionClassName = classNames("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", css(!apiOptions.satStyling && styles$s.description, apiOptions.satStyling && styles$s.satDescription, satCorrectChoice && styles$s.satDescriptionCorrect, satCorrectChoice && checked && styles$s.satDescriptionCorrectChecked, satIncorrectChecked && styles$s.satDescriptionIncorrectChecked));
25029
+ var rationaleClassName = classNames("perseus-radio-rationale-content", 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
25030
+ // disabled. However, we don't want to do this in the SAT product and
25031
+ // we also don't want to do this when we're in review mode in the
25032
+ // content library.
25060
25033
 
25061
- var checked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : this.props.checked;
25062
- var crossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : this.props.crossedOut;
25063
- this.props.onChange({
25064
- checked,
25065
- crossedOut
25034
+ var showDimmed = !apiOptions.satStyling && !reviewMode && apiOptions.readOnly || crossedOut;
25035
+ var letter = getChoiceLetter(pos);
25036
+ var a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
25037
+ var choiceId = uniqueId();
25038
+ return /*#__PURE__*/React.createElement("div", {
25039
+ style: {
25040
+ dispay: "flex",
25041
+ flexDirection: "column"
25042
+ }
25043
+ }, /*#__PURE__*/React.createElement("div", {
25044
+ style: {
25045
+ display: "flex",
25046
+ flexDirection: "row",
25047
+ width: "100%",
25048
+ opacity: showDimmed ? 0.5 : 1.0
25049
+ }
25050
+ }, /*#__PURE__*/React.createElement("div", {
25051
+ className: "perseus-sr-only"
25052
+ }, /*#__PURE__*/React.createElement("input", {
25053
+ type: multipleSelect ? "checkbox" : "radio",
25054
+ id: choiceId,
25055
+ checked: checked,
25056
+ onClick: () => {
25057
+ // If we're checking a crossed-out option, let's
25058
+ // also uncross it.
25059
+ sendChange({
25060
+ checked: !checked,
25061
+ crossedOut: false
25066
25062
  });
25067
- });
25068
-
25069
- _defineProperty(this, "focusInput", () => {
25070
- var _this$_input;
25071
-
25072
- (_this$_input = this._input) === null || _this$_input === void 0 ? void 0 : _this$_input.focus();
25073
- });
25074
- }
25075
-
25076
- UNSAFE_componentWillUpdate(nextProps) {
25077
- if (this.state.isInputFocused && nextProps.disabled) {
25078
- this.setState({
25079
- isInputFocused: false
25063
+ },
25064
+ onChange: () => {},
25065
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
25066
+ tabIndex: -1
25067
+ }), /*#__PURE__*/React.createElement("label", {
25068
+ htmlFor: choiceId
25069
+ }, a11yText, " \xA0 ", content)), /*#__PURE__*/React.createElement(Clickable, {
25070
+ onClick: () => {
25071
+ // If we're checking a crossed-out option, let's
25072
+ // also uncross it.
25073
+ sendChange({
25074
+ checked: !checked,
25075
+ crossedOut: false
25080
25076
  });
25081
- }
25082
- }
25077
+ },
25078
+ className: descriptionClassName,
25079
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
25080
+ style: {
25081
+ flex: 1
25082
+ },
25083
+ ref: forwardedRef,
25084
+ "aria-hidden": "true"
25085
+ }, _ref => {
25086
+ var _apiOptions$styling;
25083
25087
 
25084
- render() {
25085
- var sat = this.props.apiOptions.satStyling;
25086
25088
  var {
25087
- reviewMode,
25088
- correct,
25089
- checked
25090
- } = this.props; // HACK: while most of the styling for rendering SAT items is handled
25091
- // via aphrodite, we also need to assign normal CSS classnames here to
25092
- // special-case the coloring of MathJax formulas (see .MathJax .math in
25093
- // stylesheets/task-package/tasks.less)
25094
-
25095
- var satCorrectChoice = sat && reviewMode && correct;
25096
- var satIncorrectChecked = sat && reviewMode && !correct && checked;
25097
- var descriptionClassName = classNames("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", css(!sat && styles$r.description, sat && styles$r.satDescription, satCorrectChoice && styles$r.satDescriptionCorrect, satCorrectChoice && checked && styles$r.satDescriptionCorrectChecked, satIncorrectChecked && styles$r.satDescriptionIncorrectChecked));
25098
- var rationaleClassName = classNames("perseus-radio-rationale-content", 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
25099
- // disabled. However, we don't want to do this in the SAT product and
25100
- // we also don't want to do this when we're in review mode in the
25101
- // content library.
25102
-
25103
- var showDimmed = !sat && !reviewMode && this.props.apiOptions.readOnly || this.props.crossedOut;
25089
+ hovered,
25090
+ focused,
25091
+ pressed
25092
+ } = _ref;
25104
25093
  return /*#__PURE__*/React.createElement("div", {
25105
- style: {
25106
- dispay: "flex",
25107
- flexDirection: "column"
25108
- }
25109
- }, /*#__PURE__*/React.createElement("div", {
25110
25094
  style: {
25111
25095
  display: "flex",
25112
25096
  flexDirection: "row",
25113
- width: "100%",
25114
- opacity: showDimmed ? 0.5 : 1.0
25097
+ justifyContent: "center",
25098
+ alignContent: "center",
25099
+ paddingTop: Spacing.xSmall_8,
25100
+ paddingBottom: Spacing.xSmall_8
25115
25101
  }
25116
- }, /*#__PURE__*/React.createElement(Clickable, {
25117
- onClick: () => {
25118
- // If we're checking a crossed-out option, let's
25119
- // also uncross it.
25120
- this._sendChange({
25121
- checked: true,
25122
- crossedOut: false
25123
- });
25124
- },
25125
- className: descriptionClassName,
25126
- disabled: this.props.disabled || this.props.apiOptions.staticRender || this.props.apiOptions.readOnly,
25127
- "aria-label": "Select Choice ".concat(getChoiceLetter(this.props.pos)),
25128
- "aria-checked": this.props.checked,
25129
- role: "checkbox",
25102
+ }, /*#__PURE__*/React.createElement(ChoiceIcon, {
25103
+ pos: pos,
25104
+ correct: correct,
25105
+ crossedOut: crossedOut,
25106
+ pressed: pressed,
25107
+ focused: focused,
25108
+ checked: checked,
25109
+ hovered: hovered,
25110
+ showCorrectness: showCorrectness,
25111
+ multipleSelect: multipleSelect,
25112
+ reviewMode: reviewMode,
25113
+ product: apiOptions.satStyling ? "sat" : "library",
25114
+ primaryProductColor: (_apiOptions$styling = apiOptions.styling) === null || _apiOptions$styling === void 0 ? void 0 : _apiOptions$styling.primaryProductColor,
25115
+ previouslyAnswered: previouslyAnswered
25116
+ }), /*#__PURE__*/React.createElement("span", {
25130
25117
  style: {
25131
- flex: 1
25118
+ paddingLeft: Spacing.small_12,
25119
+ textAlign: "left",
25120
+ flex: 1,
25121
+ paddingTop: 4
25132
25122
  }
25133
- }, _ref2 => {
25123
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(OptionStatus, {
25124
+ checked: checked,
25125
+ correct: correct,
25126
+ crossedOut: crossedOut,
25127
+ previouslyAnswered: previouslyAnswered,
25128
+ reviewMode: reviewMode,
25129
+ satStyling: apiOptions.satStyling
25130
+ })), /*#__PURE__*/React.createElement("div", null, content)));
25131
+ }), apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React.createElement(Popover, {
25132
+ dismissEnabled: true,
25133
+ content: _ref2 => {
25134
25134
  var {
25135
- hovered,
25136
- focused,
25137
- pressed
25135
+ close
25138
25136
  } = _ref2;
25139
- return /*#__PURE__*/React.createElement("div", {
25140
- style: {
25141
- display: "flex",
25142
- flexDirection: "row",
25143
- justifyContent: "center",
25144
- alignContent: "center",
25145
- paddingTop: Spacing.xSmall_8,
25146
- paddingBottom: Spacing.xSmall_8
25147
- }
25148
- }, /*#__PURE__*/React.createElement("span", null, this.renderChoiceIcon({
25149
- isFocused: focused,
25150
- isPressed: pressed
25151
- })), /*#__PURE__*/React.createElement("span", {
25152
- style: {
25153
- paddingLeft: Spacing.small_12,
25154
- textAlign: "left",
25155
- flex: 1,
25156
- paddingTop: 4
25157
- }
25158
- }, /*#__PURE__*/React.createElement("div", null, this.renderOptionStatus()), /*#__PURE__*/React.createElement("div", null, this.props.content)));
25159
- }), this.props.apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React.createElement(Popover, {
25160
- dismissEnabled: true,
25161
- content: _ref3 => {
25162
- var {
25163
- close
25164
- } = _ref3;
25165
- return /*#__PURE__*/React.createElement(PopoverContent, {
25166
- title: "Cross out",
25167
- content: "Cross out option",
25168
- closeButtonVisible: true,
25169
- actions: /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Strut, {
25170
- size: Spacing.medium_16
25171
- }), /*#__PURE__*/React.createElement(Button, {
25172
- kind: "primary",
25173
- "aria-label": "Cross out Choice ".concat(getChoiceLetter(this.props.pos)),
25174
- disabled: this.props.apiOptions.readOnly || reviewMode,
25175
- onClick: () => {
25176
- if (!this.props.crossedOut) {
25177
- // If we're crossing
25178
- // out a checked
25179
- // option, let's also
25180
- // uncheck it.
25181
- this._sendChange({
25182
- checked: false,
25183
- crossedOut: true
25184
- });
25185
- } else {
25186
- this._sendChange({
25187
- crossedOut: false
25188
- });
25189
- }
25190
-
25191
- close();
25137
+ return /*#__PURE__*/React.createElement(PopoverContent, {
25138
+ title: "Cross out",
25139
+ content: "Cross out option",
25140
+ closeButtonVisible: true,
25141
+ actions: /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Strut, {
25142
+ size: Spacing.medium_16
25143
+ }), /*#__PURE__*/React.createElement(Button, {
25144
+ kind: "primary",
25145
+ "aria-label": "Cross out Choice ".concat(getChoiceLetter(pos)),
25146
+ disabled: apiOptions.readOnly || reviewMode,
25147
+ onClick: () => {
25148
+ if (!crossedOut) {
25149
+ // If we're crossing
25150
+ // out a checked
25151
+ // option, let's also
25152
+ // uncheck it.
25153
+ sendChange({
25154
+ checked: false,
25155
+ crossedOut: true
25156
+ });
25157
+ } else {
25158
+ sendChange({
25159
+ crossedOut: false
25160
+ });
25192
25161
  }
25193
- }, this.props.crossedOut ? "Bring back" : "Cross out"))
25194
- });
25195
- }
25162
+
25163
+ close();
25164
+ }
25165
+ }, crossedOut ? "Bring back" : "Cross out"))
25166
+ });
25167
+ }
25168
+ }, _ref3 => {
25169
+ var {
25170
+ open
25171
+ } = _ref3;
25172
+ return /*#__PURE__*/React.createElement(Clickable, {
25173
+ onClick: open,
25174
+ "aria-label": "Open menu for Choice ".concat(getChoiceLetter(pos)),
25175
+ disabled: apiOptions.staticRender
25196
25176
  }, _ref4 => {
25197
- var {
25198
- open
25199
- } = _ref4;
25200
- return /*#__PURE__*/React.createElement(Clickable, {
25201
- onClick: open,
25202
- "aria-label": "Open menu for Choice ".concat(getChoiceLetter(this.props.pos)),
25203
- disabled: this.props.apiOptions.staticRender
25204
- }, _ref5 => {
25205
- return /*#__PURE__*/React.createElement(Icon, {
25206
- icon: ellipsisHorizontalIcon,
25207
- size: 3,
25208
- color: Color.offBlack64
25209
- });
25177
+ return /*#__PURE__*/React.createElement(Icon, {
25178
+ icon: ellipsisHorizontalIcon,
25179
+ size: 3,
25180
+ color: Color.offBlack64
25210
25181
  });
25211
- })), this.props.showRationale && /*#__PURE__*/React.createElement("div", {
25212
- className: rationaleClassName,
25213
- "data-test-id": "perseus-radio-rationale-content-".concat(this.props.pos)
25214
- }, this.props.rationale));
25215
- }
25216
-
25182
+ });
25183
+ })), showRationale && /*#__PURE__*/React.createElement("div", {
25184
+ className: rationaleClassName,
25185
+ "data-test-id": "perseus-radio-rationale-content-".concat(pos)
25186
+ }, rationale));
25217
25187
  }
25218
25188
 
25219
- _defineProperty(Choice, "propTypes", {
25220
- // TODO(kevinb) use Options.propTypes from perseus-api.jsx
25221
- // This change will also require make sure that item-renderer.jsx and
25222
- // server-item-renderer.jsx have appropriate defaults for apiOptions
25223
- // because many of the properties on Options.propTypes are required.
25224
- apiOptions: PropTypes.shape({
25225
- satStyling: PropTypes.bool,
25226
- isMobile: PropTypes.bool,
25227
- styling: PropTypes.shape({
25228
- radioStyleVersion: PropTypes.oneOf(["intermediate", "final"]),
25229
- primaryProductColor: PropTypes.string
25230
- }),
25231
- readOnly: PropTypes.bool
25232
- }),
25233
- checked: PropTypes.bool,
25234
- className: PropTypes.string,
25235
- rationale: PropTypes.node,
25236
- content: PropTypes.node,
25237
- correct: PropTypes.bool,
25238
- deselectEnabled: PropTypes.bool,
25239
- disabled: PropTypes.bool,
25240
- editMode: PropTypes.bool,
25241
- groupName: PropTypes.string,
25242
- isLastChoice: PropTypes.bool,
25243
- // Needed for border styling
25244
- // This indicates the position of the choice relative to others
25245
- // (so that we can display a nice little (A), (B), etc. next to it)
25246
- // Also used to generate an id for each input.
25247
- pos: PropTypes.number,
25248
- reviewMode: PropTypes.bool,
25249
- showRationale: PropTypes.bool,
25250
- showCorrectness: PropTypes.bool,
25251
- type: PropTypes.string,
25252
- // Indicates whether the user has "crossed out" this choice, meaning
25253
- // that they don't think it's correct. This value does not affect
25254
- // scoring or other behavior; it's just a note for the user's
25255
- // reference.
25256
- crossedOut: PropTypes.bool,
25257
- // Indicates that the user has previously selected this answer. These
25258
- // answers may be rendered orange in review, rather than grey if
25259
- // incorrect.
25260
- previouslyAnswered: PropTypes.bool,
25261
- // A callback indicating that this choice has changed. Its argument is
25262
- // an object with two keys: `checked` and `crossedOut`. Each contains a
25263
- // boolean value specifying the new checked and crossed-out value of
25264
- // this choice.
25265
- onChange: PropTypes.func,
25266
- // Callbacks for when the user presses the up arrow or down arrow
25267
- // (respectively) to navigate between choices. When called, the parent
25268
- // finds the prev/next choice, focuses the input, and checks the radio
25269
- // button if appropriate. (The reason we have custom behavior is that
25270
- // we actually _don't_ want to auto-select crossed-out choices!)
25271
- goToPrevChoice: PropTypes.func.isRequired,
25272
- goToNextChoice: PropTypes.func.isRequired
25273
- });
25274
-
25275
- _defineProperty(Choice, "defaultProps", {
25276
- apiOptions: {
25277
- styling: {}
25278
- },
25189
+ Choice.defaultProps = {
25190
+ disabled: false,
25279
25191
  checked: false,
25280
- classSet: {},
25192
+ onChange: newValues => {},
25281
25193
  correct: false,
25282
- disabled: false,
25283
- editMode: false,
25284
- onChange: function onChange() {},
25285
- showRationale: false,
25286
- type: "radio",
25194
+ apiOptions: ApiOptions.defaults,
25195
+ previouslyAnswered: false,
25287
25196
  pos: 0,
25288
- previouslyAnswered: false
25289
- });
25290
-
25291
- var styles$r = StyleSheet.create({
25197
+ showRationale: false
25198
+ };
25199
+ var styles$s = StyleSheet.create({
25292
25200
  description: {
25293
25201
  display: "inline-block",
25294
25202
  width: "100%"
@@ -25328,185 +25236,87 @@ var styles$r = StyleSheet.create({
25328
25236
  marginLeft: 45
25329
25237
  }
25330
25238
  });
25239
+ var Choice$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.createElement(Choice, _extends({}, props, {
25240
+ forwardedRef: ref
25241
+ })));
25331
25242
 
25332
- class ChoiceNoneAbove extends React.Component {
25333
- constructor() {
25334
- super(...arguments);
25335
-
25336
- _defineProperty(this, "_choice", void 0);
25337
-
25338
- _defineProperty(this, "focusInput", () => {
25339
- this._choice.focusInput();
25340
- });
25341
- }
25243
+ var _excluded$2 = ["showContent", "content", "forwardedRef"];
25342
25244
 
25343
- render() {
25344
- var choiceProps = _objectSpread2(_objectSpread2({}, this.props), {}, {
25345
- className: classNames(this.props.className, "none-of-above"),
25346
- content: this.props.showContent ? this.props.content :
25347
- /*#__PURE__*/
25348
- // We use a Renderer here because that is how
25349
- // `this.props.content` is wrapped otherwise.
25350
- // We pass in a key here so that we avoid a semi-spurious
25351
- // react warning when we render this in the same place
25352
- // as the previous choice content renderer.
25353
- // Note this destroys state, but since all we're doing
25354
- // is outputting "None of the above", that is okay.
25355
- React.createElement(Renderer, {
25356
- key: "noneOfTheAboveRenderer",
25357
- content: i18n._("None of the above")
25358
- }),
25359
- ref: el => this._choice = el
25360
- });
25361
-
25362
- return /*#__PURE__*/React.createElement(Choice, choiceProps);
25363
- }
25245
+ function ChoiceNoneAbove(props) {
25246
+ var {
25247
+ showContent,
25248
+ content,
25249
+ forwardedRef
25250
+ } = props,
25251
+ rest = _objectWithoutProperties(props, _excluded$2);
25252
+
25253
+ var choiceProps = _objectSpread2(_objectSpread2({}, rest), {}, {
25254
+ content: showContent ? content :
25255
+ /*#__PURE__*/
25256
+ // We use a Renderer here because that is how
25257
+ // `this.props.content` is wrapped otherwise.
25258
+ // We pass in a key here so that we avoid a semi-spurious
25259
+ // react warning when we render this in the same place
25260
+ // as the previous choice content renderer.
25261
+ // Note this destroys state, but since all we're doing
25262
+ // is outputting "None of the above", that is okay.
25263
+ //
25264
+ // todo(matthewc): this seems like way overkill
25265
+ // just to render a string
25266
+ React.createElement(Renderer, {
25267
+ key: "noneOfTheAboveRenderer",
25268
+ content: i18n._("None of the above")
25269
+ })
25270
+ });
25364
25271
 
25272
+ return /*#__PURE__*/React.createElement(Choice$1, _extends({}, choiceProps, {
25273
+ ref: forwardedRef
25274
+ }));
25365
25275
  }
25366
25276
 
25367
- _defineProperty(ChoiceNoneAbove, "propTypes", {
25368
- className: PropTypes.string,
25369
- content: PropTypes.node,
25370
- showContent: PropTypes.bool
25371
- });
25372
-
25373
- _defineProperty(ChoiceNoneAbove, "defaultProps", {
25277
+ ChoiceNoneAbove.defaultProps = {
25374
25278
  showContent: true
25375
- });
25279
+ };
25280
+ var ChoiceNoneAbove$1 = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.createElement(ChoiceNoneAbove, _extends({}, props, {
25281
+ forwardedRef: ref
25282
+ })));
25376
25283
 
25377
25284
  var {
25378
25285
  captureScratchpadTouchStart: captureScratchpadTouchStart$2
25379
- } = Util;
25380
- var ChoicesType = PropTypes.arrayOf(PropTypes.shape({
25381
- // Indicates whether this choice is checked.
25382
- checked: PropTypes.bool,
25383
- // Indicates whether the user has "crossed out" this choice, meaning
25384
- // that they don't think it's correct. This value does not affect
25385
- // scoring or other behavior; it's just a note for the user's
25386
- // reference.
25387
- crossedOut: PropTypes.bool,
25388
- content: PropTypes.node,
25389
- rationale: PropTypes.node,
25390
- hasRationale: PropTypes.bool,
25391
- showRationale: PropTypes.bool,
25392
- showCorrectness: PropTypes.bool,
25393
- correct: PropTypes.bool,
25394
- originalIndex: PropTypes.number,
25395
- isNoneOfTheAbove: PropTypes.bool
25396
- }));
25397
- var radioBorderColor = radioBorderColor$2;
25286
+ } = Util; // exported for tests
25398
25287
 
25399
- class BaseRadio extends React.Component {
25400
- constructor() {
25401
- super(...arguments);
25402
-
25403
- _defineProperty(this, "state", {
25404
- // TODO(mdr): This keeps the ID stable across re-renders on the
25405
- // same machine, but, at time of writing, the server's state
25406
- // isn't rehydrated to the client during SSR, so the server and
25407
- // client will generate different IDs and cause a mismatch
25408
- // during SSR :(
25409
- radioGroupName: _.uniqueId("perseus_radio_")
25410
- });
25411
-
25412
- _defineProperty(this, "updateChoice", (choiceIndex, newValues) => {
25413
- // Get the baseline `checked` values. If we're checking a new answer
25414
- // and multiple-select is not on, we should clear all choices to be
25415
- // unchecked. Otherwise, we should copy the old checked values.
25416
- var newCheckedList;
25417
-
25418
- if (newValues.checked && !this.props.multipleSelect) {
25419
- newCheckedList = this.props.choices.map(_ => false);
25420
- } else {
25421
- newCheckedList = this.props.choices.map(c => c.checked);
25422
- } // Get the baseline `crossedOut` values.
25423
-
25424
-
25425
- var newCrossedOutList = this.props.choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25426
-
25427
- newCheckedList[choiceIndex] = newValues.checked;
25428
- newCrossedOutList[choiceIndex] = newValues.crossedOut;
25429
- this.props.onChange({
25430
- checked: newCheckedList,
25431
- crossedOut: newCrossedOutList
25288
+ function getInstructionsText(multipleSelect, countChoices, numCorrect) {
25289
+ if (multipleSelect) {
25290
+ if (countChoices) {
25291
+ return i18n._("Choose %(numCorrect)s answers:", {
25292
+ numCorrect: numCorrect
25432
25293
  });
25433
- });
25434
-
25435
- _defineProperty(this, "focus", i => {
25436
- // $FlowFixMe[incompatible-use]
25437
- // $FlowFixMe[prop-missing]
25438
- ReactDOM__default.findDOMNode(this.refs["radio" + (i || 0)]).focus(); // eslint-disable-line react/no-string-refs
25439
-
25440
- return true;
25441
- });
25442
-
25443
- _defineProperty(this, "getInstructionsText", () => {
25444
- if (this.props.multipleSelect) {
25445
- if (this.props.countChoices) {
25446
- return i18n._("Choose %(numCorrect)s answers:", {
25447
- numCorrect: this.props.numCorrect
25448
- });
25449
- }
25450
-
25451
- return i18n._("Choose all answers that apply:");
25452
- }
25453
-
25454
- return i18n._("Choose 1 answer:");
25455
- });
25456
-
25457
- _defineProperty(this, "deselectEnabled", () => {
25458
- // We want to force enable deselect on mobile.
25459
- return this.props.apiOptions.isMobile || this.props.deselectEnabled;
25460
- });
25461
-
25462
- _defineProperty(this, "goToChoice", newChoiceIndex => {
25463
- var numChoices = this.props.choices.length; // Wrap the newChoiceIndex around the start/end, if necessary.
25464
-
25465
- if (newChoiceIndex < 0) {
25466
- newChoiceIndex += numChoices;
25467
- }
25468
-
25469
- if (newChoiceIndex >= numChoices) {
25470
- newChoiceIndex -= numChoices;
25471
- } // Focus the new choice's input element.
25472
- // eslint-disable-next-line react/no-string-refs
25473
-
25474
-
25475
- var choiceInstance = this.refs["radio".concat(newChoiceIndex)];
25476
-
25477
- if (!choiceInstance) {
25478
- Log.error("found no choice at index ".concat(newChoiceIndex, ", even after ") + "wrapping it to be within bounds", Errors.Internal, {
25479
- loggedMetadata: {
25480
- choices: JSON.stringify(this.props.choices)
25481
- }
25482
- });
25483
- return;
25484
- }
25485
-
25486
- choiceInstance.focusInput(); // We aren't going to change any choices' crossed-out state, but we
25487
- // _are_ going to deselect all choices.
25488
-
25489
- var newCrossedOutList = this.props.choices.map(c => c.crossedOut);
25490
- var newCheckedList = this.props.choices.map(c => false); // And, if the new choice isn't crossed out, we'll select it.
25491
-
25492
- if (!this.props.choices[newChoiceIndex].crossedOut) {
25493
- newCheckedList[newChoiceIndex] = true;
25494
- }
25294
+ }
25495
25295
 
25496
- this.props.onChange({
25497
- checked: newCheckedList,
25498
- crossedOut: newCrossedOutList
25499
- });
25500
- });
25296
+ return i18n._("Choose all answers that apply:");
25501
25297
  }
25502
25298
 
25503
- componentDidUpdate(prevProps) {
25504
- var {
25505
- apiOptions,
25506
- choices,
25507
- isLastUsedWidget,
25508
- reviewModeRubric
25509
- } = this.props; // Switching into review mode can sometimes cause the selected answer
25299
+ return i18n._("Choose 1 answer:");
25300
+ }
25301
+
25302
+ function BaseRadio(props) {
25303
+ var {
25304
+ apiOptions,
25305
+ reviewModeRubric,
25306
+ choices,
25307
+ editMode,
25308
+ multipleSelect,
25309
+ labelWrap,
25310
+ countChoices,
25311
+ numCorrect,
25312
+ isLastUsedWidget,
25313
+ registerFocusFunction
25314
+ } = props; // useEffect doesn't have previous props
25315
+
25316
+ var prevReviewModeRubric = useRef();
25317
+ var choiceRefs = useRef([]);
25318
+ useEffect(() => {
25319
+ // Switching into review mode can sometimes cause the selected answer
25510
25320
  // to scroll out of view - for example, when we reveal all those tall
25511
25321
  // rationales. This can be disorienting for the user.
25512
25322
  //
@@ -25519,17 +25329,25 @@ class BaseRadio extends React.Component {
25519
25329
  // the most recently used widget (because, if the user has since
25520
25330
  // touched something else, they're probably not trying to keep their
25521
25331
  // eye on this widget anymore).
25522
-
25523
- if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevProps.reviewModeRubric) {
25332
+ if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevReviewModeRubric.current) {
25524
25333
  var checkedIndex = choices.findIndex(c => c.checked);
25525
25334
 
25526
25335
  if (checkedIndex >= 0) {
25527
- var checkedNode = ReactDOM__default.findDOMNode( // eslint-disable-next-line react/no-string-refs
25528
- this.refs["radio" + checkedIndex]);
25529
- scrollElementIntoView(checkedNode);
25336
+ var ref = choiceRefs.current[checkedIndex]; // note(matthew): we know this is only getting passed
25337
+ // to a WB Clickable button, so we force it to be of
25338
+ // type HTMLButtonElement
25339
+
25340
+ var anyNode = ReactDOM__default.findDOMNode(ref.current);
25341
+ var buttonNode = anyNode;
25342
+
25343
+ if (buttonNode) {
25344
+ scrollElementIntoView(buttonNode);
25345
+ }
25530
25346
  }
25531
25347
  }
25532
- } // When a particular choice's `onChange` handler is called, indicating a
25348
+
25349
+ prevReviewModeRubric.current = reviewModeRubric;
25350
+ }, [apiOptions, choices, isLastUsedWidget, reviewModeRubric]); // When a particular choice's `onChange` handler is called, indicating a
25533
25351
  // change in a single choice's values, we need to call our `onChange`
25534
25352
  // handler in order to notify our parent. However, our API with our parent
25535
25353
  // is that we always provide *all* values for *all* choices, even if just
@@ -25538,181 +25356,197 @@ class BaseRadio extends React.Component {
25538
25356
  // will usually cause the old answer to become unchecked.)
25539
25357
  //
25540
25358
  // So, given the new values for a particular choice, compute the new values
25541
- // for all choices, and pass them to `this.props.onChange`.
25359
+ // for all choices, and pass them to `onChange`.
25542
25360
  //
25543
25361
  // `newValues` is an object with two keys: `checked` and `crossedOut`. Each
25544
25362
  // contains a boolean value specifying the new checked and crossed-out
25545
25363
  // value of this choice.
25546
25364
 
25365
+ function updateChoice(choiceIndex, newValues) {
25366
+ var {
25367
+ multipleSelect,
25368
+ choices,
25369
+ onChange
25370
+ } = props; // Get the baseline `checked` values. If we're checking a new answer
25371
+ // and multiple-select is not on, we should clear all choices to be
25372
+ // unchecked. Otherwise, we should copy the old checked values.
25547
25373
 
25548
- render() {
25549
- var inputType = this.props.multipleSelect ? "checkbox" : "radio";
25550
- var rubric = this.props.reviewModeRubric;
25551
- var reviewMode = !!rubric;
25552
- var styles = BaseRadio.styles;
25553
- var sat = this.props.apiOptions.satStyling;
25554
- var isMobile = this.props.apiOptions.isMobile;
25555
- var choices = this.props.choices;
25556
- var firstChoiceHighlighted = choices[0].highlighted;
25557
- var lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25558
- var className = classNames("perseus-widget-radio", !this.props.editMode && "perseus-rendered-radio", css(styles.radio, // SAT doesn't use the "responsive styling" as it conflicts
25559
- // with their custom theming.
25560
- !sat && styles.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles.radioContainerLastHighlighted, sat && styles.satRadio));
25561
- var instructionsClassName = classNames("instructions", css(styles.instructions, isMobile && styles.instructionsMobile));
25562
- var instructions = this.getInstructionsText();
25563
- var shouldShowInstructions = !sat;
25564
- var responsiveClassName = css(styles.responsiveFieldset);
25565
- var fieldset = /*#__PURE__*/React.createElement("fieldset", {
25566
- className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25567
- }, /*#__PURE__*/React.createElement("legend", {
25568
- className: "perseus-sr-only"
25569
- }, instructions), shouldShowInstructions && /*#__PURE__*/React.createElement("div", {
25570
- className: instructionsClassName
25571
- }, instructions), /*#__PURE__*/React.createElement("ul", {
25572
- className: className,
25573
- style: {
25574
- listStyle: "none"
25575
- }
25576
- }, this.props.choices.map(function (choice, i) {
25577
- var Element = Choice;
25578
- var elementProps = {
25579
- ref: "radio".concat(i),
25580
- apiOptions: this.props.apiOptions,
25581
- checked: choice.checked,
25582
- crossedOut: choice.crossedOut,
25583
- previouslyAnswered: choice.previouslyAnswered,
25584
- reviewMode,
25585
- correct: choice.correct,
25586
- rationale: choice.rationale,
25587
- content: choice.content,
25588
- disabled: this.props.apiOptions.readOnly || choice.disabled,
25589
- editMode: this.props.editMode,
25590
- groupName: this.state.radioGroupName,
25591
- isLastChoice: i === this.props.choices.length - 1,
25592
- showCorrectness: reviewMode || !!choice.showCorrectness,
25593
- showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25594
- type: inputType,
25595
- pos: i,
25596
- deselectEnabled: this.deselectEnabled(),
25597
- onChange: newValues => {
25598
- this.updateChoice(i, newValues);
25599
- },
25600
- goToPrevChoice: () => this.goToChoice(i - 1),
25601
- goToNextChoice: () => this.goToChoice(i + 1)
25602
- };
25374
+ var newCheckedList;
25603
25375
 
25604
- if (choice.isNoneOfTheAbove) {
25605
- Element = ChoiceNoneAbove;
25376
+ if (newValues.checked && !multipleSelect) {
25377
+ newCheckedList = choices.map(_ => false);
25378
+ } else {
25379
+ newCheckedList = choices.map(c => c.checked);
25380
+ } // Get the baseline `crossedOut` values.
25606
25381
 
25607
- _.extend(elementProps, {
25608
- showContent: choice.revealNoneOfTheAbove
25609
- });
25610
- }
25611
25382
 
25612
- var nextChoice = this.props.choices[i + 1];
25613
- var nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25614
-
25615
- var aphroditeClassName = checked => {
25616
- // Whether or not to show correctness borders
25617
- // for this choice and the next choice.
25618
- var satShowCorrectness = sat && reviewMode && checked;
25619
- var satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25620
- return 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);
25621
- }; // HACK(abdulrahman): Preloads the selection-state
25622
- // css because of a bug that causes iOS to lag
25623
- // when selecting the button for the first time.
25624
-
25625
-
25626
- aphroditeClassName(true);
25627
- var className = classNames(aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25628
- // classNames
25629
- 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
25630
- // allow for the contentEditable area to be selected
25631
- // (label forces any clicks inside to select the input
25632
- // element) We have to add some extra behavior to make
25633
- // sure that we can still check the choice.
25634
-
25635
- var listElem = null;
25636
- var clickHandler = null;
25637
-
25638
- if (this.props.editMode) {
25639
- clickHandler = e => {
25640
- // Traverse the parent nodes of the clicked
25641
- // element.
25642
- var elem = e.target;
25643
-
25644
- while (elem && elem !== listElem) {
25645
- // If the clicked element is inside of the
25646
- // radio icon, then we want to trigger the
25647
- // check by flipping the choice of the icon.
25648
- if (elem.getAttribute("data-is-radio-icon")) {
25649
- this.updateChoice(i, {
25650
- checked: !choice.checked
25651
- });
25652
- return;
25653
- }
25383
+ var newCrossedOutList = choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25384
+
25385
+ newCheckedList[choiceIndex] = newValues.checked;
25386
+ newCrossedOutList[choiceIndex] = newValues.crossedOut;
25387
+ onChange({
25388
+ checked: newCheckedList,
25389
+ crossedOut: newCrossedOutList
25390
+ });
25391
+ } // register a callback with the parent that allows
25392
+ // the parent to focus an individual choice
25393
+
25394
+
25395
+ registerFocusFunction === null || registerFocusFunction === void 0 ? void 0 : registerFocusFunction(choiceIndex => {
25396
+ var ref = choiceRefs.current[choiceIndex || 0]; // note(matthew): we know this is only getting passed
25397
+ // to a WB Clickable button, so we force it to be of
25398
+ // type HTMLButtonElement
25399
+
25400
+ var anyNode = ReactDOM__default.findDOMNode(ref.current);
25401
+ var buttonNode = anyNode;
25402
+
25403
+ if (buttonNode) {
25404
+ buttonNode.focus();
25405
+ } else {
25406
+ return false;
25407
+ }
25408
+
25409
+ return true;
25410
+ }); // some commonly used shorthands
25411
+
25412
+ var reviewMode = !!reviewModeRubric;
25413
+ var sat = apiOptions.satStyling;
25414
+ var isMobile = apiOptions.isMobile;
25415
+ var firstChoiceHighlighted = choices[0].highlighted;
25416
+ var lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25417
+ var className = classNames("perseus-widget-radio", !editMode && "perseus-rendered-radio", css(styles$r.radio, // SAT doesn't use the "responsive styling" as it conflicts
25418
+ // with their custom theming.
25419
+ !sat && styles$r.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles$r.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles$r.radioContainerLastHighlighted, sat && styles$r.satRadio));
25420
+ var instructionsClassName = classNames("instructions", css(styles$r.instructions, isMobile && styles$r.instructionsMobile));
25421
+ var instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
25422
+ var shouldShowInstructions = !sat;
25423
+ var responsiveClassName = css(styles$r.responsiveFieldset);
25424
+ var fieldset = /*#__PURE__*/React.createElement("fieldset", {
25425
+ className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25426
+ }, /*#__PURE__*/React.createElement("legend", {
25427
+ className: "perseus-sr-only"
25428
+ }, instructions), shouldShowInstructions && /*#__PURE__*/React.createElement("div", {
25429
+ className: instructionsClassName,
25430
+ "aria-hidden": "true"
25431
+ }, instructions), /*#__PURE__*/React.createElement("ul", {
25432
+ className: className,
25433
+ style: {
25434
+ listStyle: "none"
25435
+ }
25436
+ }, choices.map(function (choice, i) {
25437
+ var Element = Choice$1;
25438
+ var ref = /*#__PURE__*/React.createRef();
25439
+ choiceRefs.current[i] = ref;
25440
+ var elementProps = {
25441
+ apiOptions: apiOptions,
25442
+ multipleSelect: multipleSelect,
25443
+ checked: choice.checked,
25444
+ crossedOut: choice.crossedOut,
25445
+ previouslyAnswered: choice.previouslyAnswered,
25446
+ reviewMode,
25447
+ correct: choice.correct,
25448
+ rationale: choice.rationale,
25449
+ content: choice.content,
25450
+ disabled: apiOptions.readOnly || choice.disabled,
25451
+ showCorrectness: reviewMode || !!choice.showCorrectness,
25452
+ showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25453
+ pos: i,
25454
+ onChange: newValues => {
25455
+ updateChoice(i, newValues);
25456
+ },
25457
+ ref
25458
+ };
25459
+
25460
+ if (choice.isNoneOfTheAbove) {
25461
+ Element = ChoiceNoneAbove$1;
25462
+
25463
+ _.extend(elementProps, {
25464
+ showContent: choice.revealNoneOfTheAbove
25465
+ });
25466
+ }
25467
+
25468
+ var nextChoice = choices[i + 1];
25469
+ var nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25470
+
25471
+ var aphroditeClassName = checked => {
25472
+ // Whether or not to show correctness borders
25473
+ // for this choice and the next choice.
25474
+ var satShowCorrectness = sat && reviewMode && checked;
25475
+ var satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25476
+ return 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);
25477
+ }; // HACK(abdulrahman): Preloads the selection-state
25478
+ // css because of a bug that causes iOS to lag
25479
+ // when selecting the button for the first time.
25480
+
25654
25481
 
25655
- elem = elem.parentNode;
25482
+ aphroditeClassName(true);
25483
+ var correctnessClass; // reviewMode is only true if there's a rubric
25484
+ // but Flow doesn't understand that
25485
+
25486
+ if (reviewMode && reviewModeRubric) {
25487
+ correctnessClass = reviewModeRubric.choices[i].correct ? ClassNames.CORRECT : ClassNames.INCORRECT;
25488
+ }
25489
+
25490
+ var className = classNames(aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25491
+ // classNames
25492
+ ClassNames.RADIO.OPTION, choice.checked && ClassNames.RADIO.SELECTED, correctnessClass); // In edit mode, the Choice renders a Div in order to
25493
+ // allow for the contentEditable area to be selected
25494
+ // (label forces any clicks inside to select the input
25495
+ // element) We have to add some extra behavior to make
25496
+ // sure that we can still check the choice.
25497
+
25498
+ var listElem = null;
25499
+ var clickHandler = null;
25500
+
25501
+ if (editMode) {
25502
+ clickHandler = e => {
25503
+ // Traverse the parent nodes of the clicked
25504
+ // element.
25505
+ var elem = e.target;
25506
+
25507
+ while (elem && elem !== listElem) {
25508
+ // If the clicked element is inside of the
25509
+ // radio icon, then we want to trigger the
25510
+ // check by flipping the choice of the icon.
25511
+ if (elem.getAttribute("data-is-radio-icon")) {
25512
+ this.updateChoice(i, {
25513
+ checked: !choice.checked,
25514
+ crossedOut: choice.crossedOut
25515
+ });
25516
+ return;
25656
25517
  }
25657
- };
25658
- } // TODO(mattdr): Index isn't a *good* choice of key
25659
- // here; is there a better one? Can we use choice
25660
- // content somehow? Would changing our choice of key
25661
- // somehow break something happening inside a choice's
25662
- // child Renderers, by changing when we mount/unmount?
25663
25518
 
25519
+ elem = elem.parentNode;
25520
+ }
25521
+ };
25522
+ } // TODO(mattdr): Index isn't a *good* choice of key
25523
+ // here; is there a better one? Can we use choice
25524
+ // content somehow? Would changing our choice of key
25525
+ // somehow break something happening inside a choice's
25526
+ // child Renderers, by changing when we mount/unmount?
25664
25527
 
25665
- return /*#__PURE__*/React.createElement("li", {
25666
- key: i,
25667
- ref: e => listElem = e,
25668
- className: className,
25669
- onClick: clickHandler,
25670
- onTouchStart: !this.props.labelWrap ? null : captureScratchpadTouchStart$2
25671
- }, /*#__PURE__*/React.createElement(Element, elementProps));
25672
- }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25673
- // an issue especially on phones.
25674
- // This is disabled in SAT, since it conflicts with their theming.
25675
25528
 
25676
- return /*#__PURE__*/React.createElement("div", {
25677
- className: css(!sat && styles.responsiveContainer)
25678
- }, fieldset);
25679
- }
25529
+ return /*#__PURE__*/React.createElement("li", {
25530
+ key: i,
25531
+ ref: e => listElem = e,
25532
+ className: className,
25533
+ onClick: clickHandler,
25534
+ onTouchStart: !labelWrap ? null : captureScratchpadTouchStart$2
25535
+ }, /*#__PURE__*/React.createElement(Element, elementProps));
25536
+ }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25537
+ // an issue especially on phones.
25538
+ // This is disabled in SAT, since it conflicts with their theming.
25680
25539
 
25540
+ return /*#__PURE__*/React.createElement("div", {
25541
+ className: css(!sat && styles$r.responsiveContainer)
25542
+ }, fieldset);
25681
25543
  }
25682
25544
 
25683
- _defineProperty(BaseRadio, "propTypes", {
25684
- apiOptions: PropTypes.shape({
25685
- readOnly: PropTypes.bool,
25686
- satStyling: PropTypes.bool,
25687
- isMobile: PropTypes.bool,
25688
- styling: PropTypes.shape({
25689
- radioStyleVersion: PropTypes.oneOf(["intermediate", "final"])
25690
- }),
25691
- canScrollPage: PropTypes.bool
25692
- }),
25693
- choices: ChoicesType,
25694
- deselectEnabled: PropTypes.bool,
25695
- editMode: PropTypes.bool,
25696
- labelWrap: PropTypes.bool,
25697
- countChoices: PropTypes.bool,
25698
- numCorrect: PropTypes.number,
25699
- multipleSelect: PropTypes.bool,
25700
- reviewModeRubric: PropTypes.shape({
25701
- choices: ChoicesType
25702
- }),
25703
- // A callback indicating that this choice has changed. Its argument is
25704
- // an object with two keys: `checked` and `crossedOut`. Each contains
25705
- // an array of boolean values, specifying the new checked and
25706
- // crossed-out value of each choice.
25707
- onChange: PropTypes.func,
25708
- // Whether this widget was the most recently used widget in this
25709
- // Renderer. Determines whether we'll auto-scroll the page upon
25710
- // entering review mode.
25711
- isLastUsedWidget: PropTypes.bool
25712
- });
25713
-
25714
- _defineProperty(BaseRadio, "styles", StyleSheet.create({
25715
- // eslint-disable-next-line react-native/no-unused-styles
25545
+ BaseRadio.defaultProps = {
25546
+ editMode: false,
25547
+ multipleSelect: false
25548
+ };
25549
+ var styles$r = StyleSheet.create({
25716
25550
  instructions: {
25717
25551
  display: "block",
25718
25552
  color: gray17,
@@ -25722,7 +25556,6 @@ _defineProperty(BaseRadio, "styles", StyleSheet.create({
25722
25556
  fontWeight: "bold",
25723
25557
  marginBottom: 16
25724
25558
  },
25725
- // eslint-disable-next-line react-native/no-unused-styles
25726
25559
  instructionsMobile: {
25727
25560
  fontSize: 18,
25728
25561
  [mediaQueries.smOrSmaller]: {
@@ -25736,36 +25569,30 @@ _defineProperty(BaseRadio, "styles", StyleSheet.create({
25736
25569
  fontSize: 20
25737
25570
  }
25738
25571
  },
25739
- // eslint-disable-next-line react-native/no-unused-styles
25740
25572
  radio: {
25741
25573
  // Avoid centering
25742
25574
  width: "100%"
25743
25575
  },
25744
- // eslint-disable-next-line react-native/no-unused-styles
25745
25576
  responsiveRadioContainer: {
25746
- borderBottom: "1px solid ".concat(radioBorderColor),
25747
- borderTop: "1px solid ".concat(radioBorderColor),
25577
+ borderBottom: "1px solid ".concat(radioBorderColor$1),
25578
+ borderTop: "1px solid ".concat(radioBorderColor$1),
25748
25579
  width: "auto",
25749
25580
  [mediaQueries.smOrSmaller]: {
25750
25581
  marginLeft: negativePhoneMargin,
25751
25582
  marginRight: negativePhoneMargin
25752
25583
  }
25753
25584
  },
25754
- // eslint-disable-next-line react-native/no-unused-styles
25755
25585
  radioContainerFirstHighlighted: {
25756
25586
  borderTop: "1px solid rgba(0, 0, 0, 0)"
25757
25587
  },
25758
- // eslint-disable-next-line react-native/no-unused-styles
25759
25588
  radioContainerLastHighlighted: {
25760
25589
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25761
25590
  },
25762
- // eslint-disable-next-line react-native/no-unused-styles
25763
25591
  satRadio: {
25764
25592
  background: "none",
25765
25593
  marginLeft: 0,
25766
25594
  userSelect: "none"
25767
25595
  },
25768
- // eslint-disable-next-line react-native/no-unused-styles
25769
25596
  satRadioOption: {
25770
25597
  margin: 0,
25771
25598
  padding: 0,
@@ -25774,57 +25601,40 @@ _defineProperty(BaseRadio, "styles", StyleSheet.create({
25774
25601
  borderTop: "1px solid #ccc"
25775
25602
  }
25776
25603
  },
25777
- // eslint-disable-next-line react-native/no-unused-styles
25778
25604
  satRadioOptionCorrect: {
25779
25605
  borderBottomColor: satCorrectBorderColor,
25780
25606
  ":first-child": {
25781
25607
  borderTopColor: satCorrectBorderColor
25782
25608
  }
25783
25609
  },
25784
- // eslint-disable-next-line react-native/no-unused-styles
25785
25610
  satRadioOptionIncorrect: {
25786
25611
  borderBottomColor: satIncorrectBorderColor,
25787
25612
  ":first-child": {
25788
25613
  borderTopColor: satIncorrectBorderColor
25789
25614
  }
25790
25615
  },
25791
- // eslint-disable-next-line react-native/no-unused-styles
25792
25616
  satRadioOptionNextCorrect: {
25793
25617
  borderBottomColor: satCorrectBorderColor
25794
25618
  },
25795
- // eslint-disable-next-line react-native/no-unused-styles
25796
25619
  satRadioOptionNextIncorrect: {
25797
25620
  borderBottomColor: satIncorrectBorderColor
25798
25621
  },
25799
- // eslint-disable-next-line react-native/no-unused-styles
25800
25622
  satReviewRadioOption: {
25801
25623
  pointerEvents: "none"
25802
25624
  },
25803
- // eslint-disable-next-line react-native/no-unused-styles
25804
25625
  item: {
25805
25626
  marginLeft: 20
25806
25627
  },
25807
- // eslint-disable-next-line react-native/no-unused-styles
25808
- inlineItem: {
25809
- display: "inline-block",
25810
- paddingLeft: 20,
25811
- verticalAlign: "middle" // See http://stackoverflow.com/q/8120466 for explanation of
25812
- // why vertical align property is needed
25813
-
25814
- },
25815
- // eslint-disable-next-line react-native/no-unused-styles
25816
25628
  responsiveItem: {
25817
25629
  marginLeft: 0,
25818
25630
  padding: 0,
25819
25631
  ":not(:last-child)": {
25820
- borderBottom: "1px solid ".concat(radioBorderColor)
25632
+ borderBottom: "1px solid ".concat(radioBorderColor$1)
25821
25633
  }
25822
25634
  },
25823
- // eslint-disable-next-line react-native/no-unused-styles
25824
25635
  selectedItem: {
25825
25636
  background: "white"
25826
25637
  },
25827
- // eslint-disable-next-line react-native/no-unused-styles
25828
25638
  aboveBackdrop: {
25829
25639
  position: "relative",
25830
25640
  // HACK(emily): We want selected choices to show up above our
@@ -25833,20 +25643,17 @@ _defineProperty(BaseRadio, "styles", StyleSheet.create({
25833
25643
  // coordinated between here and webapp. :(
25834
25644
  zIndex: 1062
25835
25645
  },
25836
- // eslint-disable-next-line react-native/no-unused-styles
25837
25646
  aboveBackdropMobile: {
25838
25647
  boxShadow: "0 0 4px 0 rgba(0, 0, 0, 0.2)," + "0 0 2px 0 rgba(0, 0, 0, 0.1)",
25839
25648
  ":not(:last-child)": {
25840
25649
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25841
25650
  }
25842
25651
  },
25843
- // eslint-disable-next-line react-native/no-unused-styles
25844
25652
  nextHighlighted: {
25845
25653
  ":not(:last-child)": {
25846
25654
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25847
25655
  }
25848
25656
  },
25849
- // eslint-disable-next-line react-native/no-unused-styles
25850
25657
  responsiveContainer: {
25851
25658
  overflow: "auto",
25852
25659
  marginLeft: negativePhoneMargin,
@@ -25854,22 +25661,22 @@ _defineProperty(BaseRadio, "styles", StyleSheet.create({
25854
25661
  paddingLeft: phoneMargin // paddingRight is handled by responsiveFieldset
25855
25662
 
25856
25663
  },
25857
- // eslint-disable-next-line react-native/no-unused-styles
25858
25664
  responsiveFieldset: {
25859
25665
  paddingRight: phoneMargin
25860
25666
  }
25861
- }));
25862
-
25863
- _defineProperty(BaseRadio, "defaultProps", {
25864
- editMode: false
25865
25667
  });
25866
25668
 
25867
25669
  class Radio$1 extends React.Component {
25868
25670
  constructor() {
25671
+ var _this;
25672
+
25869
25673
  super(...arguments);
25674
+ _this = this;
25675
+
25676
+ _defineProperty(this, "focusFunction", void 0);
25870
25677
 
25871
- _defineProperty(this, "_renderRenderer", content => {
25872
- content = content || "";
25678
+ _defineProperty(this, "_renderRenderer", function () {
25679
+ var content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
25873
25680
  var nextPassageRefId = 1;
25874
25681
  var widgets = {};
25875
25682
  var modContent = content.replace(/\{\{passage-ref (\d+) (\d+)(?: "([^"]*)")?\}\}/g, (match, passageNum, refNum, summaryText) => {
@@ -25901,19 +25708,14 @@ class Radio$1 extends React.Component {
25901
25708
  key: "choiceContentRenderer",
25902
25709
  content: modContent,
25903
25710
  widgets: widgets,
25904
- findExternalWidgets: this.props.findWidgets,
25711
+ findExternalWidgets: _this.props.findWidgets,
25905
25712
  alwaysUpdate: true,
25906
- linterContext: _objectSpread2(_objectSpread2({}, this.props.linterContext), {}, {
25713
+ linterContext: _objectSpread2(_objectSpread2({}, _this.props.linterContext), {}, {
25907
25714
  blockHighlight: true
25908
25715
  })
25909
25716
  });
25910
25717
  });
25911
25718
 
25912
- _defineProperty(this, "focus", i => {
25913
- // eslint-disable-next-line react/no-string-refs
25914
- return this.refs.baseRadio.focus(i);
25915
- });
25916
-
25917
25719
  _defineProperty(this, "updateChoices", newValueLists => {
25918
25720
  var {
25919
25721
  choiceStates,
@@ -25924,24 +25726,17 @@ class Radio$1 extends React.Component {
25924
25726
  // should clone the old `choiceStates` objects, in preparation to
25925
25727
  // mutate them.
25926
25728
 
25927
- var newChoiceStates;
25928
-
25929
- if (choiceStates) {
25930
- newChoiceStates = choiceStates.map(state => _objectSpread2({}, state));
25931
- } else {
25932
- newChoiceStates = choices.map(() => ({
25933
- selected: false,
25934
- crossedOut: false,
25935
- highlighted: false,
25936
- rationaleShown: false,
25937
- correctnessShown: false,
25938
- previouslyAnswered: false,
25939
- readOnly: false
25940
- }));
25941
- } // Mutate the new `choiceState` objects, according to the new `checked`
25729
+ var newChoiceStates = choiceStates ? choiceStates.map(state => _objectSpread2({}, state)) : choices.map(() => ({
25730
+ selected: false,
25731
+ crossedOut: false,
25732
+ highlighted: false,
25733
+ rationaleShown: false,
25734
+ correctnessShown: false,
25735
+ previouslyAnswered: false,
25736
+ readOnly: false
25737
+ })); // Mutate the new `choiceState` objects, according to the new `checked`
25942
25738
  // and `crossedOut` values provided in `newValueLists`.
25943
25739
 
25944
-
25945
25740
  newChoiceStates.forEach((choiceState, i) => {
25946
25741
  choiceState.selected = newValueLists.checked[i];
25947
25742
  choiceState.crossedOut = newValueLists.crossedOut[i];
@@ -26005,14 +25800,47 @@ class Radio$1 extends React.Component {
26005
25800
  });
26006
25801
  }
26007
25802
 
25803
+ // TODO(LP-10672): I think this might be unused right now. I can't find anywhere
25804
+ // that we pass a value to `.focus()` and it seems to have been used for
25805
+ // adding hints when editing.
25806
+ // See: https://github.com/Khan/perseus/blame/e18582b4b69959270b90e237ef1813899711ddfa/src/widgets/radio.js#L169
25807
+ focus(choiceIndex) {
25808
+ if (this.focusFunction) {
25809
+ return this.focusFunction(choiceIndex);
25810
+ }
25811
+
25812
+ return false;
25813
+ } // lets BaseRadio regiser a focus callback so widget
25814
+ // can focus an individual choice
25815
+
25816
+
25817
+ registerFocusFunction(fun) {
25818
+ this.focusFunction = fun;
25819
+ } // When `BaseRadio`'s `onChange` handler is called, indicating a change in
25820
+ // our choices' state, we need to call our `onChange` handler in order to
25821
+ // persist those changes in the item's Perseus state.
25822
+ //
25823
+ // So, given the new values for each choice, construct the new
25824
+ // `choiceStates` objects, and pass them to `this.props.onChange`.
25825
+ //
25826
+ // `newValueLists` is an object with two keys: `checked` and `crossedOut`.
25827
+ // Each contains an array of boolean values, specifying the new checked and
25828
+ // crossed-out value of each choice.
25829
+ //
25830
+ // NOTE(mdr): This method expects to be auto-bound. If this component is
25831
+ // converted to an ES6 class, take care to auto-bind this method!
25832
+
25833
+
26008
25834
  render() {
26009
- var choices = this.props.choices;
25835
+ var {
25836
+ choices
25837
+ } = this.props;
26010
25838
  var choiceStates;
26011
25839
 
26012
25840
  if (this.props.static) {
26013
- choiceStates = _.map(choices, val => ({
26014
- selected: val.correct,
26015
- crossedOut: val.crossedOut,
25841
+ choiceStates = choices.map(choice => ({
25842
+ selected: !!choice.correct,
25843
+ crossedOut: false,
26016
25844
  readOnly: true,
26017
25845
  highlighted: false,
26018
25846
  rationaleShown: true,
@@ -26020,12 +25848,12 @@ class Radio$1 extends React.Component {
26020
25848
  previouslyAnswered: false
26021
25849
  }));
26022
25850
  } else if (this.props.choiceStates) {
26023
- choiceStates = this.props.choiceStates; // $FlowFixMe[prop-missing]
25851
+ choiceStates = this.props.choiceStates;
26024
25852
  } else if (this.props.values) {
26025
25853
  // Support legacy choiceStates implementation
26026
25854
 
26027
25855
  /* istanbul ignore next - props.values is deprecated */
26028
- choiceStates = _.map(this.props.values, val => ({
25856
+ choiceStates = this.props.values.map(val => ({
26029
25857
  selected: val,
26030
25858
  crossedOut: false,
26031
25859
  readOnly: false,
@@ -26035,7 +25863,7 @@ class Radio$1 extends React.Component {
26035
25863
  previouslyAnswered: false
26036
25864
  }));
26037
25865
  } else {
26038
- choiceStates = _.map(choices, () => ({
25866
+ choiceStates = choices.map(() => ({
26039
25867
  selected: false,
26040
25868
  crossedOut: false,
26041
25869
  readOnly: false,
@@ -26046,7 +25874,7 @@ class Radio$1 extends React.Component {
26046
25874
  }));
26047
25875
  }
26048
25876
 
26049
- choices = _.map(choices, (choice, i) => {
25877
+ var choicesProp = choices.map((choice, i) => {
26050
25878
  var content = choice.isNoneOfTheAbove && !choice.content ? // we use i18n._ instead of $_ here because the content
26051
25879
  // sent to a renderer needs to be a string, not a react
26052
25880
  // node (/renderable/fragment).
@@ -26074,37 +25902,36 @@ class Radio$1 extends React.Component {
26074
25902
  // TODO(emily): Come up with a more comprehensive way to solve
26075
25903
  // this sort of "serialized state breaks when internal
26076
25904
  // structure changes" problem.
26077
- correct: typeof choice.correct === "undefined" ? !!reviewChoice && reviewChoice.correct : choice.correct,
25905
+ correct: choice.correct === undefined ? !!reviewChoice && !!reviewChoice.correct : choice.correct,
26078
25906
  disabled: readOnly,
26079
25907
  hasRationale: !!choice.clue,
26080
25908
  rationale: this._renderRenderer(choice.clue),
26081
25909
  showRationale: rationaleShown,
26082
25910
  showCorrectness: correctnessShown,
26083
- isNoneOfTheAbove: choice.isNoneOfTheAbove,
26084
- revealNoneOfTheAbove: this.props.questionCompleted && selected,
25911
+ isNoneOfTheAbove: !!choice.isNoneOfTheAbove,
25912
+ revealNoneOfTheAbove: !!(this.props.questionCompleted && selected),
26085
25913
  crossedOut,
26086
25914
  highlighted,
26087
25915
  previouslyAnswered: previouslyAnswered
26088
25916
  };
26089
25917
  });
26090
- return /*#__PURE__*/React.createElement(BaseRadio // eslint-disable-next-line react/no-string-refs
26091
- , {
26092
- ref: "baseRadio",
25918
+ return /*#__PURE__*/React.createElement(BaseRadio, {
26093
25919
  labelWrap: true,
26094
25920
  multipleSelect: this.props.multipleSelect,
26095
25921
  countChoices: this.props.countChoices,
26096
25922
  numCorrect: this.props.numCorrect,
26097
- choices: choices,
25923
+ choices: choicesProp,
26098
25924
  onChange: this.updateChoices,
26099
25925
  reviewModeRubric: this.props.reviewModeRubric,
26100
25926
  deselectEnabled: this.props.deselectEnabled,
26101
25927
  apiOptions: this.props.apiOptions,
26102
- isLastUsedWidget: this.props.isLastUsedWidget
25928
+ isLastUsedWidget: this.props.isLastUsedWidget,
25929
+ registerFocusFunction: i => this.registerFocusFunction(i)
26103
25930
  });
26104
25931
  }
26105
25932
 
26106
25933
  static validate(userInput, rubric) {
26107
- var numSelected = _.reduce(userInput.choicesSelected, (sum, selected) => {
25934
+ var numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26108
25935
  return sum + (selected ? 1 : 0);
26109
25936
  }, 0);
26110
25937
 
@@ -26113,10 +25940,9 @@ class Radio$1 extends React.Component {
26113
25940
  type: "invalid",
26114
25941
  message: null
26115
25942
  };
26116
- } // $FlowFixMe[invalid-compare]
26117
-
25943
+ }
26118
25944
 
26119
- if (userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
25945
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
26120
25946
  return {
26121
25947
  type: "invalid",
26122
25948
  message: i18n._("Please choose the correct number of answers.")
@@ -26129,14 +25955,12 @@ class Radio$1 extends React.Component {
26129
25955
  message: i18n._("'None of the above' may not be selected " + "when other answers are selected.")
26130
25956
  };
26131
25957
  }
26132
- /* jshint -W018 */
26133
25958
 
26134
-
26135
- var correct = _.all(userInput.choicesSelected, (selected, i) => {
25959
+ var correct = userInput.choicesSelected.every((selected, i) => {
26136
25960
  var isCorrect;
26137
25961
 
26138
25962
  if (userInput.noneOfTheAboveIndex === i) {
26139
- isCorrect = _.all(rubric.choices, (choice, j) => {
25963
+ isCorrect = rubric.choices.every((choice, j) => {
26140
25964
  return i === j || !choice.correct;
26141
25965
  });
26142
25966
  } else {
@@ -26145,9 +25969,6 @@ class Radio$1 extends React.Component {
26145
25969
 
26146
25970
  return isCorrect === selected;
26147
25971
  });
26148
- /* jshint +W018 */
26149
-
26150
-
26151
25972
  return {
26152
25973
  type: "points",
26153
25974
  earned: correct ? 1 : 0,
@@ -26169,9 +25990,7 @@ class Radio$1 extends React.Component {
26169
25990
  var numCorrect = props.numCorrect;
26170
25991
 
26171
25992
  for (var i = 0; i < choicesSelected.length; i++) {
26172
- // $FlowFixMe[prop-missing]
26173
- var index = props.choices[i].originalIndex; // $FlowFixMe[incompatible-use]
26174
-
25993
+ var index = props.choices[i].originalIndex;
26175
25994
  choicesSelected[index] = choiceStates[i].selected;
26176
25995
 
26177
25996
  if (props.choices[i].isNoneOfTheAbove) {
@@ -26192,31 +26011,35 @@ class Radio$1 extends React.Component {
26192
26011
  }; // Support legacy choiceState implementation
26193
26012
  }
26194
26013
  /* istanbul ignore if - props.values is deprecated */
26195
- // $FlowFixMe[prop-missing]
26196
26014
 
26197
26015
 
26198
- if (props.values) {
26016
+ var {
26017
+ values
26018
+ } = props;
26019
+
26020
+ if (values) {
26199
26021
  var _noneOfTheAboveIndex = null;
26200
26022
  var _noneOfTheAboveSelected = false;
26201
- var values = props.values.slice();
26023
+ var _choicesSelected = [...values];
26202
26024
  var _countChoices = props.countChoices;
26203
26025
  var _numCorrect = props.numCorrect;
26026
+ var valuesLength = values.length;
26204
26027
 
26205
- for (var _i = 0; _i < props.values.length; _i++) {
26028
+ for (var _i = 0; _i < valuesLength; _i++) {
26206
26029
  var _index = props.choices[_i].originalIndex;
26207
- values[_index] = props.values[_i];
26030
+ _choicesSelected[_index] = values[_i];
26208
26031
 
26209
26032
  if (props.choices[_i].isNoneOfTheAbove) {
26210
26033
  _noneOfTheAboveIndex = _index;
26211
26034
 
26212
- if (values[_i]) {
26035
+ if (_choicesSelected[_i]) {
26213
26036
  _noneOfTheAboveSelected = true;
26214
26037
  }
26215
26038
  }
26216
26039
  }
26217
26040
 
26218
26041
  return {
26219
- choicesSelected: values,
26042
+ choicesSelected: _choicesSelected,
26220
26043
  noneOfTheAboveIndex: _noneOfTheAboveIndex,
26221
26044
  noneOfTheAboveSelected: _noneOfTheAboveSelected,
26222
26045
  countChoices: _countChoices,
@@ -26226,15 +26049,14 @@ class Radio$1 extends React.Component {
26226
26049
 
26227
26050
 
26228
26051
  return {
26229
- choicesSelected: _.map(props.choices, () => false)
26052
+ choicesSelected: props.choices.map(() => false)
26230
26053
  };
26231
26054
  }
26232
26055
 
26233
26056
  }
26234
26057
 
26235
26058
  _defineProperty(Radio$1, "defaultProps", {
26236
- choices: [{}],
26237
- displayCount: null,
26059
+ choices: [],
26238
26060
  multipleSelect: false,
26239
26061
  countChoices: false,
26240
26062
  deselectEnabled: false,
@@ -26265,14 +26087,14 @@ var _choiceTransform = (widgetOptions, problemNum) => {
26265
26087
 
26266
26088
  var _addNoneOfAbove = function _addNoneOfAbove(choices) {
26267
26089
  var noneOfTheAbove = null;
26268
-
26269
- var newChoices = _.reject(choices, function (choice, index) {
26090
+ var newChoices = choices.filter((choice, index) => {
26270
26091
  if (choice.isNoneOfTheAbove) {
26271
26092
  noneOfTheAbove = choice;
26272
- return true;
26093
+ return false;
26273
26094
  }
26274
- }); // Place the "None of the above" options last
26275
26095
 
26096
+ return true;
26097
+ }); // Place the "None of the above" options last
26276
26098
 
26277
26099
  if (noneOfTheAbove) {
26278
26100
  newChoices.push(noneOfTheAbove);
@@ -26292,9 +26114,8 @@ var _choiceTransform = (widgetOptions, problemNum) => {
26292
26114
  }; // Add meta-information to choices
26293
26115
 
26294
26116
 
26295
- var choices = widgetOptions.choices.slice();
26296
- choices = _.map(choices, (choice, i) => {
26297
- return _.extend({}, choice, {
26117
+ var choices = widgetOptions.choices.map((choice, i) => {
26118
+ return _objectSpread2(_objectSpread2({}, choice), {}, {
26298
26119
  originalIndex: i,
26299
26120
  correct: Boolean(choice.correct)
26300
26121
  });
@@ -26882,7 +26703,7 @@ class Definition extends React.Component {
26882
26703
  }, /*#__PURE__*/React.createElement("span", {
26883
26704
  className: "perseus-widget-definition"
26884
26705
  }, /*#__PURE__*/React.createElement(Button, {
26885
- size: "small",
26706
+ size: "medium",
26886
26707
  kind: "tertiary",
26887
26708
  onClick: () => {
26888
26709
  this.props.trackInteraction();
@@ -29099,6 +28920,8 @@ class Group extends React.Component {
29099
28920
  super(...arguments);
29100
28921
  _this = this;
29101
28922
 
28923
+ _defineProperty(this, "rendererRef", void 0);
28924
+
29102
28925
  _defineProperty(this, "change", function () {
29103
28926
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
29104
28927
  args[_key] = arguments[_key];
@@ -29108,56 +28931,66 @@ class Group extends React.Component {
29108
28931
  });
29109
28932
 
29110
28933
  _defineProperty(this, "getUserInput", () => {
29111
- // eslint-disable-next-line react/no-string-refs
29112
- return this.refs.renderer.getUserInput();
28934
+ var _this$rendererRef;
28935
+
28936
+ return (_this$rendererRef = this.rendererRef) === null || _this$rendererRef === void 0 ? void 0 : _this$rendererRef.getUserInput();
29113
28937
  });
29114
28938
 
29115
28939
  _defineProperty(this, "getSerializedState", () => {
29116
- // eslint-disable-next-line react/no-string-refs
29117
- return this.refs.renderer.getSerializedState();
28940
+ var _this$rendererRef2;
28941
+
28942
+ return (_this$rendererRef2 = this.rendererRef) === null || _this$rendererRef2 === void 0 ? void 0 : _this$rendererRef2.getSerializedState();
29118
28943
  });
29119
28944
 
29120
28945
  _defineProperty(this, "restoreSerializedState", (state, callback) => {
29121
- // eslint-disable-next-line react/no-string-refs
29122
- this.refs.renderer.restoreSerializedState(state, callback); // Tell our renderer that we have no props to change
28946
+ var _this$rendererRef3;
28947
+
28948
+ (_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
29123
28949
  // (all our changes were in state):
29124
28950
 
29125
28951
  return null;
29126
28952
  });
29127
28953
 
29128
28954
  _defineProperty(this, "simpleValidate", rubric => {
29129
- // eslint-disable-next-line react/no-string-refs
29130
- return this.refs.renderer.score();
28955
+ var _this$rendererRef4;
28956
+
28957
+ return (_this$rendererRef4 = this.rendererRef) === null || _this$rendererRef4 === void 0 ? void 0 : _this$rendererRef4.score();
29131
28958
  });
29132
28959
 
29133
28960
  _defineProperty(this, "getInputPaths", () => {
29134
- // eslint-disable-next-line react/no-string-refs
29135
- return this.refs.renderer.getInputPaths();
28961
+ var _this$rendererRef5;
28962
+
28963
+ return (_this$rendererRef5 = this.rendererRef) === null || _this$rendererRef5 === void 0 ? void 0 : _this$rendererRef5.getInputPaths();
29136
28964
  });
29137
28965
 
29138
- _defineProperty(this, "setInputValue", (path, newValue, cb) => {
29139
- // eslint-disable-next-line react/no-string-refs
29140
- return this.refs.renderer.setInputValue(path, newValue, cb);
28966
+ _defineProperty(this, "setInputValue", (path, newValue, callback) => {
28967
+ var _this$rendererRef6;
28968
+
28969
+ return (_this$rendererRef6 = this.rendererRef) === null || _this$rendererRef6 === void 0 ? void 0 : _this$rendererRef6.setInputValue(path, newValue, callback);
29141
28970
  });
29142
28971
 
29143
28972
  _defineProperty(this, "focus", () => {
29144
- // eslint-disable-next-line react/no-string-refs
29145
- return this.refs.renderer.focus();
28973
+ var _this$rendererRef7;
28974
+
28975
+ return (_this$rendererRef7 = this.rendererRef) === null || _this$rendererRef7 === void 0 ? void 0 : _this$rendererRef7.focus();
29146
28976
  });
29147
28977
 
29148
28978
  _defineProperty(this, "focusInputPath", path => {
29149
- // eslint-disable-next-line react/no-string-refs
29150
- this.refs.renderer.focusPath(path);
28979
+ var _this$rendererRef8;
28980
+
28981
+ (_this$rendererRef8 = this.rendererRef) === null || _this$rendererRef8 === void 0 ? void 0 : _this$rendererRef8.focusPath(path);
29151
28982
  });
29152
28983
 
29153
28984
  _defineProperty(this, "blurInputPath", path => {
29154
- // eslint-disable-next-line react/no-string-refs
29155
- this.refs.renderer.blurPath(path);
28985
+ var _this$rendererRef9;
28986
+
28987
+ (_this$rendererRef9 = this.rendererRef) === null || _this$rendererRef9 === void 0 ? void 0 : _this$rendererRef9.blurPath(path);
29156
28988
  });
29157
28989
 
29158
28990
  _defineProperty(this, "showRationalesForCurrentlySelectedChoices", () => {
29159
- // eslint-disable-next-line react/no-string-refs
29160
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
28991
+ var _this$rendererRef10;
28992
+
28993
+ (_this$rendererRef10 = this.rendererRef) === null || _this$rendererRef10 === void 0 ? void 0 : _this$rendererRef10.showRationalesForCurrentlySelectedChoices();
29161
28994
  });
29162
28995
  }
29163
28996
 
@@ -29169,7 +29002,9 @@ class Group extends React.Component {
29169
29002
  }
29170
29003
 
29171
29004
  render() {
29172
- var apiOptions = _.extend({}, ApiOptions.defaults, this.props.apiOptions, {
29005
+ var _this$rendererRef11;
29006
+
29007
+ var apiOptions = _objectSpread2(_objectSpread2(_objectSpread2({}, ApiOptions.defaults), this.props.apiOptions), {}, {
29173
29008
  // Api Rewriting to support correct onFocus/onBlur
29174
29009
  // events for the mobile API
29175
29010
  onFocusChange: (newFocus, oldFocus) => {
@@ -29191,8 +29026,8 @@ class Group extends React.Component {
29191
29026
  // really we should have a more unidirectional flow. TODO(marcia): fix.
29192
29027
 
29193
29028
 
29194
- var number = _.indexOf(this.props.findWidgets("group"), this);
29195
-
29029
+ var groupWidgets = this.props.findWidgets("group");
29030
+ var number = groupWidgets.indexOf(this);
29196
29031
  var problemNumComponent = this.props.apiOptions.groupAnnotator(number, this.props.widgetId); // This is a little strange because the id of the widget that actually
29197
29032
  // changed is going to be lost in favor of the group widget's id. The
29198
29033
  // widgets prop also wasn't actually changed, and this only serves to
@@ -29200,15 +29035,12 @@ class Group extends React.Component {
29200
29035
  // has occurred.
29201
29036
 
29202
29037
  var onInteractWithWidget = id => {
29203
- // eslint-disable-next-line react/no-string-refs
29204
- if (this.refs.renderer) {
29205
- // eslint-disable-next-line react/no-string-refs
29206
- this.change("widgets", this.refs.renderer.props.widgets);
29038
+ if (this.rendererRef) {
29039
+ this.change("widgets", this.rendererRef.props.widgets);
29207
29040
  }
29208
- }; // eslint-disable-next-line react/no-string-refs
29209
-
29041
+ };
29210
29042
 
29211
- var score = this.refs.renderer && this.refs.renderer.score();
29043
+ var score = (_this$rendererRef11 = this.rendererRef) === null || _this$rendererRef11 === void 0 ? void 0 : _this$rendererRef11.score();
29212
29044
  var isValid = score && score.type !== "invalid";
29213
29045
  var isInvalid = score && score.type === "invalid"; // TODO(mdr): Widgets inside this Renderer are not discoverable through
29214
29046
  // the parent Renderer's `findWidgets` function.
@@ -29219,15 +29051,17 @@ class Group extends React.Component {
29219
29051
  "perseus-group-valid-answer": isValid,
29220
29052
  "perseus-group-invalid-answer": isInvalid
29221
29053
  })
29222
- }, problemNumComponent, /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props, {
29223
- // eslint-disable-next-line react/no-string-refs
29224
- ref: "renderer",
29054
+ }, problemNumComponent, /*#__PURE__*/React.createElement(Renderer, {
29055
+ content: this.props.content,
29056
+ widgets: this.props.widgets,
29057
+ images: this.props.images,
29058
+ ref: _ref => this.rendererRef = _ref,
29225
29059
  apiOptions: apiOptions,
29226
29060
  findExternalWidgets: this.props.findWidgets,
29227
29061
  reviewMode: !!this.props.reviewModeRubric,
29228
29062
  onInteractWithWidget: onInteractWithWidget,
29229
29063
  linterContext: this.props.linterContext
29230
- })), this.props.icon && /*#__PURE__*/React.createElement("div", {
29064
+ }), this.props.icon && /*#__PURE__*/React.createElement("div", {
29231
29065
  className: "group-icon"
29232
29066
  }, this.props.icon));
29233
29067
  }
@@ -29242,7 +29076,7 @@ _defineProperty(Group, "defaultProps", {
29242
29076
  });
29243
29077
 
29244
29078
  var traverseChildWidgets$1 = function traverseChildWidgets(props, traverseRenderer) {
29245
- return _.extend({}, props, traverseRenderer(props));
29079
+ return _objectSpread2(_objectSpread2({}, props), traverseRenderer(props));
29246
29080
  };
29247
29081
 
29248
29082
  var Group$1 = {
@@ -41132,43 +40966,23 @@ var styles$9 = StyleSheet.create({
41132
40966
  }
41133
40967
  });
41134
40968
 
41135
- var getLineHeightForNode = ($line1, $line2) => {
41136
- return $line2.offset().top - $line1.offset().top;
40969
+ var getLineHeightForNode = (line1, line2) => {
40970
+ return $(line2).offset().top - $(line1).offset().top;
41137
40971
  };
41138
40972
 
41139
- var START_REF_PREFIX = "start-ref-";
41140
- var END_REF_PREFIX = "end-ref-";
41141
- var REF_STYLE = {
41142
- display: "inline-block",
41143
- width: 0,
41144
- visibility: "hidden"
41145
- };
41146
- var LABEL_OUTER_STYLE = {
41147
- // for some reason we need these to keep the nbsp from wrapping when the
41148
- // inner circle/square is display: inline-block
41149
- display: "inline",
41150
- whiteSpace: "nowrap"
41151
- };
41152
- var SQUARE_LABEL_STYLE = {
41153
- display: "inline-block",
41154
- color: "rgb(255, 255, 255)",
41155
- backgroundColor: "rgb(90, 90, 90)",
41156
- paddingLeft: 10,
41157
- paddingRight: 10,
41158
- userSelect: "none",
41159
- WebkitUserSelect: "none"
41160
- };
41161
- var CIRCLE_LABEL_STYLE = {
41162
- display: "inline-block",
41163
- color: "rgb(255, 255, 255)",
41164
- backgroundColor: "rgb(90, 90, 90)",
41165
- userSelect: "none",
41166
- WebkitUserSelect: "none",
41167
- width: 22,
41168
- height: 22,
41169
- borderRadius: "50%",
41170
- textAlign: "center"
41171
- };
40973
+ function getInitialParseState() {
40974
+ return {
40975
+ currentRef: [],
40976
+ useRefs: true,
40977
+ lastRef: 0,
40978
+ firstSentenceRef: null,
40979
+ firstQuestionRef: null,
40980
+ lastFootnote: {
40981
+ id: 0,
40982
+ text: ""
40983
+ }
40984
+ };
40985
+ }
41172
40986
 
41173
40987
  class RefStart extends React.Component {
41174
40988
  constructor() {
@@ -41187,10 +41001,6 @@ class RefStart extends React.Component {
41187
41001
 
41188
41002
  }
41189
41003
 
41190
- _defineProperty(RefStart, "propTypes", {
41191
- refContent: PropTypes.node.isRequired
41192
- });
41193
-
41194
41004
  class RefEnd extends React.Component {
41195
41005
  render() {
41196
41006
  return /*#__PURE__*/React.createElement("span", {
@@ -41201,20 +41011,11 @@ class RefEnd extends React.Component {
41201
41011
  }
41202
41012
 
41203
41013
  var rules = {
41204
- // $FlowFixMe[prop-missing]
41205
- // $FlowFixMe[incompatible-use]
41206
41014
  newline: SimpleMarkdown.defaultRules.newline,
41207
- // $FlowFixMe[prop-missing]
41208
- // $FlowFixMe[incompatible-use]
41209
41015
  paragraph: SimpleMarkdown.defaultRules.paragraph,
41210
- // $FlowFixMe[prop-missing]
41211
- // $FlowFixMe[incompatible-use]
41212
41016
  escape: SimpleMarkdown.defaultRules.escape,
41213
41017
  passageFootnote: {
41214
- // $FlowFixMe[prop-missing]
41215
- // $FlowFixMe[incompatible-use]
41216
41018
  order: SimpleMarkdown.defaultRules.escape.order + 0.1,
41217
- // $FlowFixMe[prop-missing]
41218
41019
  match: SimpleMarkdown.inlineRegex(/^\^/),
41219
41020
  parse: (capture, _parse, state) => {
41220
41021
  // if no footnotes have been seen, we're id 1. otherwise,
@@ -41243,10 +41044,8 @@ var rules = {
41243
41044
  }
41244
41045
  },
41245
41046
  refStart: {
41246
- // $FlowFixMe[prop-missing]
41247
- // $FlowFixMe[incompatible-use]
41248
41047
  order: SimpleMarkdown.defaultRules.escape.order + 0.2,
41249
- match: function match(source, state) {
41048
+ match: function match(source) {
41250
41049
  var capture = /^\{\{/.exec(source);
41251
41050
 
41252
41051
  if (capture) {
@@ -41277,12 +41076,14 @@ var rules = {
41277
41076
  }
41278
41077
 
41279
41078
  closeIndex++;
41280
- }
41281
-
41282
- var refText = source.slice(2, closeIndex); // A "magic" capture that matches the opening {{
41079
+ } // A "magic" capture that matches the opening {{
41283
41080
  // but captures the full ref text internally :D
41081
+ // unfortunately the "magic" makes Flow upset,
41082
+ // so there's this questionable workaround
41284
41083
 
41285
- return [capture[0], refText];
41084
+
41085
+ capture[1] = source.slice(2, closeIndex);
41086
+ return capture;
41286
41087
  }
41287
41088
 
41288
41089
  return null;
@@ -41306,7 +41107,7 @@ var rules = {
41306
41107
  // to make those refs into react refs on the
41307
41108
  // passage, for instance!
41308
41109
  useRefs: false
41309
- }, INITIAL_PARSE_STATE));
41110
+ }, getInitialParseState()));
41310
41111
 
41311
41112
  return {
41312
41113
  ref: ref,
@@ -41314,26 +41115,27 @@ var rules = {
41314
41115
  };
41315
41116
  },
41316
41117
  react: (node, output, state) => {
41317
- if (node.ref == null) {
41118
+ var ref = node.ref;
41119
+
41120
+ if (ref == null) {
41318
41121
  return null;
41319
41122
  } // We don't pass state here because this is parsed
41320
41123
  // and output out-of-band. We don't want to affect
41321
41124
  // our state by the double-output here :).
41322
41125
 
41323
41126
 
41324
- var refContent = output(node.refContent, {});
41127
+ var refContent = output(node.refContent, {}); // note(matthewc) the refs created here become the refs
41128
+ // pulled from `this.refs` in passage.jsx
41129
+
41325
41130
  return /*#__PURE__*/React.createElement(RefStart, {
41326
- ref: START_REF_PREFIX + node.ref,
41327
- key: START_REF_PREFIX + node.ref,
41131
+ ref: START_REF_PREFIX + ref,
41132
+ key: START_REF_PREFIX + ref,
41328
41133
  refContent: refContent
41329
41134
  });
41330
41135
  }
41331
41136
  },
41332
41137
  refEnd: {
41333
- // $FlowFixMe[prop-missing]
41334
- // $FlowFixMe[incompatible-use]
41335
41138
  order: SimpleMarkdown.defaultRules.escape.order + 0.3,
41336
- // $FlowFixMe[prop-missing]
41337
41139
  match: SimpleMarkdown.inlineRegex(/^\}\}/),
41338
41140
  parse: (capture, _parse3, state) => {
41339
41141
  if (!state.useRefs) {
@@ -41349,6 +41151,8 @@ var rules = {
41349
41151
  },
41350
41152
  react: (node, output, state) => {
41351
41153
  if (node.ref != null) {
41154
+ // note(matthewc) the refs created here become the refs
41155
+ // pulled from `this.refs` in passage.jsx
41352
41156
  return /*#__PURE__*/React.createElement(RefEnd, {
41353
41157
  ref: END_REF_PREFIX + node.ref,
41354
41158
  key: END_REF_PREFIX + node.ref
@@ -41362,10 +41166,7 @@ var rules = {
41362
41166
  }
41363
41167
  },
41364
41168
  squareLabel: {
41365
- // $FlowFixMe[prop-missing]
41366
- // $FlowFixMe[incompatible-use]
41367
41169
  order: SimpleMarkdown.defaultRules.escape.order + 0.4,
41368
- // $FlowFixMe[prop-missing]
41369
41170
  match: SimpleMarkdown.inlineRegex(/^\[\[(\w+)\]\]( *)/),
41370
41171
  parse: (capture, _parse4, state) => {
41371
41172
  if (!state.firstQuestionRef) {
@@ -41394,10 +41195,7 @@ var rules = {
41394
41195
  }
41395
41196
  },
41396
41197
  circleLabel: {
41397
- // $FlowFixMe[prop-missing]
41398
- // $FlowFixMe[incompatible-use]
41399
41198
  order: SimpleMarkdown.defaultRules.escape.order + 0.5,
41400
- // $FlowFixMe[prop-missing]
41401
41199
  match: SimpleMarkdown.inlineRegex(/^\(\((\w+)\)\)( *)/),
41402
41200
  parse: (capture, _parse5, state) => {
41403
41201
  return {
@@ -41422,10 +41220,7 @@ var rules = {
41422
41220
  }
41423
41221
  },
41424
41222
  squareBracketRef: {
41425
- // $FlowFixMe[prop-missing]
41426
- // $FlowFixMe[incompatible-use]
41427
41223
  order: SimpleMarkdown.defaultRules.escape.order + 0.6,
41428
- // $FlowFixMe[prop-missing]
41429
41224
  match: SimpleMarkdown.inlineRegex(/^\[(\d+)\]( *)/),
41430
41225
  parse: (capture, _parse6, state) => {
41431
41226
  if (!state.firstSentenceRef) {
@@ -41451,10 +41246,7 @@ var rules = {
41451
41246
  }
41452
41247
  },
41453
41248
  highlight: {
41454
- // $FlowFixMe[prop-missing]
41455
- // $FlowFixMe[incompatible-use]
41456
41249
  order: SimpleMarkdown.defaultRules.escape.order + 0.7,
41457
- // $FlowFixMe[prop-missing]
41458
41250
  match: SimpleMarkdown.inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
41459
41251
  parse: (capture, _parse7, state) => {
41460
41252
  return {
@@ -41469,10 +41261,7 @@ var rules = {
41469
41261
  }
41470
41262
  },
41471
41263
  reviewHighlight: {
41472
- // $FlowFixMe[prop-missing]
41473
- // $FlowFixMe[incompatible-use]
41474
41264
  order: SimpleMarkdown.defaultRules.escape.order + 0.7,
41475
- // $FlowFixMe[prop-missing]
41476
41265
  match: SimpleMarkdown.inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
41477
41266
  parse: (capture, _parse8, state) => {
41478
41267
  return {
@@ -41486,17 +41275,9 @@ var rules = {
41486
41275
  }, node.content)];
41487
41276
  }
41488
41277
  },
41489
- // $FlowFixMe[prop-missing]
41490
- // $FlowFixMe[incompatible-use]
41491
41278
  strong: SimpleMarkdown.defaultRules.strong,
41492
- // $FlowFixMe[prop-missing]
41493
- // $FlowFixMe[incompatible-use]
41494
41279
  u: SimpleMarkdown.defaultRules.u,
41495
- // $FlowFixMe[prop-missing]
41496
- // $FlowFixMe[incompatible-use]
41497
41280
  em: SimpleMarkdown.defaultRules.em,
41498
- // $FlowFixMe[prop-missing]
41499
- // $FlowFixMe[incompatible-use]
41500
41281
  del: SimpleMarkdown.defaultRules.del,
41501
41282
  text: _objectSpread2(_objectSpread2({}, SimpleMarkdown.defaultRules.text), {}, {
41502
41283
  react(node, output, state) {
@@ -41507,32 +41288,54 @@ var rules = {
41507
41288
 
41508
41289
  })
41509
41290
  };
41510
- var INITIAL_PARSE_STATE = {
41511
- currentRef: [],
41512
- useRefs: true,
41513
- lastRef: 0,
41514
- lastFootnote: {
41515
- id: 0,
41516
- text: ""
41517
- }
41518
- }; // $FlowFixMe[prop-missing]
41519
-
41291
+ var START_REF_PREFIX = "start-ref-";
41292
+ var END_REF_PREFIX = "end-ref-";
41293
+ var REF_STYLE = {
41294
+ display: "inline-block",
41295
+ width: 0,
41296
+ visibility: "hidden"
41297
+ };
41298
+ var LABEL_OUTER_STYLE = {
41299
+ // for some reason we need these to keep the nbsp from wrapping when the
41300
+ // inner circle/square is display: inline-block
41301
+ display: "inline",
41302
+ whiteSpace: "nowrap"
41303
+ };
41304
+ var SQUARE_LABEL_STYLE = {
41305
+ display: "inline-block",
41306
+ color: "rgb(255, 255, 255)",
41307
+ backgroundColor: "rgb(90, 90, 90)",
41308
+ paddingLeft: 10,
41309
+ paddingRight: 10,
41310
+ userSelect: "none",
41311
+ WebkitUserSelect: "none"
41312
+ };
41313
+ var CIRCLE_LABEL_STYLE = {
41314
+ display: "inline-block",
41315
+ color: "rgb(255, 255, 255)",
41316
+ backgroundColor: "rgb(90, 90, 90)",
41317
+ userSelect: "none",
41318
+ WebkitUserSelect: "none",
41319
+ width: 22,
41320
+ height: 22,
41321
+ borderRadius: "50%",
41322
+ textAlign: "center"
41323
+ };
41520
41324
  var builtParser = SimpleMarkdown.parserFor(rules);
41521
41325
 
41522
41326
  var parse = (source, state) => {
41523
41327
  state = state || {};
41524
41328
  var paragraphedSource = source + "\n\n";
41525
- return builtParser(paragraphedSource, _.extend(state, INITIAL_PARSE_STATE));
41329
+ return builtParser(paragraphedSource, _.extend(state, getInitialParseState()));
41526
41330
  };
41527
41331
 
41332
+ var output = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, "react"));
41528
41333
  var PassageMarkdown = {
41529
- parse: parse,
41530
- // $FlowFixMe[prop-missing]
41531
- output: SimpleMarkdown.reactFor( // $FlowFixMe[prop-missing]
41532
- SimpleMarkdown.ruleOutput(rules, "react")),
41533
- START_REF_PREFIX: START_REF_PREFIX,
41534
- END_REF_PREFIX: END_REF_PREFIX,
41535
- _rulesForTesting: rules
41334
+ parse,
41335
+ output,
41336
+ START_REF_PREFIX,
41337
+ END_REF_PREFIX,
41338
+ getInitialParseState
41536
41339
  };
41537
41340
 
41538
41341
  // A fake paragraph to measure the line height of the passage. In CSS we always
@@ -41545,9 +41348,9 @@ class LineHeightMeasurer extends React.Component {
41545
41348
 
41546
41349
  _defineProperty(this, "_cachedLineHeight", void 0);
41547
41350
 
41548
- _defineProperty(this, "$body", void 0);
41351
+ _defineProperty(this, "body", void 0);
41549
41352
 
41550
- _defineProperty(this, "$end", void 0);
41353
+ _defineProperty(this, "end", void 0);
41551
41354
  }
41552
41355
 
41553
41356
  measureLineHeight() {
@@ -41559,23 +41362,30 @@ class LineHeightMeasurer extends React.Component {
41559
41362
  }
41560
41363
 
41561
41364
  forceMeasureLineHeight() {
41562
- // Add some text which magically fills an entire line.
41563
- this.$body.text(" \u0080"); // Now, the line height is the difference between the top of the
41365
+ var body = this.body;
41366
+ var end = this.end;
41367
+
41368
+ if (!body || !end) {
41369
+ return;
41370
+ } // Add some text which magically fills an entire line.
41371
+
41372
+
41373
+ body.textContent = " \u0080"; // Now, the line height is the difference between the top of the
41564
41374
  // second line and the top of the first line.
41565
41375
 
41566
- this._cachedLineHeight = getLineHeightForNode(this.$body, this.$end); // Clear out the first line so it doesn't overlap the passage.
41376
+ this._cachedLineHeight = getLineHeightForNode(body, end); // Clear out the first line so it doesn't overlap the passage.
41567
41377
 
41568
- this.$body.text("");
41378
+ body.textContent = "";
41569
41379
  }
41570
41380
 
41571
41381
  render() {
41572
41382
  return /*#__PURE__*/React.createElement("div", {
41573
41383
  className: css(styles$8.measurer)
41574
41384
  }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
41575
- ref: e => this.$body = $(e),
41385
+ ref: _ref => this.body = _ref,
41576
41386
  className: "paragraph"
41577
41387
  }), /*#__PURE__*/React.createElement("div", {
41578
- ref: e => this.$end = $(e)
41388
+ ref: _ref2 => this.end = _ref2
41579
41389
  })));
41580
41390
  }
41581
41391
 
@@ -41594,9 +41404,11 @@ class Passage extends React.Component {
41594
41404
  constructor() {
41595
41405
  super(...arguments);
41596
41406
 
41597
- _defineProperty(this, "_onResize", void 0);
41407
+ _defineProperty(this, "_contentRef", void 0);
41598
41408
 
41599
- _defineProperty(this, "_lineHeightMeasurer", void 0);
41409
+ _defineProperty(this, "_lineHeightMeasurerRef", void 0);
41410
+
41411
+ _defineProperty(this, "_onResize", void 0);
41600
41412
 
41601
41413
  _defineProperty(this, "_stylesAppiedTimer", void 0);
41602
41414
 
@@ -41617,6 +41429,8 @@ class Passage extends React.Component {
41617
41429
  this._updateState();
41618
41430
 
41619
41431
  this._onResize = _.throttle(() => {
41432
+ var _this$_lineHeightMeas;
41433
+
41620
41434
  // If we're rendering JIPT text, we won't have line numbers or a
41621
41435
  // line height measurer, so skip handling this resize.
41622
41436
  if (this.shouldRenderJipt()) {
@@ -41627,7 +41441,7 @@ class Passage extends React.Component {
41627
41441
  // listen for resize events.
41628
41442
 
41629
41443
 
41630
- this._lineHeightMeasurer.forceMeasureLineHeight();
41444
+ (_this$_lineHeightMeas = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas === void 0 ? void 0 : _this$_lineHeightMeas.forceMeasureLineHeight();
41631
41445
 
41632
41446
  this._updateState();
41633
41447
  }, 500);
@@ -41690,9 +41504,8 @@ class Passage extends React.Component {
41690
41504
  }
41691
41505
 
41692
41506
  _measureLines() {
41693
- // eslint-disable-next-line react/no-string-refs
41694
- var $renderer = $(ReactDOM__default.findDOMNode(this.refs.content));
41695
- var contentsHeight = $renderer.height();
41507
+ var renderer = ReactDOM__default.findDOMNode(this._contentRef);
41508
+ var contentsHeight = $(renderer).height();
41696
41509
 
41697
41510
  var lineHeight = this._getLineHeight();
41698
41511
 
@@ -41719,7 +41532,9 @@ class Passage extends React.Component {
41719
41532
  }
41720
41533
 
41721
41534
  _getLineHeight() {
41722
- return this._lineHeightMeasurer.measureLineHeight();
41535
+ var _this$_lineHeightMeas2;
41536
+
41537
+ return ((_this$_lineHeightMeas2 = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas2 === void 0 ? void 0 : _this$_lineHeightMeas2.measureLineHeight()) || 0;
41723
41538
  }
41724
41539
 
41725
41540
  getLineCount() {
@@ -41735,12 +41550,16 @@ class Passage extends React.Component {
41735
41550
  * These are functions to support the passage refs inter-widgets feature
41736
41551
  * where other widgets can fetch the line numbers of a reference inside of
41737
41552
  * a passage.
41553
+ *
41554
+ * todo(matthewc): The refs are created by PassageMarkdown's refStart and refEnd,
41555
+ * somehow bubbling up to Passage's `this.refs`. This runs against
41556
+ * current best practices for refs by using string refs, but also
41557
+ * by breaking our expectation of explicit data flow.
41738
41558
  */
41739
41559
 
41740
41560
 
41741
41561
  _getStartRefLineNumber(referenceNumber) {
41742
- var refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41743
-
41562
+ var refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41744
41563
  var ref = this.refs[refRef];
41745
41564
 
41746
41565
  if (!ref) {
@@ -41763,8 +41582,7 @@ class Passage extends React.Component {
41763
41582
  }
41764
41583
 
41765
41584
  _getEndRefLineNumber(referenceNumber) {
41766
- var refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41767
-
41585
+ var refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber;
41768
41586
  var ref = this.refs[refRef];
41769
41587
 
41770
41588
  if (!ref) {
@@ -41801,9 +41619,8 @@ class Passage extends React.Component {
41801
41619
  }
41802
41620
 
41803
41621
  _convertPosToLineNumber(absoluteVPos) {
41804
- // eslint-disable-next-line react/no-string-refs
41805
- var $content = $(ReactDOM__default.findDOMNode(this.refs.content));
41806
- var relativeVPos = absoluteVPos - $content.offset().top;
41622
+ var content = ReactDOM__default.findDOMNode(this._contentRef);
41623
+ var relativeVPos = absoluteVPos - $(content).offset().top;
41807
41624
 
41808
41625
  var lineHeight = this._getLineHeight();
41809
41626
 
@@ -41812,8 +41629,7 @@ class Passage extends React.Component {
41812
41629
  }
41813
41630
 
41814
41631
  _getRefContent(referenceNumber) {
41815
- var refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41816
-
41632
+ var refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41817
41633
  var ref = this.refs[refRef];
41818
41634
 
41819
41635
  if (!ref) {
@@ -41919,12 +41735,9 @@ class Passage extends React.Component {
41919
41735
  onSerializedHighlightsUpdate: this._handleSerializedHighlightsUpdate,
41920
41736
  serializedHighlights: this.props.highlights
41921
41737
  }, /*#__PURE__*/React.createElement("div", {
41922
- ref: "content"
41923
- }, /*#__PURE__*/React.createElement(LineHeightMeasurer // TODO(mdr): We found a new Flow error when upgrading:
41924
- // "e (null) This type is incompatible with this._lineHeightMeasurer (LineHeightMeasurer)"
41925
- // $FlowFixMe[incompatible-type](0.52.0->0.53.0)
41926
- , {
41927
- ref: e => this._lineHeightMeasurer = e
41738
+ ref: _ref4 => this._contentRef = _ref4
41739
+ }, /*#__PURE__*/React.createElement(LineHeightMeasurer, {
41740
+ ref: _ref3 => this._lineHeightMeasurerRef = _ref3
41928
41741
  }), PassageMarkdown.output(parsed)));
41929
41742
  }
41930
41743
 
@@ -41967,10 +41780,7 @@ class Passage extends React.Component {
41967
41780
  });
41968
41781
  }
41969
41782
 
41970
- var parseState = {
41971
- firstSentenceRef: null,
41972
- firstQuestionRef: null
41973
- }; // Replace the vertical double quote characters quoting text with
41783
+ var parseState = PassageMarkdown.getInitialParseState(); // Replace the vertical double quote characters quoting text with
41974
41784
  // an unicode left and right double quote characters. This would
41975
41785
  // avoid rendering right or left unicode double quotes on both
41976
41786
  // ends of a sentence wrapped by double quotes.