@khanacademy/perseus 0.4.2 → 1.0.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 (50) hide show
  1. package/.eslintrc.js +11 -0
  2. package/CHANGELOG.md +47 -0
  3. package/dist/es/index.js +1078 -1299
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.js +1077 -1304
  6. package/dist/index.js.map +1 -1
  7. package/dist/strings.js +0 -4
  8. package/package.json +9 -8
  9. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +110 -110
  10. package/src/perseus-api.jsx +10 -36
  11. package/src/perseus-types.js +0 -138
  12. package/src/styles/constants.js +0 -2
  13. package/src/styles/variables.less +0 -2
  14. package/src/types.js +18 -35
  15. package/src/util.js +0 -6
  16. package/src/version.js +1 -1
  17. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  18. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +110 -110
  19. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +1136 -1160
  20. package/src/widgets/__tests__/radio/base-radio_test.jsx +243 -41
  21. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  22. package/src/widgets/__tests__/radio/choice_test.jsx +201 -0
  23. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  24. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  25. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  26. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  27. package/src/widgets/passage/passage-markdown.jsx +176 -120
  28. package/src/widgets/passage.jsx +48 -46
  29. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  30. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  31. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  32. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  33. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  34. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  35. package/src/widgets/radio/base-radio.jsx +483 -556
  36. package/src/widgets/radio/choice-icon/choice-icon.jsx +155 -0
  37. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  38. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +219 -0
  39. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +129 -0
  40. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  41. package/src/widgets/radio/choice-none-above.jsx +49 -40
  42. package/src/widgets/radio/choice.jsx +282 -353
  43. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  44. package/src/widgets/radio/focus-ring.jsx +29 -22
  45. package/src/widgets/radio/option-status.jsx +58 -36
  46. package/src/widgets/radio/widget.jsx +181 -136
  47. package/src/widgets/radio.jsx +16 -17
  48. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  49. package/src/widgets/radio/choice-icon.jsx +0 -469
  50. package/src/widgets/radio/cross-out-button.jsx +0 -134
package/dist/index.js CHANGED
@@ -73,7 +73,7 @@ var Link__default = /*#__PURE__*/_interopDefaultLegacy(Link);
73
73
 
74
74
  var version = {
75
75
  apiVersion: {
76
- major: 11,
76
+ major: 12,
77
77
  minor: 0
78
78
  },
79
79
  itemDataVersion: {
@@ -438,38 +438,9 @@ const ApiOptions = {
438
438
  // values. For now, though, you must either specify all fields
439
439
  // of `styling`, or omit the `styling` option entirely.
440
440
  styling: PropTypes__default["default"].shape({
441
- // Which version of radio widget styles to use in non-SAT
442
- // contexts.
443
- //
444
- // "legacy" was the version of the widget display after XOM but
445
- // before we started adding MCR styles. It doesn't have support
446
- // for rationales. It has since been removed.
447
- //
448
- // "intermediate" is a design which adds several new additions
449
- // to the "legacy" styles such as:
450
- // 1. Using the XOM "desktop" styles (with a visible check icon
451
- // and lines in between choices) on mobile devices.
452
- // 2. Designs for rationales
453
- // 3. Using the single-select styles for multi-select styles
454
- //
455
- // "final" is a design which will build off of the
456
- // "intermediate" designs and adds some improved designs as well
457
- // as:
458
- // 1. a/b/c/d/etc. letters inside of the prompt check box
459
- // 2. New iconography and styles to indicate choice correctness
460
- //
461
- // The "legacy" and "intermediate" designs will be A/B tested
462
- // against each other to ensure that its changes don't cause
463
- // problems due to the new designs. Once the "intermediate"
464
- // designs are finished, they will be A/B tested against the
465
- // "final" designs.
466
- //
467
- // If no flag is provided, "legacy" styles will be shown.
468
- //
469
- // TODO(emily): Remove this by Aug 1, 2017, at which point all
470
- // callsites should have been switched to using the "final"
471
- // designs.
472
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"])
441
+ // TODO(Nicole): Remove this after landing wonderblock-ifying
442
+ // the radio widget
443
+ primaryProductColor: PropTypes__default["default"].string
473
444
  }),
474
445
  // The color used for the hint progress indicator (eg. 1 / 3)
475
446
  hintProgressColor: PropTypes__default["default"].string,
@@ -520,9 +491,7 @@ const ApiOptions = {
520
491
  },
521
492
  setDrawingAreaAvailable: function () {},
522
493
  useDraftEditor: false,
523
- styling: {
524
- radioStyleVersion: "final"
525
- },
494
+ styling: {},
526
495
  canScrollPage: false,
527
496
  inModal: false,
528
497
  crossOutEnabled: false,
@@ -1643,16 +1612,10 @@ function inputPathsEqual(a, b) {
1643
1612
  return a.length === b.length && a.every((item, index) => {
1644
1613
  return b[index] === item;
1645
1614
  });
1646
- } // Keep `rWidgetRule` in sync with `WIDGET_RE`
1647
- // in content_internal/perseus_traversal.py
1648
- // sync-start:widget-id-regex 1271666319 services/content-editing/perseus/translation/content_traversal.go
1649
- // sync-start:widget-id-regex 680240484 services/static/javascript/perseus-merged-editor-package/editor.jsx
1650
- // sync-start:widget-id-regex 1400368396 content_internal/perseus_traversal.py
1651
-
1615
+ }
1652
1616
 
1653
1617
  const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
1654
- const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/; // sync-end:widget-id-regex
1655
-
1618
+ const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
1656
1619
  const rWidgetParts = new RegExp(rWidgetRule.source + "$");
1657
1620
  const snowman = "\u2603";
1658
1621
  const noScore = {
@@ -3085,11 +3048,10 @@ const warningColor = "#f86700";
3085
3048
  const warningColorHover = "#df5c00";
3086
3049
  const warningColorActive = "#c75300";
3087
3050
  const publishBlockingErrorColor = "#be2612";
3088
- const radioBorderColor$2 = "#BABEC2";
3089
- const checkedColor$1 = "#71B307"; // sync-start:article-max-width 986452807 services/static/javascript/perseus-all-package/styles/variables.less
3090
-
3051
+ const radioBorderColor$1 = "#BABEC2";
3052
+ const checkedColor$1 = "#71B307";
3091
3053
  const articleMaxWidthInPx = 688;
3092
- const articleMaxWidthTableInPx = 512; // sync-end:article-max-width
3054
+ const articleMaxWidthTableInPx = 512;
3093
3055
 
3094
3056
  var constants$1 = /*#__PURE__*/Object.freeze({
3095
3057
  __proto__: null,
@@ -3154,7 +3116,7 @@ var constants$1 = /*#__PURE__*/Object.freeze({
3154
3116
  warningColorHover: warningColorHover,
3155
3117
  warningColorActive: warningColorActive,
3156
3118
  publishBlockingErrorColor: publishBlockingErrorColor,
3157
- radioBorderColor: radioBorderColor$2,
3119
+ radioBorderColor: radioBorderColor$1,
3158
3120
  checkedColor: checkedColor$1,
3159
3121
  articleMaxWidthInPx: articleMaxWidthInPx,
3160
3122
  articleMaxWidthTableInPx: articleMaxWidthTableInPx
@@ -17888,17 +17850,17 @@ class Lint extends React__namespace.Component {
17888
17850
  let warningTextStyle;
17889
17851
 
17890
17852
  if (this.props.severity === 1) {
17891
- severityStyle = styles$w.indicatorError;
17853
+ severityStyle = styles$A.indicatorError;
17892
17854
  warningText = "Error";
17893
- warningTextStyle = styles$w.publishBlockingError;
17855
+ warningTextStyle = styles$A.publishBlockingError;
17894
17856
  } else if (this.props.severity === 2) {
17895
- severityStyle = styles$w.indicatorWarning;
17857
+ severityStyle = styles$A.indicatorWarning;
17896
17858
  warningText = "Warning";
17897
- warningTextStyle = styles$w.warning;
17859
+ warningTextStyle = styles$A.warning;
17898
17860
  } else {
17899
- severityStyle = styles$w.indicatorGuideline;
17861
+ severityStyle = styles$A.indicatorGuideline;
17900
17862
  warningText = "Recommendation";
17901
- warningTextStyle = styles$w.warning;
17863
+ warningTextStyle = styles$A.warning;
17902
17864
  }
17903
17865
 
17904
17866
  return /*#__PURE__*/React__namespace.createElement("a", {
@@ -17906,16 +17868,16 @@ class Lint extends React__namespace.Component {
17906
17868
  target: "lint-help-window",
17907
17869
  className: aphrodite.css(style)
17908
17870
  }, /*#__PURE__*/React__namespace.createElement("span", {
17909
- className: aphrodite.css(styles$w.indicator, severityStyle)
17871
+ className: aphrodite.css(styles$A.indicator, severityStyle)
17910
17872
  }, this.props.severity === 1 && /*#__PURE__*/React__namespace.createElement(InlineIcon, exclamationIcon)), /*#__PURE__*/React__namespace.createElement("div", {
17911
- className: aphrodite.css(styles$w.tooltip, tooltipAbove && styles$w.tooltipAbove)
17873
+ className: aphrodite.css(styles$A.tooltip, tooltipAbove && styles$A.tooltipAbove)
17912
17874
  }, this.props.message.split("\n\n").map((m, i) => /*#__PURE__*/React__namespace.createElement("p", {
17913
17875
  key: i,
17914
- className: aphrodite.css(styles$w.tooltipParagraph)
17876
+ className: aphrodite.css(styles$A.tooltipParagraph)
17915
17877
  }, /*#__PURE__*/React__namespace.createElement("span", {
17916
17878
  className: aphrodite.css(warningTextStyle)
17917
17879
  }, warningText, ":", " "), m)), /*#__PURE__*/React__namespace.createElement("div", {
17918
- className: aphrodite.css(styles$w.tail, tooltipAbove && styles$w.tailAbove)
17880
+ className: aphrodite.css(styles$A.tail, tooltipAbove && styles$A.tailAbove)
17919
17881
  })));
17920
17882
  });
17921
17883
  }
@@ -17963,19 +17925,19 @@ class Lint extends React__namespace.Component {
17963
17925
 
17964
17926
  if (blockHighlight) {
17965
17927
  return /*#__PURE__*/React__namespace.createElement("span", {
17966
- className: aphrodite.css(styles$w.lintContainer, styles$w.lintContainerBlock)
17967
- }, this.renderLink(styles$w.radioWidgetHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17928
+ className: aphrodite.css(styles$A.lintContainer, styles$A.lintContainerBlock)
17929
+ }, this.renderLink(styles$A.radioWidgetHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17968
17930
  }
17969
17931
 
17970
17932
  if (inline) {
17971
17933
  return /*#__PURE__*/React__namespace.createElement("span", {
17972
- className: aphrodite.css(styles$w.lintContainer)
17973
- }, this.renderLink(styles$w.inlineHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17934
+ className: aphrodite.css(styles$A.lintContainer)
17935
+ }, this.renderLink(styles$A.inlineHoverTarget), /*#__PURE__*/React__namespace.createElement("span", null, children));
17974
17936
  }
17975
17937
 
17976
17938
  return /*#__PURE__*/React__namespace.createElement("div", {
17977
- className: aphrodite.css(styles$w.lintContainer)
17978
- }, this.renderLink(styles$w.hoverTarget), /*#__PURE__*/React__namespace.createElement("div", null, children));
17939
+ className: aphrodite.css(styles$A.lintContainer)
17940
+ }, this.renderLink(styles$A.hoverTarget), /*#__PURE__*/React__namespace.createElement("div", null, children));
17979
17941
  }
17980
17942
 
17981
17943
  } // $FlowFixMe[prop-missing]
@@ -18000,7 +17962,7 @@ _defineProperty(Lint, "propTypes", {
18000
17962
  severity: PropTypes__default["default"].number
18001
17963
  });
18002
17964
 
18003
- const styles$w = aphrodite.StyleSheet.create({
17965
+ const styles$A = aphrodite.StyleSheet.create({
18004
17966
  // This is the class of the outermost element.
18005
17967
  // We use relative positioning so that the lint indicator can be
18006
17968
  // positioned absolutely relative to the position of the linty container.
@@ -19893,7 +19855,7 @@ class Renderer extends React__namespace.Component {
19893
19855
  paddingRight: margin
19894
19856
  };
19895
19857
  const mobileInnerStyle = { ...innerStyle,
19896
- ...styles$v.mobileZoomableParentFix
19858
+ ...styles$z.mobileZoomableParentFix
19897
19859
  };
19898
19860
  return /*#__PURE__*/React__namespace.createElement("div", {
19899
19861
  key: state.key,
@@ -20033,7 +19995,7 @@ class Renderer extends React__namespace.Component {
20033
19995
  paddingRight: 0
20034
19996
  };
20035
19997
  const mobileInnerStyle = { ...innerStyle,
20036
- ...styles$v.mobileZoomableParentFix
19998
+ ...styles$z.mobileZoomableParentFix
20037
19999
  };
20038
20000
  const wrappedOutput = /*#__PURE__*/React__namespace.createElement("div", {
20039
20001
  style: { ...mobileInnerStyle,
@@ -20714,7 +20676,7 @@ _defineProperty(Renderer, "defaultProps", {
20714
20676
  linterContext: PerseusLinter__namespace.linterContextDefault
20715
20677
  });
20716
20678
 
20717
- const styles$v = {
20679
+ const styles$z = {
20718
20680
  mobileZoomableParentFix: {
20719
20681
  // NOTE(abdul): There is an issue where transform animations will
20720
20682
  // cause the Zoomable component to disappear when running on the
@@ -24016,7 +23978,7 @@ var mediaQueries = {
24016
23978
  const {
24017
23979
  zIndexAboveScratchpad,
24018
23980
  zIndexInteractiveComponent,
24019
- radioBorderColor: radioBorderColor$1,
23981
+ radioBorderColor,
24020
23982
  checkedColor,
24021
23983
  circleSize,
24022
23984
  radioMarginWidth
@@ -24071,7 +24033,7 @@ var sharedStyles = aphrodite.StyleSheet.create({
24071
24033
  },
24072
24034
  backgroundColor: "#fff",
24073
24035
  border: "2px solid #fff",
24074
- boxShadow: "0 0px 0px 1px ".concat(radioBorderColor$1),
24036
+ boxShadow: "0 0px 0px 1px ".concat(radioBorderColor),
24075
24037
  outline: "none",
24076
24038
  boxSizing: "border-box",
24077
24039
  flexShrink: 0,
@@ -24323,36 +24285,6 @@ _defineProperty(Icon, "defaultProps", {
24323
24285
  color: "currentColor"
24324
24286
  });
24325
24287
 
24326
- class FocusRing extends React__namespace.Component {
24327
- render() {
24328
- const borderColor = this.props.visible ? this.props.color : "transparent";
24329
- const style = {
24330
- borderColor: borderColor
24331
- };
24332
- return /*#__PURE__*/React__namespace.createElement("span", {
24333
- className: aphrodite.css(styles$u.ring),
24334
- style: style
24335
- }, this.props.children);
24336
- }
24337
-
24338
- }
24339
-
24340
- _defineProperty(FocusRing, "defaultProps", {
24341
- visible: true,
24342
- color: kaGreen$1
24343
- });
24344
-
24345
- const styles$u = aphrodite.StyleSheet.create({
24346
- ring: {
24347
- margin: "auto",
24348
- display: "inline-block",
24349
- borderRadius: "50%",
24350
- borderWidth: 2,
24351
- padding: 2,
24352
- borderStyle: "solid"
24353
- }
24354
- });
24355
-
24356
24288
  /**
24357
24289
  * Given a choice's position in the radio widget, return the corresponding
24358
24290
  * "choice letter". (For example, in English, `getChoiceLetter(0)` is "A",
@@ -24383,179 +24315,57 @@ function getChoiceLetter(pos) {
24383
24315
  return " ";
24384
24316
  }
24385
24317
 
24386
- const SAT_ICON_SIZE = 25;
24387
-
24388
- class SATChoiceIcon extends React__namespace.Component {
24389
- // TODO(amy): figure out a better scheme for specifying these
24390
- // styles that isn't such a pain to grok. See some neat ideas
24391
- // from MDR in https://phabricator.khanacademy.org/D35249.
24392
- constructStyles(reviewMode, correct, checked) {
24393
- let backgroundColor;
24394
- let borderColor = satBlue;
24395
- let color = satBlue;
24396
-
24397
- if (reviewMode) {
24398
- if (correct) {
24399
- borderColor = satCorrectColor;
24400
- color = checked ? white$1 : satCorrectColor;
24401
- backgroundColor = checked ? satCorrectColor : white$1;
24402
- } else if (checked) {
24403
- borderColor = satIncorrectColor;
24404
- color = white$1;
24405
- backgroundColor = satIncorrectColor;
24406
- }
24407
- } else if (checked) {
24408
- color = white$1;
24409
- backgroundColor = satBlue;
24410
- }
24411
-
24412
- return {
24413
- color,
24414
- backgroundColor,
24415
- borderColor
24416
- };
24417
- }
24418
-
24419
- render() {
24420
- const {
24421
- letter,
24422
- a11yText,
24423
- reviewMode,
24424
- checked,
24425
- correct,
24426
- crossedOut
24427
- } = this.props;
24428
- const {
24429
- color,
24430
- backgroundColor,
24431
- borderColor
24432
- } = this.constructStyles(reviewMode, correct, checked);
24433
- return /*#__PURE__*/React__namespace.createElement("div", {
24434
- className: aphrodite.css(styles$t.iconWrapper)
24435
- }, /*#__PURE__*/React__namespace.createElement("div", {
24436
- className: aphrodite.css(styles$t.satCircle),
24437
- style: {
24438
- backgroundColor,
24439
- borderColor
24440
- }
24441
- }), /*#__PURE__*/React__namespace.createElement("div", {
24442
- style: {
24443
- color
24444
- },
24445
- className: aphrodite.css(styles$t.letter)
24446
- }, /*#__PURE__*/React__namespace.createElement("span", {
24447
- className: "perseus-sr-only"
24448
- }, a11yText), /*#__PURE__*/React__namespace.createElement("span", {
24449
- "aria-hidden": "true"
24450
- }, letter)), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24451
- color: borderColor,
24452
- sat: true
24453
- }));
24454
- }
24318
+ /**
24319
+ * Renders a circular selection ring around the child.
24320
+ */
24455
24321
 
24322
+ function FocusRing(props) {
24323
+ const {
24324
+ visible,
24325
+ color,
24326
+ children,
24327
+ multipleSelect
24328
+ } = props;
24329
+ const borderColor = visible ? color : "transparent";
24330
+ const borderRadius = multipleSelect ? 5 : "50%";
24331
+ const style = {
24332
+ borderColor,
24333
+ borderRadius
24334
+ };
24335
+ return /*#__PURE__*/React__namespace.createElement("span", {
24336
+ "data-test-id": "focus-ring",
24337
+ className: aphrodite.css(styles$y.ring),
24338
+ style: style
24339
+ }, children);
24456
24340
  }
24457
24341
 
24458
- const LIBRARY_ICON_SIZE = 24;
24459
-
24460
- class LibraryChoiceIcon extends React__namespace.Component {
24461
- getChoiceInner() {
24462
- const {
24463
- letter,
24464
- showCorrectness,
24465
- correct
24466
- } = this.props;
24467
-
24468
- if (!showCorrectness) {
24469
- return letter;
24470
- }
24471
-
24472
- if (correct) {
24473
- return /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconCheck, {
24474
- style: {
24475
- position: "relative",
24476
- top: -1
24477
- }
24478
- }));
24479
- }
24480
-
24481
- return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
24482
- } // Handle dynamic styling of the multiple choice icon. Most
24483
- // MC icon styles are constant, but we do allow the caller
24484
- // to specify the selected color, and thus must control styles
24485
- // related to the selected state dynamically.
24486
-
24487
-
24488
- getDynamicStyles() {
24489
- const {
24490
- checked,
24491
- showCorrectness,
24492
- pressed,
24493
- primaryProductColor,
24494
- correct,
24495
- transparentBackground
24496
- } = this.props;
24497
-
24498
- if (!showCorrectness && pressed) {
24499
- return {
24500
- borderColor: primaryProductColor,
24501
- color: primaryProductColor,
24502
- backgroundColor: transparentBackground ? "transparent" : white$1
24503
- };
24504
- }
24505
-
24506
- if (checked) {
24507
- // Note: kaGreen is not only the default product color,
24508
- // but also the "correctness" color
24509
- const bg = showCorrectness && correct ? Color__default["default"].green : primaryProductColor;
24510
- return {
24511
- color: white$1,
24512
- backgroundColor: bg,
24513
- borderColor: bg
24514
- };
24515
- }
24516
-
24517
- return {
24518
- borderColor: Color__default["default"].offBlack64,
24519
- color: Color__default["default"].offBlack64
24520
- };
24342
+ FocusRing.defaultProps = {
24343
+ visible: true,
24344
+ color: kaGreen$1,
24345
+ multipleSelect: false
24346
+ };
24347
+ const styles$y = aphrodite.StyleSheet.create({
24348
+ ring: {
24349
+ margin: "auto",
24350
+ display: "inline-block",
24351
+ borderWidth: 2,
24352
+ padding: 2,
24353
+ borderStyle: "solid"
24521
24354
  }
24355
+ });
24522
24356
 
24523
- render() {
24524
- const {
24525
- a11yText,
24526
- checked,
24527
- crossedOut,
24528
- showCorrectness,
24529
- correct,
24530
- focused,
24531
- primaryProductColor,
24532
- previouslyAnswered
24533
- } = this.props;
24534
- const dynamicStyles = this.getDynamicStyles();
24535
- return /*#__PURE__*/React__namespace.createElement("div", {
24536
- className: aphrodite.css(styles$t.iconWrapper)
24537
- }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
24538
- color: primaryProductColor,
24539
- visible: focused
24540
- }, /*#__PURE__*/React__namespace.createElement("div", {
24541
- style: dynamicStyles,
24542
- className: aphrodite.css(styles$t.libraryCircle, showCorrectness && correct && styles$t.libraryCircleCorrect, showCorrectness && !correct && styles$t.libraryCircleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$t.libraryCircleIncorrectAnswered) // used in BaseRadio to check if we actually clicked on the
24543
- // radio icon
24544
- ,
24545
- "data-is-radio-icon": true
24546
- }, /*#__PURE__*/React__namespace.createElement("div", {
24547
- className: "perseus-sr-only"
24548
- }, a11yText), /*#__PURE__*/React__namespace.createElement("div", {
24549
- "aria-hidden": true
24550
- }, this.getChoiceInner()))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24551
- color: dynamicStyles.borderColor
24552
- }));
24357
+ const SAT_ICON_SIZE = 25;
24358
+ const LIBRARY_ICON_SIZE = 24;
24359
+ const styles$x = aphrodite.StyleSheet.create({
24360
+ iconWrapper: {
24361
+ display: "inline-block",
24362
+ position: "relative"
24553
24363
  }
24364
+ });
24554
24365
 
24555
- } // The "cross-out line" SVG is a bit bigger than the library icon, to provide
24366
+ /* Component for rendering a letter icon in radio choice */
24556
24367
  // extra space for the cute rounded line cap.
24557
24368
 
24558
-
24559
24369
  const CROSS_OUT_LINE_PADDING = 2;
24560
24370
  const CROSS_OUT_LINE_SIZE = LIBRARY_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
24561
24371
  const SAT_CROSS_OUT_LINE_SIZE = SAT_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
@@ -24570,7 +24380,7 @@ function CrossOutLine(props) {
24570
24380
  width: crossOutLineSize,
24571
24381
  height: crossOutLineSize,
24572
24382
  viewBox: "0 0 ".concat(crossOutLineSize, " ").concat(crossOutLineSize),
24573
- className: aphrodite.css(styles$t.crossOutLine)
24383
+ className: aphrodite.css(styles$w.crossOutLine)
24574
24384
  }, /*#__PURE__*/React__namespace.createElement("line", {
24575
24385
  // The line stretches from the bottom-left to top-right.
24576
24386
  // We don't quite go to the _very_ corner, because the cute
@@ -24585,151 +24395,129 @@ function CrossOutLine(props) {
24585
24395
  }));
24586
24396
  }
24587
24397
 
24588
- class ChoiceIcon extends React__namespace.Component {
24589
- a11yText(letter) {
24590
- const {
24591
- checked,
24592
- correct,
24593
- crossedOut,
24594
- showCorrectness
24595
- } = this.props; // There are two pieces of metadata we want to add to each a11yText:
24596
- // whether the answer was checked/crossed-out/neither, and whether the
24597
- // answer is correct/incorrect/not-yet-revealed.
24598
- //
24599
- // Translation is tricky for cross-product situations like this, so
24600
- // we've just enumerated all 9 possibilities as separate strings.
24601
-
24602
- if (showCorrectness && correct) {
24603
- if (checked) {
24604
- return i18n__namespace._("(Choice %(letter)s, Checked, Correct)", {
24605
- letter
24606
- });
24607
- }
24608
-
24609
- if (crossedOut) {
24610
- return i18n__namespace._("(Choice %(letter)s, Crossed out, Correct)", {
24611
- letter
24612
- });
24613
- }
24398
+ const styles$w = aphrodite.StyleSheet.create({
24399
+ crossOutLine: {
24400
+ // Center the icon within the container.
24401
+ position: "absolute",
24402
+ top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24403
+ left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24404
+ }
24405
+ });
24614
24406
 
24615
- return i18n__namespace._("(Choice %(letter)s, Correct)", {
24616
- letter
24617
- });
24618
- }
24407
+ function ChoiceInner(props) {
24408
+ const {
24409
+ letter,
24410
+ showCorrectness,
24411
+ correct
24412
+ } = props;
24619
24413
 
24620
- if (showCorrectness && !correct) {
24621
- if (checked) {
24622
- return i18n__namespace._("(Choice %(letter)s, Checked, Incorrect)", {
24623
- letter
24624
- });
24625
- }
24414
+ if (!showCorrectness) {
24415
+ return letter;
24416
+ }
24626
24417
 
24627
- if (crossedOut) {
24628
- return i18n__namespace._("(Choice %(letter)s, Crossed out, Incorrect)", {
24629
- letter
24630
- });
24418
+ if (correct) {
24419
+ return /*#__PURE__*/React__namespace.createElement(InlineIcon, _extends({}, iconCheck, {
24420
+ style: {
24421
+ position: "relative",
24422
+ top: -1
24631
24423
  }
24632
-
24633
- return i18n__namespace._("(Choice %(letter)s, Incorrect)", {
24634
- letter
24635
- });
24636
- }
24637
-
24638
- if (checked) {
24639
- return i18n__namespace._("(Choice %(letter)s, Checked)", {
24640
- letter
24641
- });
24642
- }
24643
-
24644
- if (crossedOut) {
24645
- return i18n__namespace._("(Choice %(letter)s, Crossed out)", {
24646
- letter
24647
- });
24648
- }
24649
-
24650
- return i18n__namespace._("(Choice %(letter)s)", {
24651
- letter
24652
- });
24424
+ }));
24653
24425
  }
24654
24426
 
24655
- render() {
24656
- const {
24657
- pos,
24658
- reviewMode,
24659
- checked,
24660
- crossedOut,
24661
- correct,
24662
- product,
24663
- showCorrectness,
24664
- pressed,
24665
- focused,
24666
- primaryProductColor,
24667
- previouslyAnswered,
24668
- transparentBackground
24669
- } = this.props;
24670
- const letter = getChoiceLetter(pos);
24427
+ return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
24428
+ } // Handle dynamic styling of the multiple choice icon. Most
24429
+ // MC icon styles are constant, but we do allow the caller
24430
+ // to specify the selected color, and thus must control styles
24431
+ // related to the selected state dynamically.
24432
+
24433
+
24434
+ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground) {
24435
+ let backgroundColor;
24436
+ let borderColor;
24437
+ let color;
24438
+
24439
+ if (!showCorrectness && pressed) {
24440
+ borderColor = primaryProductColor;
24441
+ color = primaryProductColor;
24442
+ backgroundColor = transparentBackground ? "transparent" : white$1;
24443
+ } else if (checked) {
24444
+ // Note: kaGreen is not only the default product color,
24445
+ // but also the "correctness" color
24446
+ const bg = showCorrectness && correct ? Color__default["default"].green : primaryProductColor;
24447
+ color = white$1;
24448
+ backgroundColor = bg;
24449
+ borderColor = bg;
24450
+ } else {
24451
+ borderColor = Color__default["default"].offBlack64;
24452
+ color = Color__default["default"].offBlack64;
24453
+ } // define shape
24671
24454
 
24672
- if (product === "sat") {
24673
- return /*#__PURE__*/React__namespace.createElement(SATChoiceIcon, {
24674
- letter: letter,
24675
- a11yText: this.a11yText(letter),
24676
- reviewMode: reviewMode,
24677
- checked: checked,
24678
- correct: correct,
24679
- crossedOut: crossedOut
24680
- });
24681
- }
24682
24455
 
24683
- return /*#__PURE__*/React__namespace.createElement(LibraryChoiceIcon, {
24684
- letter: letter,
24685
- a11yText: this.a11yText(letter),
24686
- reviewMode: reviewMode,
24687
- checked: checked,
24688
- crossedOut: crossedOut,
24689
- pressed: pressed,
24690
- focused: focused,
24691
- correct: correct,
24692
- showCorrectness: showCorrectness,
24693
- primaryProductColor: primaryProductColor,
24694
- previouslyAnswered: previouslyAnswered,
24695
- transparentBackground: transparentBackground
24696
- });
24456
+ let borderRadius;
24457
+
24458
+ if (multipleSelect) {
24459
+ borderRadius = 3;
24460
+ } else {
24461
+ borderRadius = LIBRARY_ICON_SIZE;
24697
24462
  }
24698
24463
 
24464
+ return {
24465
+ backgroundColor,
24466
+ borderColor,
24467
+ color,
24468
+ borderRadius
24469
+ };
24699
24470
  }
24700
24471
 
24701
- _defineProperty(ChoiceIcon, "defaultProps", {
24702
- primaryProductColor: Color__default["default"].blue
24703
- });
24472
+ function LibraryChoiceIcon(props) {
24473
+ const {
24474
+ a11yText,
24475
+ checked,
24476
+ crossedOut,
24477
+ showCorrectness,
24478
+ correct,
24479
+ focused,
24480
+ hovered,
24481
+ multipleSelect,
24482
+ primaryProductColor,
24483
+ previouslyAnswered,
24484
+ letter,
24485
+ pressed,
24486
+ transparentBackground
24487
+ } = props;
24488
+ const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, primaryProductColor, correct, transparentBackground);
24489
+ return /*#__PURE__*/React__namespace.createElement("div", {
24490
+ className: aphrodite.css(styles$x.iconWrapper)
24491
+ }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
24492
+ color: primaryProductColor,
24493
+ visible: focused || hovered,
24494
+ multipleSelect: multipleSelect
24495
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24496
+ style: dynamicStyles,
24497
+ "data-test-id": "choice-icon__library-choice-icon",
24498
+ className: aphrodite.css(styles$v.circle, showCorrectness && correct && styles$v.circleCorrect, showCorrectness && !correct && styles$v.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$v.circleIncorrectAnswered) // used in BaseRadio to check if we actually clicked on the
24499
+ // radio icon
24500
+ ,
24501
+ "data-is-radio-icon": true
24502
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24503
+ className: "perseus-sr-only"
24504
+ }, a11yText), /*#__PURE__*/React__namespace.createElement("div", {
24505
+ "aria-hidden": true
24506
+ }, /*#__PURE__*/React__namespace.createElement(ChoiceInner, {
24507
+ letter: letter,
24508
+ showCorrectness: showCorrectness,
24509
+ correct: correct
24510
+ })))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24511
+ color: dynamicStyles.borderColor
24512
+ }));
24513
+ }
24704
24514
 
24705
- const styles$t = aphrodite.StyleSheet.create({
24706
- satCircle: {
24707
- display: "block",
24708
- borderRadius: SAT_ICON_SIZE,
24709
- borderStyle: "solid",
24710
- borderWidth: 2,
24711
- content: "''",
24712
- height: SAT_ICON_SIZE,
24713
- width: SAT_ICON_SIZE,
24714
- top: 1,
24715
- left: 1
24716
- },
24717
- iconWrapper: {
24718
- display: "inline-block",
24719
- position: "relative"
24720
- },
24721
- crossOutLine: {
24722
- // Center the icon within the container.
24723
- position: "absolute",
24724
- top: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)"),
24725
- left: "calc(50% - ".concat(CROSS_OUT_LINE_SIZE / 2, "px)")
24726
- },
24727
- libraryCircle: {
24515
+ const styles$v = aphrodite.StyleSheet.create({
24516
+ circle: {
24728
24517
  // Make the circle
24729
24518
  width: LIBRARY_ICON_SIZE,
24730
24519
  height: LIBRARY_ICON_SIZE,
24731
24520
  boxSizing: "border-box",
24732
- borderRadius: LIBRARY_ICON_SIZE,
24733
24521
  borderStyle: "solid",
24734
24522
  borderWidth: 2,
24735
24523
  // The default icons have letters in them. Style those letters.
@@ -24750,18 +24538,113 @@ const styles$t = aphrodite.StyleSheet.create({
24750
24538
  // pixel so it is much better centered.
24751
24539
  lineHeight: "1px"
24752
24540
  },
24753
- libraryCircleCorrect: {
24541
+ circleCorrect: {
24754
24542
  fontSize: LIBRARY_ICON_SIZE
24755
24543
  },
24756
- libraryCircleIncorrect: {
24544
+ circleIncorrect: {
24757
24545
  fontSize: LIBRARY_ICON_SIZE,
24758
24546
  borderColor: gray68,
24759
24547
  color: gray68
24760
24548
  },
24761
- libraryCircleIncorrectAnswered: {
24549
+ circleIncorrectAnswered: {
24762
24550
  backgroundColor: Color__default["default"].red,
24763
24551
  borderColor: Color__default["default"].red,
24764
24552
  color: Color__default["default"].white
24553
+ }
24554
+ });
24555
+
24556
+ /* Component for rendering a letter icon in a SAT radio choice */
24557
+
24558
+ // TODO(amy): figure out a better scheme for specifying these
24559
+ // styles that isn't such a pain to grok. See some neat ideas
24560
+ // from MDR in https://phabricator.khanacademy.org/D35249.
24561
+ function constructSATStyles(reviewMode, correct, checked, multipleSelect) {
24562
+ let backgroundColor;
24563
+ let borderColor = satBlue;
24564
+ let color = satBlue;
24565
+
24566
+ if (reviewMode) {
24567
+ if (correct) {
24568
+ borderColor = satCorrectColor;
24569
+ color = checked ? white$1 : satCorrectColor;
24570
+ backgroundColor = checked ? satCorrectColor : white$1;
24571
+ } else if (checked) {
24572
+ borderColor = satIncorrectColor;
24573
+ color = white$1;
24574
+ backgroundColor = satIncorrectColor;
24575
+ }
24576
+ } else if (checked) {
24577
+ color = white$1;
24578
+ backgroundColor = satBlue;
24579
+ }
24580
+
24581
+ let borderRadius;
24582
+
24583
+ if (multipleSelect) {
24584
+ borderRadius = 3;
24585
+ } else {
24586
+ borderRadius = SAT_ICON_SIZE;
24587
+ }
24588
+
24589
+ return {
24590
+ color,
24591
+ backgroundColor,
24592
+ borderColor,
24593
+ borderRadius
24594
+ };
24595
+ }
24596
+
24597
+ function SATChoiceIcon(props) {
24598
+ const {
24599
+ letter,
24600
+ a11yText,
24601
+ crossedOut,
24602
+ reviewMode,
24603
+ correct,
24604
+ checked,
24605
+ multipleSelect
24606
+ } = props;
24607
+ const {
24608
+ color,
24609
+ backgroundColor,
24610
+ borderColor,
24611
+ borderRadius
24612
+ } = constructSATStyles(reviewMode, correct, checked, multipleSelect);
24613
+ return /*#__PURE__*/React__namespace.createElement("div", {
24614
+ className: aphrodite.css(styles$x.iconWrapper)
24615
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24616
+ className: aphrodite.css(styles$u.satCircle),
24617
+ "data-test-id": "choice-icon__sat-choice-icon",
24618
+ style: {
24619
+ backgroundColor,
24620
+ borderColor,
24621
+ borderRadius
24622
+ }
24623
+ }), /*#__PURE__*/React__namespace.createElement("div", {
24624
+ style: {
24625
+ color
24626
+ },
24627
+ className: aphrodite.css(styles$u.letter)
24628
+ }, /*#__PURE__*/React__namespace.createElement("span", {
24629
+ className: "perseus-sr-only"
24630
+ }, a11yText), /*#__PURE__*/React__namespace.createElement("span", {
24631
+ "aria-hidden": "true"
24632
+ }, letter)), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24633
+ color: borderColor,
24634
+ sat: true
24635
+ }));
24636
+ }
24637
+
24638
+ const styles$u = aphrodite.StyleSheet.create({
24639
+ satCircle: {
24640
+ display: "block",
24641
+ borderStyle: "solid",
24642
+ borderWidth: 2,
24643
+ content: "''",
24644
+ height: SAT_ICON_SIZE,
24645
+ width: SAT_ICON_SIZE,
24646
+ top: 1,
24647
+ left: 1
24765
24648
  },
24766
24649
  letter: {
24767
24650
  // These properties make sure that this element has the exact
@@ -24781,6 +24664,118 @@ const styles$t = aphrodite.StyleSheet.create({
24781
24664
  }
24782
24665
  });
24783
24666
 
24667
+ /* Component for rendering a letter icon in radio choice */
24668
+
24669
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
24670
+ // There are two pieces of metadata we want to add to each a11yText:
24671
+ // whether the answer was checked/crossed-out/neither, and whether the
24672
+ // answer is correct/incorrect/not-yet-revealed.
24673
+ //
24674
+ // Translation is tricky for cross-product situations like this, so
24675
+ // we've just enumerated all 9 possibilities as separate strings.
24676
+ if (showCorrectness && correct) {
24677
+ if (checked) {
24678
+ return i18n__namespace._("(Choice %(letter)s, Checked, Correct)", {
24679
+ letter
24680
+ });
24681
+ }
24682
+
24683
+ if (crossedOut) {
24684
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Correct)", {
24685
+ letter
24686
+ });
24687
+ }
24688
+
24689
+ return i18n__namespace._("(Choice %(letter)s, Correct)", {
24690
+ letter
24691
+ });
24692
+ }
24693
+
24694
+ if (showCorrectness && !correct) {
24695
+ if (checked) {
24696
+ return i18n__namespace._("(Choice %(letter)s, Checked, Incorrect)", {
24697
+ letter
24698
+ });
24699
+ }
24700
+
24701
+ if (crossedOut) {
24702
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Incorrect)", {
24703
+ letter
24704
+ });
24705
+ }
24706
+
24707
+ return i18n__namespace._("(Choice %(letter)s, Incorrect)", {
24708
+ letter
24709
+ });
24710
+ }
24711
+
24712
+ if (checked) {
24713
+ return i18n__namespace._("(Choice %(letter)s, Checked)", {
24714
+ letter
24715
+ });
24716
+ }
24717
+
24718
+ if (crossedOut) {
24719
+ return i18n__namespace._("(Choice %(letter)s, Crossed out)", {
24720
+ letter
24721
+ });
24722
+ }
24723
+
24724
+ return i18n__namespace._("(Choice %(letter)s)", {
24725
+ letter
24726
+ });
24727
+ }
24728
+
24729
+ function ChoiceIcon(props) {
24730
+ const {
24731
+ pos,
24732
+ reviewMode,
24733
+ checked,
24734
+ crossedOut,
24735
+ correct,
24736
+ product,
24737
+ multipleSelect,
24738
+ showCorrectness,
24739
+ focused,
24740
+ hovered,
24741
+ pressed,
24742
+ primaryProductColor = Color__default["default"].blue,
24743
+ previouslyAnswered,
24744
+ transparentBackground
24745
+ } = props;
24746
+ const letter = getChoiceLetter(pos);
24747
+ const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
24748
+
24749
+ if (product === "sat") {
24750
+ return /*#__PURE__*/React__namespace.createElement(SATChoiceIcon, {
24751
+ letter: letter,
24752
+ a11yText: a11yText,
24753
+ reviewMode: reviewMode,
24754
+ checked: checked,
24755
+ correct: correct,
24756
+ crossedOut: crossedOut,
24757
+ multipleSelect: multipleSelect
24758
+ });
24759
+ }
24760
+
24761
+ return /*#__PURE__*/React__namespace.createElement(LibraryChoiceIcon, {
24762
+ letter: letter,
24763
+ a11yText: a11yText,
24764
+ reviewMode: reviewMode,
24765
+ checked: checked,
24766
+ crossedOut: crossedOut,
24767
+ focused: focused,
24768
+ hovered: hovered,
24769
+ pressed: pressed,
24770
+ correct: correct,
24771
+ showCorrectness: showCorrectness,
24772
+ primaryProductColor: primaryProductColor,
24773
+ previouslyAnswered: previouslyAnswered,
24774
+ transparentBackground: transparentBackground,
24775
+ multipleSelect: multipleSelect
24776
+ });
24777
+ }
24778
+
24784
24779
  /**
24785
24780
  * Renders text indicating whether the choice was correct or
24786
24781
  * not and whether the choice was selected or not.
@@ -24789,63 +24784,68 @@ const styles$t = aphrodite.StyleSheet.create({
24789
24784
  * between the states more immediately clear to users.
24790
24785
  */
24791
24786
 
24792
- class OptionStatus extends React__namespace.Component {
24793
- _renderText() {
24794
- const {
24795
- checked,
24796
- correct,
24797
- crossedOut
24798
- } = this.props;
24787
+ function renderText(checked, correct, crossedOut) {
24788
+ if (correct) {
24789
+ // For correct answers, we surface checked _or_ crossedOut state,
24790
+ // because any interaction with the correct answer is noteworthy!
24791
+ if (checked) {
24792
+ return i18n__namespace._("Correct (selected)");
24793
+ }
24799
24794
 
24800
- if (correct) {
24801
- // For correct answers, we surface checked _or_ crossedOut state,
24802
- // because any interaction with the correct answer is noteworthy!
24803
- if (checked) {
24804
- return i18n__namespace._("Correct (selected)");
24805
- }
24795
+ if (crossedOut) {
24796
+ return i18n__namespace._("Correct (but you crossed it out)");
24797
+ }
24806
24798
 
24807
- if (crossedOut) {
24808
- return i18n__namespace._("Correct (but you crossed it out)");
24809
- }
24799
+ return i18n__namespace._("Correct");
24800
+ } // But, for incorrect answers, we only surface checked state,
24801
+ // because crossing out an incorrect answer is not noteworthy.
24810
24802
 
24811
- return i18n__namespace._("Correct");
24812
- } // But, for incorrect answers, we only surface checked state,
24813
- // because crossing out an incorrect answer is not noteworthy.
24814
24803
 
24804
+ if (checked) {
24805
+ return i18n__namespace._("Incorrect (selected)");
24806
+ }
24815
24807
 
24816
- if (checked) {
24817
- return i18n__namespace._("Incorrect (selected)");
24818
- }
24808
+ return i18n__namespace._("Incorrect");
24809
+ }
24819
24810
 
24820
- return i18n__namespace._("Incorrect");
24811
+ function OptionStatus(props) {
24812
+ const {
24813
+ checked,
24814
+ correct,
24815
+ crossedOut,
24816
+ previouslyAnswered,
24817
+ reviewMode,
24818
+ satStyling
24819
+ } = props; // Option status is exclued for SAT
24820
+
24821
+ if (satStyling) {
24822
+ return null;
24823
+ } // Option status is shown only in review mode, or for incorrectly
24824
+ // answered items.
24825
+
24826
+
24827
+ if (!reviewMode && !previouslyAnswered) {
24828
+ return null;
24821
24829
  }
24822
24830
 
24823
- render() {
24824
- const {
24825
- checked,
24826
- correct,
24827
- previouslyAnswered
24828
- } = this.props;
24829
- let textStyle;
24831
+ let textStyle;
24830
24832
 
24831
- if (correct) {
24832
- textStyle = styles$s.correct;
24833
+ if (correct) {
24834
+ textStyle = styles$t.correct;
24835
+ } else {
24836
+ if (checked || previouslyAnswered) {
24837
+ textStyle = styles$t.incorrectAnswered;
24833
24838
  } else {
24834
- if (checked || previouslyAnswered) {
24835
- textStyle = styles$s.incorrectAnswered;
24836
- } else {
24837
- textStyle = styles$s.incorrect;
24838
- }
24839
+ textStyle = styles$t.incorrect;
24839
24840
  }
24840
-
24841
- return /*#__PURE__*/React__namespace.createElement("div", {
24842
- className: aphrodite.css(styles$s.text, textStyle)
24843
- }, this._renderText());
24844
24841
  }
24845
24842
 
24843
+ return /*#__PURE__*/React__namespace.createElement("div", {
24844
+ className: aphrodite.css(styles$t.text, textStyle)
24845
+ }, renderText(checked, correct, crossedOut));
24846
24846
  }
24847
24847
 
24848
- const styles$s = aphrodite.StyleSheet.create({
24848
+ const styles$t = aphrodite.StyleSheet.create({
24849
24849
  text: {
24850
24850
  alignItems: "center",
24851
24851
  display: "flex",
@@ -24872,316 +24872,206 @@ const ellipsisHorizontalIcon = {
24872
24872
  height: 27.284
24873
24873
  };
24874
24874
 
24875
- class Choice extends React__namespace.Component {
24876
- constructor() {
24877
- super(...arguments);
24878
-
24879
- _defineProperty(this, "justFinishedTouch", void 0);
24880
-
24881
- _defineProperty(this, "justFinishedTimeoutID", void 0);
24882
-
24883
- _defineProperty(this, "_input", void 0);
24884
-
24885
- _defineProperty(this, "state", {
24886
- isInputFocused: false,
24887
- isInputActive: false
24888
- });
24889
-
24890
- _defineProperty(this, "inputRef", ref => {
24891
- this._input = ref;
24892
- });
24893
-
24894
- _defineProperty(this, "renderOptionStatus", () => {
24895
- const {
24896
- correct,
24897
- checked,
24898
- crossedOut,
24899
- reviewMode,
24900
- previouslyAnswered
24901
- } = this.props; // Option status is exclued for SAT
24902
-
24903
- if (this.props.apiOptions.satStyling) {
24904
- return null;
24905
- } // Option status is shown only in review mode, or for incorrectly
24906
- // answered items.
24907
-
24908
-
24909
- if (!reviewMode && !previouslyAnswered) {
24910
- return null;
24911
- }
24912
-
24913
- return /*#__PURE__*/React__namespace.createElement(OptionStatus, {
24914
- checked: checked,
24915
- correct: correct,
24916
- crossedOut: crossedOut,
24917
- previouslyAnswered: previouslyAnswered
24918
- });
24919
- });
24920
-
24921
- _defineProperty(this, "renderChoiceIcon", _ref => {
24922
- let {
24923
- isFocused,
24924
- isPressed
24925
- } = _ref;
24926
- const {
24927
- radioStyleVersion,
24928
- primaryProductColor
24929
- } = this.props.apiOptions.styling;
24930
- const finalStyles = typeof radioStyleVersion === "undefined" ? false : radioStyleVersion === "final";
24931
-
24932
- if (!finalStyles && !this.props.apiOptions.satStyling) {
24933
- return null;
24934
- }
24935
-
24936
- return /*#__PURE__*/React__namespace.createElement(ChoiceIcon, {
24937
- pos: this.props.pos,
24938
- correct: this.props.correct,
24939
- crossedOut: this.props.crossedOut,
24940
- pressed: isPressed,
24941
- focused: isFocused,
24942
- checked: this.props.checked,
24943
- showCorrectness: this.props.showCorrectness,
24944
- reviewMode: this.props.reviewMode,
24945
- product: this.props.apiOptions.satStyling ? "sat" : "library",
24946
- primaryProductColor: primaryProductColor,
24947
- previouslyAnswered: this.props.previouslyAnswered
24948
- });
24949
- });
24875
+ function Choice(props) {
24876
+ const {
24877
+ disabled,
24878
+ checked,
24879
+ content,
24880
+ crossedOut,
24881
+ showCorrectness,
24882
+ multipleSelect,
24883
+ onChange,
24884
+ reviewMode,
24885
+ correct,
24886
+ apiOptions,
24887
+ previouslyAnswered,
24888
+ pos,
24889
+ showRationale,
24890
+ rationale,
24891
+ forwardedRef
24892
+ } = props;
24893
+ const [isInputFocused, setIsInputFocused] = React.useState(false);
24894
+ React.useEffect(() => {
24895
+ if (isInputFocused && disabled) {
24896
+ setIsInputFocused(false);
24897
+ }
24898
+ }, [disabled, isInputFocused, setIsInputFocused]); // Call `this.props.onChange` with the given values. Any keys that are not
24899
+ // specified will be filled in with the current value. (For example, if
24900
+ // `checked` is specified but `crossedOut` is not, then `crossedOut` will
24901
+ // be filled in with `this.props.crossedOut`.)
24902
+ //
24903
+ // This enables us to use shorthand inside this component, while
24904
+ // maintaining a consistent API for the parent.
24950
24905
 
24951
- _defineProperty(this, "_sendChange", newValues => {
24952
- var _newValues$checked, _newValues$crossedOut;
24906
+ function sendChange(newValues) {
24907
+ var _newValues$checked, _newValues$crossedOut;
24953
24908
 
24954
- const checked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : this.props.checked;
24955
- const crossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : this.props.crossedOut;
24956
- this.props.onChange({
24957
- checked,
24958
- crossedOut
24959
- });
24909
+ const updatedChecked = (_newValues$checked = newValues.checked) !== null && _newValues$checked !== void 0 ? _newValues$checked : checked;
24910
+ const updatedCrossedOut = (_newValues$crossedOut = newValues.crossedOut) !== null && _newValues$crossedOut !== void 0 ? _newValues$crossedOut : crossedOut;
24911
+ onChange({
24912
+ checked: updatedChecked,
24913
+ crossedOut: updatedCrossedOut
24960
24914
  });
24915
+ } // HACK: while most of the styling for rendering SAT items is handled
24916
+ // via aphrodite, we also need to assign normal CSS classnames here to
24917
+ // special-case the coloring of MathJax formulas (see .MathJax .math in
24918
+ // stylesheets/task-package/tasks.less)
24961
24919
 
24962
- _defineProperty(this, "focusInput", () => {
24963
- var _this$_input;
24964
24920
 
24965
- (_this$_input = this._input) === null || _this$_input === void 0 ? void 0 : _this$_input.focus();
24966
- });
24967
- }
24921
+ const satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
24922
+ const satIncorrectChecked = apiOptions.satStyling && reviewMode && !correct && checked;
24923
+ const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!apiOptions.satStyling && styles$s.description, apiOptions.satStyling && styles$s.satDescription, satCorrectChoice && styles$s.satDescriptionCorrect, satCorrectChoice && checked && styles$s.satDescriptionCorrectChecked, satIncorrectChecked && styles$s.satDescriptionIncorrectChecked));
24924
+ const rationaleClassName = classNames__default["default"]("perseus-radio-rationale-content", aphrodite.css(styles$s.rationale, !apiOptions.satStyling && styles$s.nonSatRationale, apiOptions.satStyling && styles$s.satReviewRationale)); // We want to show the choices as dimmed out when the choices are
24925
+ // disabled. However, we don't want to do this in the SAT product and
24926
+ // we also don't want to do this when we're in review mode in the
24927
+ // content library.
24968
24928
 
24969
- UNSAFE_componentWillUpdate(nextProps) {
24970
- if (this.state.isInputFocused && nextProps.disabled) {
24971
- this.setState({
24972
- isInputFocused: false
24973
- });
24929
+ const showDimmed = !apiOptions.satStyling && !reviewMode && apiOptions.readOnly || crossedOut;
24930
+ return /*#__PURE__*/React__namespace.createElement("div", {
24931
+ style: {
24932
+ dispay: "flex",
24933
+ flexDirection: "column"
24974
24934
  }
24975
- }
24935
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24936
+ style: {
24937
+ display: "flex",
24938
+ flexDirection: "row",
24939
+ width: "100%",
24940
+ opacity: showDimmed ? 0.5 : 1.0
24941
+ }
24942
+ }, /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
24943
+ onClick: () => {
24944
+ // If we're checking a crossed-out option, let's
24945
+ // also uncross it.
24946
+ sendChange({
24947
+ checked: !checked,
24948
+ crossedOut: false
24949
+ });
24950
+ },
24951
+ className: descriptionClassName,
24952
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
24953
+ "aria-label": "Select Choice ".concat(getChoiceLetter(pos)),
24954
+ "aria-checked": checked ? "true" : "false",
24955
+ role: "checkbox",
24956
+ style: {
24957
+ flex: 1
24958
+ },
24959
+ ref: forwardedRef
24960
+ }, _ref => {
24961
+ var _apiOptions$styling;
24976
24962
 
24977
- render() {
24978
- const sat = this.props.apiOptions.satStyling;
24979
- const {
24980
- reviewMode,
24981
- correct,
24982
- checked
24983
- } = this.props; // HACK: while most of the styling for rendering SAT items is handled
24984
- // via aphrodite, we also need to assign normal CSS classnames here to
24985
- // special-case the coloring of MathJax formulas (see .MathJax .math in
24986
- // stylesheets/task-package/tasks.less)
24987
-
24988
- const satCorrectChoice = sat && reviewMode && correct;
24989
- const satIncorrectChecked = sat && reviewMode && !correct && checked;
24990
- const descriptionClassName = classNames__default["default"]("description", satCorrectChoice && "sat-correct", satIncorrectChecked && "sat-incorrect", aphrodite.css(!sat && styles$r.description, sat && styles$r.satDescription, satCorrectChoice && styles$r.satDescriptionCorrect, satCorrectChoice && checked && styles$r.satDescriptionCorrectChecked, satIncorrectChecked && styles$r.satDescriptionIncorrectChecked));
24991
- const rationaleClassName = classNames__default["default"]("perseus-radio-rationale-content", aphrodite.css(styles$r.rationale, !sat && styles$r.nonSatRationale, sat && styles$r.satReviewRationale)); // We want to show the choices as dimmed out when the choices are
24992
- // disabled. However, we don't want to do this in the SAT product and
24993
- // we also don't want to do this when we're in review mode in the
24994
- // content library.
24995
-
24996
- const showDimmed = !sat && !reviewMode && this.props.apiOptions.readOnly || this.props.crossedOut;
24963
+ let {
24964
+ hovered,
24965
+ focused,
24966
+ pressed
24967
+ } = _ref;
24997
24968
  return /*#__PURE__*/React__namespace.createElement("div", {
24998
- style: {
24999
- dispay: "flex",
25000
- flexDirection: "column"
25001
- }
25002
- }, /*#__PURE__*/React__namespace.createElement("div", {
25003
24969
  style: {
25004
24970
  display: "flex",
25005
24971
  flexDirection: "row",
25006
- width: "100%",
25007
- opacity: showDimmed ? 0.5 : 1.0
24972
+ justifyContent: "center",
24973
+ alignContent: "center",
24974
+ paddingTop: Spacing__default["default"].xSmall_8,
24975
+ paddingBottom: Spacing__default["default"].xSmall_8
25008
24976
  }
25009
- }, /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25010
- onClick: () => {
25011
- // If we're checking a crossed-out option, let's
25012
- // also uncross it.
25013
- this._sendChange({
25014
- checked: true,
25015
- crossedOut: false
25016
- });
25017
- },
25018
- className: descriptionClassName,
25019
- disabled: this.props.disabled || this.props.apiOptions.staticRender || this.props.apiOptions.readOnly,
25020
- "aria-label": "Select Choice ".concat(getChoiceLetter(this.props.pos)),
25021
- "aria-checked": this.props.checked,
25022
- role: "checkbox",
24977
+ }, /*#__PURE__*/React__namespace.createElement(ChoiceIcon, {
24978
+ pos: pos,
24979
+ correct: correct,
24980
+ crossedOut: crossedOut,
24981
+ pressed: pressed,
24982
+ focused: focused,
24983
+ checked: checked,
24984
+ hovered: hovered,
24985
+ showCorrectness: showCorrectness,
24986
+ multipleSelect: multipleSelect,
24987
+ reviewMode: reviewMode,
24988
+ product: apiOptions.satStyling ? "sat" : "library",
24989
+ primaryProductColor: (_apiOptions$styling = apiOptions.styling) === null || _apiOptions$styling === void 0 ? void 0 : _apiOptions$styling.primaryProductColor,
24990
+ previouslyAnswered: previouslyAnswered
24991
+ }), /*#__PURE__*/React__namespace.createElement("span", {
25023
24992
  style: {
25024
- flex: 1
24993
+ paddingLeft: Spacing__default["default"].small_12,
24994
+ textAlign: "left",
24995
+ flex: 1,
24996
+ paddingTop: 4
25025
24997
  }
25026
- }, _ref2 => {
24998
+ }, /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement(OptionStatus, {
24999
+ checked: checked,
25000
+ correct: correct,
25001
+ crossedOut: crossedOut,
25002
+ previouslyAnswered: previouslyAnswered,
25003
+ reviewMode: reviewMode,
25004
+ satStyling: apiOptions.satStyling
25005
+ })), /*#__PURE__*/React__namespace.createElement("div", null, content)));
25006
+ }), apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.Popover, {
25007
+ dismissEnabled: true,
25008
+ content: _ref2 => {
25027
25009
  let {
25028
- hovered,
25029
- focused,
25030
- pressed
25010
+ close
25031
25011
  } = _ref2;
25032
- return /*#__PURE__*/React__namespace.createElement("div", {
25033
- style: {
25034
- display: "flex",
25035
- flexDirection: "row",
25036
- justifyContent: "center",
25037
- alignContent: "center",
25038
- paddingTop: Spacing__default["default"].xSmall_8,
25039
- paddingBottom: Spacing__default["default"].xSmall_8
25040
- }
25041
- }, /*#__PURE__*/React__namespace.createElement("span", null, this.renderChoiceIcon({
25042
- isFocused: focused,
25043
- isPressed: pressed
25044
- })), /*#__PURE__*/React__namespace.createElement("span", {
25045
- style: {
25046
- paddingLeft: Spacing__default["default"].small_12,
25047
- textAlign: "left",
25048
- flex: 1,
25049
- paddingTop: 4
25050
- }
25051
- }, /*#__PURE__*/React__namespace.createElement("div", null, this.renderOptionStatus()), /*#__PURE__*/React__namespace.createElement("div", null, this.props.content)));
25052
- }), this.props.apiOptions.crossOutEnabled && !reviewMode && /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.Popover, {
25053
- dismissEnabled: true,
25054
- content: _ref3 => {
25055
- let {
25056
- close
25057
- } = _ref3;
25058
- return /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.PopoverContent, {
25059
- title: "Cross out",
25060
- content: "Cross out option",
25061
- closeButtonVisible: true,
25062
- actions: /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
25063
- size: Spacing__default["default"].medium_16
25064
- }), /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
25065
- kind: "primary",
25066
- "aria-label": "Cross out Choice ".concat(getChoiceLetter(this.props.pos)),
25067
- disabled: this.props.apiOptions.readOnly || reviewMode,
25068
- onClick: () => {
25069
- if (!this.props.crossedOut) {
25070
- // If we're crossing
25071
- // out a checked
25072
- // option, let's also
25073
- // uncheck it.
25074
- this._sendChange({
25075
- checked: false,
25076
- crossedOut: true
25077
- });
25078
- } else {
25079
- this._sendChange({
25080
- crossedOut: false
25081
- });
25082
- }
25083
-
25084
- close();
25012
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksPopover.PopoverContent, {
25013
+ title: "Cross out",
25014
+ content: "Cross out option",
25015
+ closeButtonVisible: true,
25016
+ actions: /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, null, /*#__PURE__*/React__namespace.createElement(wonderBlocksLayout.Strut, {
25017
+ size: Spacing__default["default"].medium_16
25018
+ }), /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
25019
+ kind: "primary",
25020
+ "aria-label": "Cross out Choice ".concat(getChoiceLetter(pos)),
25021
+ disabled: apiOptions.readOnly || reviewMode,
25022
+ onClick: () => {
25023
+ if (!crossedOut) {
25024
+ // If we're crossing
25025
+ // out a checked
25026
+ // option, let's also
25027
+ // uncheck it.
25028
+ sendChange({
25029
+ checked: false,
25030
+ crossedOut: true
25031
+ });
25032
+ } else {
25033
+ sendChange({
25034
+ crossedOut: false
25035
+ });
25085
25036
  }
25086
- }, this.props.crossedOut ? "Bring back" : "Cross out"))
25087
- });
25088
- }
25037
+
25038
+ close();
25039
+ }
25040
+ }, crossedOut ? "Bring back" : "Cross out"))
25041
+ });
25042
+ }
25043
+ }, _ref3 => {
25044
+ let {
25045
+ open
25046
+ } = _ref3;
25047
+ return /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25048
+ onClick: open,
25049
+ "aria-label": "Open menu for Choice ".concat(getChoiceLetter(pos)),
25050
+ disabled: apiOptions.staticRender
25089
25051
  }, _ref4 => {
25090
- let {
25091
- open
25092
- } = _ref4;
25093
- return /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
25094
- onClick: open,
25095
- "aria-label": "Open menu for Choice ".concat(getChoiceLetter(this.props.pos)),
25096
- disabled: this.props.apiOptions.staticRender
25097
- }, _ref5 => {
25098
- return /*#__PURE__*/React__namespace.createElement(Icon, {
25099
- icon: ellipsisHorizontalIcon,
25100
- size: 3,
25101
- color: Color__default["default"].offBlack64
25102
- });
25052
+ return /*#__PURE__*/React__namespace.createElement(Icon, {
25053
+ icon: ellipsisHorizontalIcon,
25054
+ size: 3,
25055
+ color: Color__default["default"].offBlack64
25103
25056
  });
25104
- })), this.props.showRationale && /*#__PURE__*/React__namespace.createElement("div", {
25105
- className: rationaleClassName,
25106
- "data-test-id": "perseus-radio-rationale-content-".concat(this.props.pos)
25107
- }, this.props.rationale));
25108
- }
25109
-
25057
+ });
25058
+ })), showRationale && /*#__PURE__*/React__namespace.createElement("div", {
25059
+ className: rationaleClassName,
25060
+ "data-test-id": "perseus-radio-rationale-content-".concat(pos)
25061
+ }, rationale));
25110
25062
  }
25111
25063
 
25112
- _defineProperty(Choice, "propTypes", {
25113
- // TODO(kevinb) use Options.propTypes from perseus-api.jsx
25114
- // This change will also require make sure that item-renderer.jsx and
25115
- // server-item-renderer.jsx have appropriate defaults for apiOptions
25116
- // because many of the properties on Options.propTypes are required.
25117
- apiOptions: PropTypes__default["default"].shape({
25118
- satStyling: PropTypes__default["default"].bool,
25119
- isMobile: PropTypes__default["default"].bool,
25120
- styling: PropTypes__default["default"].shape({
25121
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"]),
25122
- primaryProductColor: PropTypes__default["default"].string
25123
- }),
25124
- readOnly: PropTypes__default["default"].bool
25125
- }),
25126
- checked: PropTypes__default["default"].bool,
25127
- className: PropTypes__default["default"].string,
25128
- rationale: PropTypes__default["default"].node,
25129
- content: PropTypes__default["default"].node,
25130
- correct: PropTypes__default["default"].bool,
25131
- deselectEnabled: PropTypes__default["default"].bool,
25132
- disabled: PropTypes__default["default"].bool,
25133
- editMode: PropTypes__default["default"].bool,
25134
- groupName: PropTypes__default["default"].string,
25135
- isLastChoice: PropTypes__default["default"].bool,
25136
- // Needed for border styling
25137
- // This indicates the position of the choice relative to others
25138
- // (so that we can display a nice little (A), (B), etc. next to it)
25139
- // Also used to generate an id for each input.
25140
- pos: PropTypes__default["default"].number,
25141
- reviewMode: PropTypes__default["default"].bool,
25142
- showRationale: PropTypes__default["default"].bool,
25143
- showCorrectness: PropTypes__default["default"].bool,
25144
- type: PropTypes__default["default"].string,
25145
- // Indicates whether the user has "crossed out" this choice, meaning
25146
- // that they don't think it's correct. This value does not affect
25147
- // scoring or other behavior; it's just a note for the user's
25148
- // reference.
25149
- crossedOut: PropTypes__default["default"].bool,
25150
- // Indicates that the user has previously selected this answer. These
25151
- // answers may be rendered orange in review, rather than grey if
25152
- // incorrect.
25153
- previouslyAnswered: PropTypes__default["default"].bool,
25154
- // A callback indicating that this choice has changed. Its argument is
25155
- // an object with two keys: `checked` and `crossedOut`. Each contains a
25156
- // boolean value specifying the new checked and crossed-out value of
25157
- // this choice.
25158
- onChange: PropTypes__default["default"].func,
25159
- // Callbacks for when the user presses the up arrow or down arrow
25160
- // (respectively) to navigate between choices. When called, the parent
25161
- // finds the prev/next choice, focuses the input, and checks the radio
25162
- // button if appropriate. (The reason we have custom behavior is that
25163
- // we actually _don't_ want to auto-select crossed-out choices!)
25164
- goToPrevChoice: PropTypes__default["default"].func.isRequired,
25165
- goToNextChoice: PropTypes__default["default"].func.isRequired
25166
- });
25167
-
25168
- _defineProperty(Choice, "defaultProps", {
25169
- apiOptions: {
25170
- styling: {}
25171
- },
25064
+ Choice.defaultProps = {
25065
+ disabled: false,
25172
25066
  checked: false,
25173
- classSet: {},
25067
+ onChange: newValues => {},
25174
25068
  correct: false,
25175
- disabled: false,
25176
- editMode: false,
25177
- onChange: function () {},
25178
- showRationale: false,
25179
- type: "radio",
25069
+ apiOptions: ApiOptions.defaults,
25070
+ previouslyAnswered: false,
25180
25071
  pos: 0,
25181
- previouslyAnswered: false
25182
- });
25183
-
25184
- const styles$r = aphrodite.StyleSheet.create({
25072
+ showRationale: false
25073
+ };
25074
+ const styles$s = aphrodite.StyleSheet.create({
25185
25075
  description: {
25186
25076
  display: "inline-block",
25187
25077
  width: "100%"
@@ -25221,184 +25111,83 @@ const styles$r = aphrodite.StyleSheet.create({
25221
25111
  marginLeft: 45
25222
25112
  }
25223
25113
  });
25114
+ var Choice$1 = /*#__PURE__*/React__namespace.forwardRef((props, ref) => /*#__PURE__*/React__namespace.createElement(Choice, _extends({}, props, {
25115
+ forwardedRef: ref
25116
+ })));
25224
25117
 
25225
- class ChoiceNoneAbove extends React__namespace.Component {
25226
- constructor() {
25227
- super(...arguments);
25228
-
25229
- _defineProperty(this, "_choice", void 0);
25230
-
25231
- _defineProperty(this, "focusInput", () => {
25232
- this._choice.focusInput();
25233
- });
25234
- }
25235
-
25236
- render() {
25237
- const choiceProps = { ...this.props,
25238
- className: classNames__default["default"](this.props.className, "none-of-above"),
25239
- content: this.props.showContent ? this.props.content :
25240
- /*#__PURE__*/
25241
- // We use a Renderer here because that is how
25242
- // `this.props.content` is wrapped otherwise.
25243
- // We pass in a key here so that we avoid a semi-spurious
25244
- // react warning when we render this in the same place
25245
- // as the previous choice content renderer.
25246
- // Note this destroys state, but since all we're doing
25247
- // is outputting "None of the above", that is okay.
25248
- React__namespace.createElement(Renderer, {
25249
- key: "noneOfTheAboveRenderer",
25250
- content: i18n__namespace._("None of the above")
25251
- }),
25252
- ref: el => this._choice = el
25253
- };
25254
- return /*#__PURE__*/React__namespace.createElement(Choice, choiceProps);
25255
- }
25256
-
25118
+ function ChoiceNoneAbove(props) {
25119
+ const {
25120
+ showContent,
25121
+ content,
25122
+ forwardedRef,
25123
+ ...rest
25124
+ } = props;
25125
+ const choiceProps = { ...rest,
25126
+ content: showContent ? content :
25127
+ /*#__PURE__*/
25128
+ // We use a Renderer here because that is how
25129
+ // `this.props.content` is wrapped otherwise.
25130
+ // We pass in a key here so that we avoid a semi-spurious
25131
+ // react warning when we render this in the same place
25132
+ // as the previous choice content renderer.
25133
+ // Note this destroys state, but since all we're doing
25134
+ // is outputting "None of the above", that is okay.
25135
+ //
25136
+ // todo(matthewc): this seems like way overkill
25137
+ // just to render a string
25138
+ React__namespace.createElement(Renderer, {
25139
+ key: "noneOfTheAboveRenderer",
25140
+ content: i18n__namespace._("None of the above")
25141
+ })
25142
+ };
25143
+ return /*#__PURE__*/React__namespace.createElement(Choice$1, _extends({}, choiceProps, {
25144
+ ref: forwardedRef
25145
+ }));
25257
25146
  }
25258
25147
 
25259
- _defineProperty(ChoiceNoneAbove, "propTypes", {
25260
- className: PropTypes__default["default"].string,
25261
- content: PropTypes__default["default"].node,
25262
- showContent: PropTypes__default["default"].bool
25263
- });
25264
-
25265
- _defineProperty(ChoiceNoneAbove, "defaultProps", {
25148
+ ChoiceNoneAbove.defaultProps = {
25266
25149
  showContent: true
25267
- });
25150
+ };
25151
+ var ChoiceNoneAbove$1 = /*#__PURE__*/React__namespace.forwardRef((props, ref) => /*#__PURE__*/React__namespace.createElement(ChoiceNoneAbove, _extends({}, props, {
25152
+ forwardedRef: ref
25153
+ })));
25268
25154
 
25269
25155
  const {
25270
25156
  captureScratchpadTouchStart: captureScratchpadTouchStart$2
25271
- } = Util;
25272
- const ChoicesType = PropTypes__default["default"].arrayOf(PropTypes__default["default"].shape({
25273
- // Indicates whether this choice is checked.
25274
- checked: PropTypes__default["default"].bool,
25275
- // Indicates whether the user has "crossed out" this choice, meaning
25276
- // that they don't think it's correct. This value does not affect
25277
- // scoring or other behavior; it's just a note for the user's
25278
- // reference.
25279
- crossedOut: PropTypes__default["default"].bool,
25280
- content: PropTypes__default["default"].node,
25281
- rationale: PropTypes__default["default"].node,
25282
- hasRationale: PropTypes__default["default"].bool,
25283
- showRationale: PropTypes__default["default"].bool,
25284
- showCorrectness: PropTypes__default["default"].bool,
25285
- correct: PropTypes__default["default"].bool,
25286
- originalIndex: PropTypes__default["default"].number,
25287
- isNoneOfTheAbove: PropTypes__default["default"].bool
25288
- }));
25289
- const radioBorderColor = radioBorderColor$2;
25290
-
25291
- class BaseRadio extends React__namespace.Component {
25292
- constructor() {
25293
- super(...arguments);
25294
-
25295
- _defineProperty(this, "state", {
25296
- // TODO(mdr): This keeps the ID stable across re-renders on the
25297
- // same machine, but, at time of writing, the server's state
25298
- // isn't rehydrated to the client during SSR, so the server and
25299
- // client will generate different IDs and cause a mismatch
25300
- // during SSR :(
25301
- radioGroupName: ___default["default"].uniqueId("perseus_radio_")
25302
- });
25303
-
25304
- _defineProperty(this, "updateChoice", (choiceIndex, newValues) => {
25305
- // Get the baseline `checked` values. If we're checking a new answer
25306
- // and multiple-select is not on, we should clear all choices to be
25307
- // unchecked. Otherwise, we should copy the old checked values.
25308
- let newCheckedList;
25309
-
25310
- if (newValues.checked && !this.props.multipleSelect) {
25311
- newCheckedList = this.props.choices.map(_ => false);
25312
- } else {
25313
- newCheckedList = this.props.choices.map(c => c.checked);
25314
- } // Get the baseline `crossedOut` values.
25157
+ } = Util; // exported for tests
25315
25158
 
25316
-
25317
- const newCrossedOutList = this.props.choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25318
-
25319
- newCheckedList[choiceIndex] = newValues.checked;
25320
- newCrossedOutList[choiceIndex] = newValues.crossedOut;
25321
- this.props.onChange({
25322
- checked: newCheckedList,
25323
- crossedOut: newCrossedOutList
25159
+ function getInstructionsText(multipleSelect, countChoices, numCorrect) {
25160
+ if (multipleSelect) {
25161
+ if (countChoices) {
25162
+ return i18n__namespace._("Choose %(numCorrect)s answers:", {
25163
+ numCorrect: numCorrect
25324
25164
  });
25325
- });
25326
-
25327
- _defineProperty(this, "focus", i => {
25328
- // $FlowFixMe[incompatible-use]
25329
- // $FlowFixMe[prop-missing]
25330
- ReactDOM__default["default"].findDOMNode(this.refs["radio" + (i || 0)]).focus(); // eslint-disable-line react/no-string-refs
25331
-
25332
- return true;
25333
- });
25334
-
25335
- _defineProperty(this, "getInstructionsText", () => {
25336
- if (this.props.multipleSelect) {
25337
- if (this.props.countChoices) {
25338
- return i18n__namespace._("Choose %(numCorrect)s answers:", {
25339
- numCorrect: this.props.numCorrect
25340
- });
25341
- }
25342
-
25343
- return i18n__namespace._("Choose all answers that apply:");
25344
- }
25345
-
25346
- return i18n__namespace._("Choose 1 answer:");
25347
- });
25348
-
25349
- _defineProperty(this, "deselectEnabled", () => {
25350
- // We want to force enable deselect on mobile.
25351
- return this.props.apiOptions.isMobile || this.props.deselectEnabled;
25352
- });
25353
-
25354
- _defineProperty(this, "goToChoice", newChoiceIndex => {
25355
- const numChoices = this.props.choices.length; // Wrap the newChoiceIndex around the start/end, if necessary.
25356
-
25357
- if (newChoiceIndex < 0) {
25358
- newChoiceIndex += numChoices;
25359
- }
25360
-
25361
- if (newChoiceIndex >= numChoices) {
25362
- newChoiceIndex -= numChoices;
25363
- } // Focus the new choice's input element.
25364
- // eslint-disable-next-line react/no-string-refs
25365
-
25366
-
25367
- const choiceInstance = this.refs["radio".concat(newChoiceIndex)];
25368
-
25369
- if (!choiceInstance) {
25370
- Log.error("found no choice at index ".concat(newChoiceIndex, ", even after ") + "wrapping it to be within bounds", Errors.Internal, {
25371
- loggedMetadata: {
25372
- choices: JSON.stringify(this.props.choices)
25373
- }
25374
- });
25375
- return;
25376
- }
25377
-
25378
- choiceInstance.focusInput(); // We aren't going to change any choices' crossed-out state, but we
25379
- // _are_ going to deselect all choices.
25380
-
25381
- const newCrossedOutList = this.props.choices.map(c => c.crossedOut);
25382
- const newCheckedList = this.props.choices.map(c => false); // And, if the new choice isn't crossed out, we'll select it.
25383
-
25384
- if (!this.props.choices[newChoiceIndex].crossedOut) {
25385
- newCheckedList[newChoiceIndex] = true;
25386
- }
25165
+ }
25387
25166
 
25388
- this.props.onChange({
25389
- checked: newCheckedList,
25390
- crossedOut: newCrossedOutList
25391
- });
25392
- });
25167
+ return i18n__namespace._("Choose all answers that apply:");
25393
25168
  }
25394
25169
 
25395
- componentDidUpdate(prevProps) {
25396
- const {
25397
- apiOptions,
25398
- choices,
25399
- isLastUsedWidget,
25400
- reviewModeRubric
25401
- } = this.props; // Switching into review mode can sometimes cause the selected answer
25170
+ return i18n__namespace._("Choose 1 answer:");
25171
+ }
25172
+
25173
+ function BaseRadio(props) {
25174
+ const {
25175
+ apiOptions,
25176
+ reviewModeRubric,
25177
+ choices,
25178
+ editMode,
25179
+ multipleSelect,
25180
+ labelWrap,
25181
+ countChoices,
25182
+ numCorrect,
25183
+ isLastUsedWidget,
25184
+ registerFocusFunction
25185
+ } = props; // useEffect doesn't have previous props
25186
+
25187
+ const prevReviewModeRubric = React.useRef();
25188
+ const choiceRefs = React.useRef([]);
25189
+ React.useEffect(() => {
25190
+ // Switching into review mode can sometimes cause the selected answer
25402
25191
  // to scroll out of view - for example, when we reveal all those tall
25403
25192
  // rationales. This can be disorienting for the user.
25404
25193
  //
@@ -25411,17 +25200,25 @@ class BaseRadio extends React__namespace.Component {
25411
25200
  // the most recently used widget (because, if the user has since
25412
25201
  // touched something else, they're probably not trying to keep their
25413
25202
  // eye on this widget anymore).
25414
-
25415
- if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevProps.reviewModeRubric) {
25203
+ if (apiOptions.canScrollPage && isLastUsedWidget && reviewModeRubric && !prevReviewModeRubric.current) {
25416
25204
  const checkedIndex = choices.findIndex(c => c.checked);
25417
25205
 
25418
25206
  if (checkedIndex >= 0) {
25419
- const checkedNode = ReactDOM__default["default"].findDOMNode( // eslint-disable-next-line react/no-string-refs
25420
- this.refs["radio" + checkedIndex]);
25421
- scrollElementIntoView(checkedNode);
25207
+ const ref = choiceRefs.current[checkedIndex]; // note(matthew): we know this is only getting passed
25208
+ // to a WB Clickable button, so we force it to be of
25209
+ // type HTMLButtonElement
25210
+
25211
+ const anyNode = ReactDOM__default["default"].findDOMNode(ref.current);
25212
+ const buttonNode = anyNode;
25213
+
25214
+ if (buttonNode) {
25215
+ scrollElementIntoView(buttonNode);
25216
+ }
25422
25217
  }
25423
25218
  }
25424
- } // When a particular choice's `onChange` handler is called, indicating a
25219
+
25220
+ prevReviewModeRubric.current = reviewModeRubric;
25221
+ }, [apiOptions, choices, isLastUsedWidget, reviewModeRubric]); // When a particular choice's `onChange` handler is called, indicating a
25425
25222
  // change in a single choice's values, we need to call our `onChange`
25426
25223
  // handler in order to notify our parent. However, our API with our parent
25427
25224
  // is that we always provide *all* values for *all* choices, even if just
@@ -25430,181 +25227,196 @@ class BaseRadio extends React__namespace.Component {
25430
25227
  // will usually cause the old answer to become unchecked.)
25431
25228
  //
25432
25229
  // So, given the new values for a particular choice, compute the new values
25433
- // for all choices, and pass them to `this.props.onChange`.
25230
+ // for all choices, and pass them to `onChange`.
25434
25231
  //
25435
25232
  // `newValues` is an object with two keys: `checked` and `crossedOut`. Each
25436
25233
  // contains a boolean value specifying the new checked and crossed-out
25437
25234
  // value of this choice.
25438
25235
 
25236
+ function updateChoice(choiceIndex, newValues) {
25237
+ const {
25238
+ multipleSelect,
25239
+ choices,
25240
+ onChange
25241
+ } = props; // Get the baseline `checked` values. If we're checking a new answer
25242
+ // and multiple-select is not on, we should clear all choices to be
25243
+ // unchecked. Otherwise, we should copy the old checked values.
25439
25244
 
25440
- render() {
25441
- const inputType = this.props.multipleSelect ? "checkbox" : "radio";
25442
- const rubric = this.props.reviewModeRubric;
25443
- const reviewMode = !!rubric;
25444
- const styles = BaseRadio.styles;
25445
- const sat = this.props.apiOptions.satStyling;
25446
- const isMobile = this.props.apiOptions.isMobile;
25447
- const choices = this.props.choices;
25448
- const firstChoiceHighlighted = choices[0].highlighted;
25449
- const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25450
- const className = classNames__default["default"]("perseus-widget-radio", !this.props.editMode && "perseus-rendered-radio", aphrodite.css(styles.radio, // SAT doesn't use the "responsive styling" as it conflicts
25451
- // with their custom theming.
25452
- !sat && styles.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles.radioContainerLastHighlighted, sat && styles.satRadio));
25453
- const instructionsClassName = classNames__default["default"]("instructions", aphrodite.css(styles.instructions, isMobile && styles.instructionsMobile));
25454
- const instructions = this.getInstructionsText();
25455
- const shouldShowInstructions = !sat;
25456
- const responsiveClassName = aphrodite.css(styles.responsiveFieldset);
25457
- const fieldset = /*#__PURE__*/React__namespace.createElement("fieldset", {
25458
- className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25459
- }, /*#__PURE__*/React__namespace.createElement("legend", {
25460
- className: "perseus-sr-only"
25461
- }, instructions), shouldShowInstructions && /*#__PURE__*/React__namespace.createElement("div", {
25462
- className: instructionsClassName
25463
- }, instructions), /*#__PURE__*/React__namespace.createElement("ul", {
25464
- className: className,
25465
- style: {
25466
- listStyle: "none"
25467
- }
25468
- }, this.props.choices.map(function (choice, i) {
25469
- let Element = Choice;
25470
- const elementProps = {
25471
- ref: "radio".concat(i),
25472
- apiOptions: this.props.apiOptions,
25473
- checked: choice.checked,
25474
- crossedOut: choice.crossedOut,
25475
- previouslyAnswered: choice.previouslyAnswered,
25476
- reviewMode,
25477
- correct: choice.correct,
25478
- rationale: choice.rationale,
25479
- content: choice.content,
25480
- disabled: this.props.apiOptions.readOnly || choice.disabled,
25481
- editMode: this.props.editMode,
25482
- groupName: this.state.radioGroupName,
25483
- isLastChoice: i === this.props.choices.length - 1,
25484
- showCorrectness: reviewMode || !!choice.showCorrectness,
25485
- showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25486
- type: inputType,
25487
- pos: i,
25488
- deselectEnabled: this.deselectEnabled(),
25489
- onChange: newValues => {
25490
- this.updateChoice(i, newValues);
25491
- },
25492
- goToPrevChoice: () => this.goToChoice(i - 1),
25493
- goToNextChoice: () => this.goToChoice(i + 1)
25494
- };
25245
+ let newCheckedList;
25495
25246
 
25496
- if (choice.isNoneOfTheAbove) {
25497
- Element = ChoiceNoneAbove;
25247
+ if (newValues.checked && !multipleSelect) {
25248
+ newCheckedList = choices.map(_ => false);
25249
+ } else {
25250
+ newCheckedList = choices.map(c => c.checked);
25251
+ } // Get the baseline `crossedOut` values.
25498
25252
 
25499
- ___default["default"].extend(elementProps, {
25500
- showContent: choice.revealNoneOfTheAbove
25501
- });
25502
- }
25503
25253
 
25504
- const nextChoice = this.props.choices[i + 1];
25505
- const nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25506
-
25507
- const aphroditeClassName = checked => {
25508
- // Whether or not to show correctness borders
25509
- // for this choice and the next choice.
25510
- const satShowCorrectness = sat && reviewMode && checked;
25511
- const satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25512
- return aphrodite.css(sharedStyles.aboveScratchpad, styles.item, !sat && styles.responsiveItem, !sat && checked && styles.selectedItem, !sat && checked && choice.highlighted && styles.aboveBackdrop, !sat && checked && choice.highlighted && this.props.apiOptions.isMobile && styles.aboveBackdropMobile, !sat && nextChoiceHighlighted && this.props.apiOptions.isMobile && styles.nextHighlighted, sat && styles.satRadioOption, satShowCorrectness && !choice.correct && styles.satRadioOptionIncorrect, satShowCorrectness && choice.correct && styles.satRadioOptionCorrect, satShowCorrectnessNext && !nextChoice.correct && styles.satRadioOptionNextIncorrect, satShowCorrectnessNext && nextChoice.correct && styles.satRadioOptionNextCorrect, sat && rubric && styles.satReviewRadioOption);
25513
- }; // HACK(abdulrahman): Preloads the selection-state
25514
- // css because of a bug that causes iOS to lag
25515
- // when selecting the button for the first time.
25516
-
25517
-
25518
- aphroditeClassName(true);
25519
- const className = classNames__default["default"](aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25520
- // classNames
25521
- ClassNames.RADIO.OPTION, choice.checked && ClassNames.RADIO.SELECTED, reviewMode && rubric.choices[i].correct && ClassNames.CORRECT, reviewMode && !rubric.choices[i].correct && ClassNames.INCORRECT); // In edit mode, the Choice renders a Div in order to
25522
- // allow for the contentEditable area to be selected
25523
- // (label forces any clicks inside to select the input
25524
- // element) We have to add some extra behavior to make
25525
- // sure that we can still check the choice.
25526
-
25527
- let listElem = null;
25528
- let clickHandler = null;
25529
-
25530
- if (this.props.editMode) {
25531
- clickHandler = e => {
25532
- // Traverse the parent nodes of the clicked
25533
- // element.
25534
- let elem = e.target;
25535
-
25536
- while (elem && elem !== listElem) {
25537
- // If the clicked element is inside of the
25538
- // radio icon, then we want to trigger the
25539
- // check by flipping the choice of the icon.
25540
- if (elem.getAttribute("data-is-radio-icon")) {
25541
- this.updateChoice(i, {
25542
- checked: !choice.checked
25543
- });
25544
- return;
25545
- }
25254
+ const newCrossedOutList = choices.map(c => c.crossedOut); // Update this choice's `checked` and `crossedOut` values.
25255
+
25256
+ newCheckedList[choiceIndex] = newValues.checked;
25257
+ newCrossedOutList[choiceIndex] = newValues.crossedOut;
25258
+ onChange({
25259
+ checked: newCheckedList,
25260
+ crossedOut: newCrossedOutList
25261
+ });
25262
+ } // register a callback with the parent that allows
25263
+ // the parent to focus an individual choice
25264
+
25265
+
25266
+ registerFocusFunction === null || registerFocusFunction === void 0 ? void 0 : registerFocusFunction(choiceIndex => {
25267
+ const ref = choiceRefs.current[choiceIndex || 0]; // note(matthew): we know this is only getting passed
25268
+ // to a WB Clickable button, so we force it to be of
25269
+ // type HTMLButtonElement
25270
+
25271
+ const anyNode = ReactDOM__default["default"].findDOMNode(ref.current);
25272
+ const buttonNode = anyNode;
25273
+
25274
+ if (buttonNode) {
25275
+ buttonNode.focus();
25276
+ } else {
25277
+ return false;
25278
+ }
25279
+
25280
+ return true;
25281
+ }); // some commonly used shorthands
25282
+
25283
+ const reviewMode = !!reviewModeRubric;
25284
+ const sat = apiOptions.satStyling;
25285
+ const isMobile = apiOptions.isMobile;
25286
+ const firstChoiceHighlighted = choices[0].highlighted;
25287
+ const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
25288
+ const className = classNames__default["default"]("perseus-widget-radio", !editMode && "perseus-rendered-radio", aphrodite.css(styles$r.radio, // SAT doesn't use the "responsive styling" as it conflicts
25289
+ // with their custom theming.
25290
+ !sat && styles$r.responsiveRadioContainer, !sat && firstChoiceHighlighted && isMobile && styles$r.radioContainerFirstHighlighted, !sat && lastChoiceHighlighted && isMobile && styles$r.radioContainerLastHighlighted, sat && styles$r.satRadio));
25291
+ const instructionsClassName = classNames__default["default"]("instructions", aphrodite.css(styles$r.instructions, isMobile && styles$r.instructionsMobile));
25292
+ const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
25293
+ const shouldShowInstructions = !sat;
25294
+ const responsiveClassName = aphrodite.css(styles$r.responsiveFieldset);
25295
+ const fieldset = /*#__PURE__*/React__namespace.createElement("fieldset", {
25296
+ className: "perseus-widget-radio-fieldset ".concat(responsiveClassName)
25297
+ }, /*#__PURE__*/React__namespace.createElement("legend", {
25298
+ className: "perseus-sr-only"
25299
+ }, instructions), shouldShowInstructions && /*#__PURE__*/React__namespace.createElement("div", {
25300
+ className: instructionsClassName
25301
+ }, instructions), /*#__PURE__*/React__namespace.createElement("ul", {
25302
+ className: className,
25303
+ style: {
25304
+ listStyle: "none"
25305
+ }
25306
+ }, choices.map(function (choice, i) {
25307
+ let Element = Choice$1;
25308
+ const ref = /*#__PURE__*/React__namespace.createRef();
25309
+ choiceRefs.current[i] = ref;
25310
+ const elementProps = {
25311
+ apiOptions: apiOptions,
25312
+ multipleSelect: multipleSelect,
25313
+ checked: choice.checked,
25314
+ crossedOut: choice.crossedOut,
25315
+ previouslyAnswered: choice.previouslyAnswered,
25316
+ reviewMode,
25317
+ correct: choice.correct,
25318
+ rationale: choice.rationale,
25319
+ content: choice.content,
25320
+ disabled: apiOptions.readOnly || choice.disabled,
25321
+ showCorrectness: reviewMode || !!choice.showCorrectness,
25322
+ showRationale: choice.hasRationale && (reviewMode || choice.showRationale),
25323
+ pos: i,
25324
+ onChange: newValues => {
25325
+ updateChoice(i, newValues);
25326
+ },
25327
+ ref
25328
+ };
25546
25329
 
25547
- elem = elem.parentNode;
25330
+ if (choice.isNoneOfTheAbove) {
25331
+ Element = ChoiceNoneAbove$1;
25332
+
25333
+ ___default["default"].extend(elementProps, {
25334
+ showContent: choice.revealNoneOfTheAbove
25335
+ });
25336
+ }
25337
+
25338
+ const nextChoice = choices[i + 1];
25339
+ const nextChoiceHighlighted = !!nextChoice && nextChoice.highlighted;
25340
+
25341
+ const aphroditeClassName = checked => {
25342
+ // Whether or not to show correctness borders
25343
+ // for this choice and the next choice.
25344
+ const satShowCorrectness = sat && reviewMode && checked;
25345
+ const satShowCorrectnessNext = sat && reviewMode && nextChoice && nextChoice.checked;
25346
+ return aphrodite.css(sharedStyles.aboveScratchpad, styles$r.item, !sat && styles$r.responsiveItem, !sat && checked && styles$r.selectedItem, !sat && checked && choice.highlighted && styles$r.aboveBackdrop, !sat && checked && choice.highlighted && apiOptions.isMobile && styles$r.aboveBackdropMobile, !sat && nextChoiceHighlighted && apiOptions.isMobile && styles$r.nextHighlighted, sat && styles$r.satRadioOption, satShowCorrectness && !choice.correct && styles$r.satRadioOptionIncorrect, satShowCorrectness && choice.correct && styles$r.satRadioOptionCorrect, satShowCorrectnessNext && !nextChoice.correct && styles$r.satRadioOptionNextIncorrect, satShowCorrectnessNext && nextChoice.correct && styles$r.satRadioOptionNextCorrect, sat && reviewModeRubric && styles$r.satReviewRadioOption);
25347
+ }; // HACK(abdulrahman): Preloads the selection-state
25348
+ // css because of a bug that causes iOS to lag
25349
+ // when selecting the button for the first time.
25350
+
25351
+
25352
+ aphroditeClassName(true);
25353
+ let correctnessClass; // reviewMode is only true if there's a rubric
25354
+ // but Flow doesn't understand that
25355
+
25356
+ if (reviewMode && reviewModeRubric) {
25357
+ correctnessClass = reviewModeRubric.choices[i].correct ? ClassNames.CORRECT : ClassNames.INCORRECT;
25358
+ }
25359
+
25360
+ const className = classNames__default["default"](aphroditeClassName(choice.checked), // TODO(aria): Make test case for these API
25361
+ // classNames
25362
+ ClassNames.RADIO.OPTION, choice.checked && ClassNames.RADIO.SELECTED, correctnessClass); // In edit mode, the Choice renders a Div in order to
25363
+ // allow for the contentEditable area to be selected
25364
+ // (label forces any clicks inside to select the input
25365
+ // element) We have to add some extra behavior to make
25366
+ // sure that we can still check the choice.
25367
+
25368
+ let listElem = null;
25369
+ let clickHandler = null;
25370
+
25371
+ if (editMode) {
25372
+ clickHandler = e => {
25373
+ // Traverse the parent nodes of the clicked
25374
+ // element.
25375
+ let elem = e.target;
25376
+
25377
+ while (elem && elem !== listElem) {
25378
+ // If the clicked element is inside of the
25379
+ // radio icon, then we want to trigger the
25380
+ // check by flipping the choice of the icon.
25381
+ if (elem.getAttribute("data-is-radio-icon")) {
25382
+ this.updateChoice(i, {
25383
+ checked: !choice.checked,
25384
+ crossedOut: choice.crossedOut
25385
+ });
25386
+ return;
25548
25387
  }
25549
- };
25550
- } // TODO(mattdr): Index isn't a *good* choice of key
25551
- // here; is there a better one? Can we use choice
25552
- // content somehow? Would changing our choice of key
25553
- // somehow break something happening inside a choice's
25554
- // child Renderers, by changing when we mount/unmount?
25555
25388
 
25389
+ elem = elem.parentNode;
25390
+ }
25391
+ };
25392
+ } // TODO(mattdr): Index isn't a *good* choice of key
25393
+ // here; is there a better one? Can we use choice
25394
+ // content somehow? Would changing our choice of key
25395
+ // somehow break something happening inside a choice's
25396
+ // child Renderers, by changing when we mount/unmount?
25556
25397
 
25557
- return /*#__PURE__*/React__namespace.createElement("li", {
25558
- key: i,
25559
- ref: e => listElem = e,
25560
- className: className,
25561
- onClick: clickHandler,
25562
- onTouchStart: !this.props.labelWrap ? null : captureScratchpadTouchStart$2
25563
- }, /*#__PURE__*/React__namespace.createElement(Element, elementProps));
25564
- }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25565
- // an issue especially on phones.
25566
- // This is disabled in SAT, since it conflicts with their theming.
25567
25398
 
25568
- return /*#__PURE__*/React__namespace.createElement("div", {
25569
- className: aphrodite.css(!sat && styles.responsiveContainer)
25570
- }, fieldset);
25571
- }
25399
+ return /*#__PURE__*/React__namespace.createElement("li", {
25400
+ key: i,
25401
+ ref: e => listElem = e,
25402
+ className: className,
25403
+ onClick: clickHandler,
25404
+ onTouchStart: !labelWrap ? null : captureScratchpadTouchStart$2
25405
+ }, /*#__PURE__*/React__namespace.createElement(Element, elementProps));
25406
+ }, this))); // Allow for horizontal scrolling if content is too wide, which may be
25407
+ // an issue especially on phones.
25408
+ // This is disabled in SAT, since it conflicts with their theming.
25572
25409
 
25410
+ return /*#__PURE__*/React__namespace.createElement("div", {
25411
+ className: aphrodite.css(!sat && styles$r.responsiveContainer)
25412
+ }, fieldset);
25573
25413
  }
25574
25414
 
25575
- _defineProperty(BaseRadio, "propTypes", {
25576
- apiOptions: PropTypes__default["default"].shape({
25577
- readOnly: PropTypes__default["default"].bool,
25578
- satStyling: PropTypes__default["default"].bool,
25579
- isMobile: PropTypes__default["default"].bool,
25580
- styling: PropTypes__default["default"].shape({
25581
- radioStyleVersion: PropTypes__default["default"].oneOf(["intermediate", "final"])
25582
- }),
25583
- canScrollPage: PropTypes__default["default"].bool
25584
- }),
25585
- choices: ChoicesType,
25586
- deselectEnabled: PropTypes__default["default"].bool,
25587
- editMode: PropTypes__default["default"].bool,
25588
- labelWrap: PropTypes__default["default"].bool,
25589
- countChoices: PropTypes__default["default"].bool,
25590
- numCorrect: PropTypes__default["default"].number,
25591
- multipleSelect: PropTypes__default["default"].bool,
25592
- reviewModeRubric: PropTypes__default["default"].shape({
25593
- choices: ChoicesType
25594
- }),
25595
- // A callback indicating that this choice has changed. Its argument is
25596
- // an object with two keys: `checked` and `crossedOut`. Each contains
25597
- // an array of boolean values, specifying the new checked and
25598
- // crossed-out value of each choice.
25599
- onChange: PropTypes__default["default"].func,
25600
- // Whether this widget was the most recently used widget in this
25601
- // Renderer. Determines whether we'll auto-scroll the page upon
25602
- // entering review mode.
25603
- isLastUsedWidget: PropTypes__default["default"].bool
25604
- });
25605
-
25606
- _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25607
- // eslint-disable-next-line react-native/no-unused-styles
25415
+ BaseRadio.defaultProps = {
25416
+ editMode: false,
25417
+ multipleSelect: false
25418
+ };
25419
+ const styles$r = aphrodite.StyleSheet.create({
25608
25420
  instructions: {
25609
25421
  display: "block",
25610
25422
  color: gray17,
@@ -25614,7 +25426,6 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25614
25426
  fontWeight: "bold",
25615
25427
  marginBottom: 16
25616
25428
  },
25617
- // eslint-disable-next-line react-native/no-unused-styles
25618
25429
  instructionsMobile: {
25619
25430
  fontSize: 18,
25620
25431
  [mediaQueries.smOrSmaller]: {
@@ -25628,36 +25439,30 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25628
25439
  fontSize: 20
25629
25440
  }
25630
25441
  },
25631
- // eslint-disable-next-line react-native/no-unused-styles
25632
25442
  radio: {
25633
25443
  // Avoid centering
25634
25444
  width: "100%"
25635
25445
  },
25636
- // eslint-disable-next-line react-native/no-unused-styles
25637
25446
  responsiveRadioContainer: {
25638
- borderBottom: "1px solid ".concat(radioBorderColor),
25639
- borderTop: "1px solid ".concat(radioBorderColor),
25447
+ borderBottom: "1px solid ".concat(radioBorderColor$1),
25448
+ borderTop: "1px solid ".concat(radioBorderColor$1),
25640
25449
  width: "auto",
25641
25450
  [mediaQueries.smOrSmaller]: {
25642
25451
  marginLeft: negativePhoneMargin,
25643
25452
  marginRight: negativePhoneMargin
25644
25453
  }
25645
25454
  },
25646
- // eslint-disable-next-line react-native/no-unused-styles
25647
25455
  radioContainerFirstHighlighted: {
25648
25456
  borderTop: "1px solid rgba(0, 0, 0, 0)"
25649
25457
  },
25650
- // eslint-disable-next-line react-native/no-unused-styles
25651
25458
  radioContainerLastHighlighted: {
25652
25459
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25653
25460
  },
25654
- // eslint-disable-next-line react-native/no-unused-styles
25655
25461
  satRadio: {
25656
25462
  background: "none",
25657
25463
  marginLeft: 0,
25658
25464
  userSelect: "none"
25659
25465
  },
25660
- // eslint-disable-next-line react-native/no-unused-styles
25661
25466
  satRadioOption: {
25662
25467
  margin: 0,
25663
25468
  padding: 0,
@@ -25666,57 +25471,40 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25666
25471
  borderTop: "1px solid #ccc"
25667
25472
  }
25668
25473
  },
25669
- // eslint-disable-next-line react-native/no-unused-styles
25670
25474
  satRadioOptionCorrect: {
25671
25475
  borderBottomColor: satCorrectBorderColor,
25672
25476
  ":first-child": {
25673
25477
  borderTopColor: satCorrectBorderColor
25674
25478
  }
25675
25479
  },
25676
- // eslint-disable-next-line react-native/no-unused-styles
25677
25480
  satRadioOptionIncorrect: {
25678
25481
  borderBottomColor: satIncorrectBorderColor,
25679
25482
  ":first-child": {
25680
25483
  borderTopColor: satIncorrectBorderColor
25681
25484
  }
25682
25485
  },
25683
- // eslint-disable-next-line react-native/no-unused-styles
25684
25486
  satRadioOptionNextCorrect: {
25685
25487
  borderBottomColor: satCorrectBorderColor
25686
25488
  },
25687
- // eslint-disable-next-line react-native/no-unused-styles
25688
25489
  satRadioOptionNextIncorrect: {
25689
25490
  borderBottomColor: satIncorrectBorderColor
25690
25491
  },
25691
- // eslint-disable-next-line react-native/no-unused-styles
25692
25492
  satReviewRadioOption: {
25693
25493
  pointerEvents: "none"
25694
25494
  },
25695
- // eslint-disable-next-line react-native/no-unused-styles
25696
25495
  item: {
25697
25496
  marginLeft: 20
25698
25497
  },
25699
- // eslint-disable-next-line react-native/no-unused-styles
25700
- inlineItem: {
25701
- display: "inline-block",
25702
- paddingLeft: 20,
25703
- verticalAlign: "middle" // See http://stackoverflow.com/q/8120466 for explanation of
25704
- // why vertical align property is needed
25705
-
25706
- },
25707
- // eslint-disable-next-line react-native/no-unused-styles
25708
25498
  responsiveItem: {
25709
25499
  marginLeft: 0,
25710
25500
  padding: 0,
25711
25501
  ":not(:last-child)": {
25712
- borderBottom: "1px solid ".concat(radioBorderColor)
25502
+ borderBottom: "1px solid ".concat(radioBorderColor$1)
25713
25503
  }
25714
25504
  },
25715
- // eslint-disable-next-line react-native/no-unused-styles
25716
25505
  selectedItem: {
25717
25506
  background: "white"
25718
25507
  },
25719
- // eslint-disable-next-line react-native/no-unused-styles
25720
25508
  aboveBackdrop: {
25721
25509
  position: "relative",
25722
25510
  // HACK(emily): We want selected choices to show up above our
@@ -25725,20 +25513,17 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25725
25513
  // coordinated between here and webapp. :(
25726
25514
  zIndex: 1062
25727
25515
  },
25728
- // eslint-disable-next-line react-native/no-unused-styles
25729
25516
  aboveBackdropMobile: {
25730
25517
  boxShadow: "0 0 4px 0 rgba(0, 0, 0, 0.2)," + "0 0 2px 0 rgba(0, 0, 0, 0.1)",
25731
25518
  ":not(:last-child)": {
25732
25519
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25733
25520
  }
25734
25521
  },
25735
- // eslint-disable-next-line react-native/no-unused-styles
25736
25522
  nextHighlighted: {
25737
25523
  ":not(:last-child)": {
25738
25524
  borderBottom: "1px solid rgba(0, 0, 0, 0)"
25739
25525
  }
25740
25526
  },
25741
- // eslint-disable-next-line react-native/no-unused-styles
25742
25527
  responsiveContainer: {
25743
25528
  overflow: "auto",
25744
25529
  marginLeft: negativePhoneMargin,
@@ -25746,22 +25531,22 @@ _defineProperty(BaseRadio, "styles", aphrodite.StyleSheet.create({
25746
25531
  paddingLeft: phoneMargin // paddingRight is handled by responsiveFieldset
25747
25532
 
25748
25533
  },
25749
- // eslint-disable-next-line react-native/no-unused-styles
25750
25534
  responsiveFieldset: {
25751
25535
  paddingRight: phoneMargin
25752
25536
  }
25753
- }));
25754
-
25755
- _defineProperty(BaseRadio, "defaultProps", {
25756
- editMode: false
25757
25537
  });
25758
25538
 
25759
25539
  class Radio$1 extends React__namespace.Component {
25760
25540
  constructor() {
25541
+ var _this;
25542
+
25761
25543
  super(...arguments);
25544
+ _this = this;
25545
+
25546
+ _defineProperty(this, "focusFunction", void 0);
25762
25547
 
25763
- _defineProperty(this, "_renderRenderer", content => {
25764
- content = content || "";
25548
+ _defineProperty(this, "_renderRenderer", function () {
25549
+ let content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
25765
25550
  let nextPassageRefId = 1;
25766
25551
  const widgets = {};
25767
25552
  const modContent = content.replace(/\{\{passage-ref (\d+) (\d+)(?: "([^"]*)")?\}\}/g, (match, passageNum, refNum, summaryText) => {
@@ -25793,19 +25578,14 @@ class Radio$1 extends React__namespace.Component {
25793
25578
  key: "choiceContentRenderer",
25794
25579
  content: modContent,
25795
25580
  widgets: widgets,
25796
- findExternalWidgets: this.props.findWidgets,
25581
+ findExternalWidgets: _this.props.findWidgets,
25797
25582
  alwaysUpdate: true,
25798
- linterContext: { ...this.props.linterContext,
25583
+ linterContext: { ..._this.props.linterContext,
25799
25584
  blockHighlight: true
25800
25585
  }
25801
25586
  });
25802
25587
  });
25803
25588
 
25804
- _defineProperty(this, "focus", i => {
25805
- // eslint-disable-next-line react/no-string-refs
25806
- return this.refs.baseRadio.focus(i);
25807
- });
25808
-
25809
25589
  _defineProperty(this, "updateChoices", newValueLists => {
25810
25590
  const {
25811
25591
  choiceStates,
@@ -25816,25 +25596,18 @@ class Radio$1 extends React__namespace.Component {
25816
25596
  // should clone the old `choiceStates` objects, in preparation to
25817
25597
  // mutate them.
25818
25598
 
25819
- let newChoiceStates;
25820
-
25821
- if (choiceStates) {
25822
- newChoiceStates = choiceStates.map(state => ({ ...state
25823
- }));
25824
- } else {
25825
- newChoiceStates = choices.map(() => ({
25826
- selected: false,
25827
- crossedOut: false,
25828
- highlighted: false,
25829
- rationaleShown: false,
25830
- correctnessShown: false,
25831
- previouslyAnswered: false,
25832
- readOnly: false
25833
- }));
25834
- } // Mutate the new `choiceState` objects, according to the new `checked`
25599
+ const newChoiceStates = choiceStates ? choiceStates.map(state => ({ ...state
25600
+ })) : choices.map(() => ({
25601
+ selected: false,
25602
+ crossedOut: false,
25603
+ highlighted: false,
25604
+ rationaleShown: false,
25605
+ correctnessShown: false,
25606
+ previouslyAnswered: false,
25607
+ readOnly: false
25608
+ })); // Mutate the new `choiceState` objects, according to the new `checked`
25835
25609
  // and `crossedOut` values provided in `newValueLists`.
25836
25610
 
25837
-
25838
25611
  newChoiceStates.forEach((choiceState, i) => {
25839
25612
  choiceState.selected = newValueLists.checked[i];
25840
25613
  choiceState.crossedOut = newValueLists.crossedOut[i];
@@ -25898,14 +25671,47 @@ class Radio$1 extends React__namespace.Component {
25898
25671
  });
25899
25672
  }
25900
25673
 
25674
+ // TODO(LP-10672): I think this might be unused right now. I can't find anywhere
25675
+ // that we pass a value to `.focus()` and it seems to have been used for
25676
+ // adding hints when editing.
25677
+ // See: https://github.com/Khan/perseus/blame/e18582b4b69959270b90e237ef1813899711ddfa/src/widgets/radio.js#L169
25678
+ focus(choiceIndex) {
25679
+ if (this.focusFunction) {
25680
+ return this.focusFunction(choiceIndex);
25681
+ }
25682
+
25683
+ return false;
25684
+ } // lets BaseRadio regiser a focus callback so widget
25685
+ // can focus an individual choice
25686
+
25687
+
25688
+ registerFocusFunction(fun) {
25689
+ this.focusFunction = fun;
25690
+ } // When `BaseRadio`'s `onChange` handler is called, indicating a change in
25691
+ // our choices' state, we need to call our `onChange` handler in order to
25692
+ // persist those changes in the item's Perseus state.
25693
+ //
25694
+ // So, given the new values for each choice, construct the new
25695
+ // `choiceStates` objects, and pass them to `this.props.onChange`.
25696
+ //
25697
+ // `newValueLists` is an object with two keys: `checked` and `crossedOut`.
25698
+ // Each contains an array of boolean values, specifying the new checked and
25699
+ // crossed-out value of each choice.
25700
+ //
25701
+ // NOTE(mdr): This method expects to be auto-bound. If this component is
25702
+ // converted to an ES6 class, take care to auto-bind this method!
25703
+
25704
+
25901
25705
  render() {
25902
- let choices = this.props.choices;
25706
+ const {
25707
+ choices
25708
+ } = this.props;
25903
25709
  let choiceStates;
25904
25710
 
25905
25711
  if (this.props.static) {
25906
- choiceStates = ___default["default"].map(choices, val => ({
25907
- selected: val.correct,
25908
- crossedOut: val.crossedOut,
25712
+ choiceStates = choices.map(choice => ({
25713
+ selected: !!choice.correct,
25714
+ crossedOut: false,
25909
25715
  readOnly: true,
25910
25716
  highlighted: false,
25911
25717
  rationaleShown: true,
@@ -25913,12 +25719,12 @@ class Radio$1 extends React__namespace.Component {
25913
25719
  previouslyAnswered: false
25914
25720
  }));
25915
25721
  } else if (this.props.choiceStates) {
25916
- choiceStates = this.props.choiceStates; // $FlowFixMe[prop-missing]
25722
+ choiceStates = this.props.choiceStates;
25917
25723
  } else if (this.props.values) {
25918
25724
  // Support legacy choiceStates implementation
25919
25725
 
25920
25726
  /* istanbul ignore next - props.values is deprecated */
25921
- choiceStates = ___default["default"].map(this.props.values, val => ({
25727
+ choiceStates = this.props.values.map(val => ({
25922
25728
  selected: val,
25923
25729
  crossedOut: false,
25924
25730
  readOnly: false,
@@ -25928,7 +25734,7 @@ class Radio$1 extends React__namespace.Component {
25928
25734
  previouslyAnswered: false
25929
25735
  }));
25930
25736
  } else {
25931
- choiceStates = ___default["default"].map(choices, () => ({
25737
+ choiceStates = choices.map(() => ({
25932
25738
  selected: false,
25933
25739
  crossedOut: false,
25934
25740
  readOnly: false,
@@ -25939,7 +25745,7 @@ class Radio$1 extends React__namespace.Component {
25939
25745
  }));
25940
25746
  }
25941
25747
 
25942
- choices = ___default["default"].map(choices, (choice, i) => {
25748
+ const choicesProp = choices.map((choice, i) => {
25943
25749
  const content = choice.isNoneOfTheAbove && !choice.content ? // we use i18n._ instead of $_ here because the content
25944
25750
  // sent to a renderer needs to be a string, not a react
25945
25751
  // node (/renderable/fragment).
@@ -25967,37 +25773,36 @@ class Radio$1 extends React__namespace.Component {
25967
25773
  // TODO(emily): Come up with a more comprehensive way to solve
25968
25774
  // this sort of "serialized state breaks when internal
25969
25775
  // structure changes" problem.
25970
- correct: typeof choice.correct === "undefined" ? !!reviewChoice && reviewChoice.correct : choice.correct,
25776
+ correct: choice.correct === undefined ? !!reviewChoice && !!reviewChoice.correct : choice.correct,
25971
25777
  disabled: readOnly,
25972
25778
  hasRationale: !!choice.clue,
25973
25779
  rationale: this._renderRenderer(choice.clue),
25974
25780
  showRationale: rationaleShown,
25975
25781
  showCorrectness: correctnessShown,
25976
- isNoneOfTheAbove: choice.isNoneOfTheAbove,
25977
- revealNoneOfTheAbove: this.props.questionCompleted && selected,
25782
+ isNoneOfTheAbove: !!choice.isNoneOfTheAbove,
25783
+ revealNoneOfTheAbove: !!(this.props.questionCompleted && selected),
25978
25784
  crossedOut,
25979
25785
  highlighted,
25980
25786
  previouslyAnswered: previouslyAnswered
25981
25787
  };
25982
25788
  });
25983
- return /*#__PURE__*/React__namespace.createElement(BaseRadio // eslint-disable-next-line react/no-string-refs
25984
- , {
25985
- ref: "baseRadio",
25789
+ return /*#__PURE__*/React__namespace.createElement(BaseRadio, {
25986
25790
  labelWrap: true,
25987
25791
  multipleSelect: this.props.multipleSelect,
25988
25792
  countChoices: this.props.countChoices,
25989
25793
  numCorrect: this.props.numCorrect,
25990
- choices: choices,
25794
+ choices: choicesProp,
25991
25795
  onChange: this.updateChoices,
25992
25796
  reviewModeRubric: this.props.reviewModeRubric,
25993
25797
  deselectEnabled: this.props.deselectEnabled,
25994
25798
  apiOptions: this.props.apiOptions,
25995
- isLastUsedWidget: this.props.isLastUsedWidget
25799
+ isLastUsedWidget: this.props.isLastUsedWidget,
25800
+ registerFocusFunction: i => this.registerFocusFunction(i)
25996
25801
  });
25997
25802
  }
25998
25803
 
25999
25804
  static validate(userInput, rubric) {
26000
- const numSelected = ___default["default"].reduce(userInput.choicesSelected, (sum, selected) => {
25805
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
26001
25806
  return sum + (selected ? 1 : 0);
26002
25807
  }, 0);
26003
25808
 
@@ -26006,10 +25811,9 @@ class Radio$1 extends React__namespace.Component {
26006
25811
  type: "invalid",
26007
25812
  message: null
26008
25813
  };
26009
- } // $FlowFixMe[invalid-compare]
26010
-
25814
+ }
26011
25815
 
26012
- if (userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
25816
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
26013
25817
  return {
26014
25818
  type: "invalid",
26015
25819
  message: i18n__namespace._("Please choose the correct number of answers.")
@@ -26022,14 +25826,12 @@ class Radio$1 extends React__namespace.Component {
26022
25826
  message: i18n__namespace._("'None of the above' may not be selected " + "when other answers are selected.")
26023
25827
  };
26024
25828
  }
26025
- /* jshint -W018 */
26026
-
26027
25829
 
26028
- const correct = ___default["default"].all(userInput.choicesSelected, (selected, i) => {
25830
+ const correct = userInput.choicesSelected.every((selected, i) => {
26029
25831
  let isCorrect;
26030
25832
 
26031
25833
  if (userInput.noneOfTheAboveIndex === i) {
26032
- isCorrect = ___default["default"].all(rubric.choices, (choice, j) => {
25834
+ isCorrect = rubric.choices.every((choice, j) => {
26033
25835
  return i === j || !choice.correct;
26034
25836
  });
26035
25837
  } else {
@@ -26038,9 +25840,6 @@ class Radio$1 extends React__namespace.Component {
26038
25840
 
26039
25841
  return isCorrect === selected;
26040
25842
  });
26041
- /* jshint +W018 */
26042
-
26043
-
26044
25843
  return {
26045
25844
  type: "points",
26046
25845
  earned: correct ? 1 : 0,
@@ -26062,9 +25861,7 @@ class Radio$1 extends React__namespace.Component {
26062
25861
  const numCorrect = props.numCorrect;
26063
25862
 
26064
25863
  for (let i = 0; i < choicesSelected.length; i++) {
26065
- // $FlowFixMe[prop-missing]
26066
- const index = props.choices[i].originalIndex; // $FlowFixMe[incompatible-use]
26067
-
25864
+ const index = props.choices[i].originalIndex;
26068
25865
  choicesSelected[index] = choiceStates[i].selected;
26069
25866
 
26070
25867
  if (props.choices[i].isNoneOfTheAbove) {
@@ -26085,31 +25882,35 @@ class Radio$1 extends React__namespace.Component {
26085
25882
  }; // Support legacy choiceState implementation
26086
25883
  }
26087
25884
  /* istanbul ignore if - props.values is deprecated */
26088
- // $FlowFixMe[prop-missing]
26089
25885
 
26090
25886
 
26091
- if (props.values) {
25887
+ const {
25888
+ values
25889
+ } = props;
25890
+
25891
+ if (values) {
26092
25892
  let noneOfTheAboveIndex = null;
26093
25893
  let noneOfTheAboveSelected = false;
26094
- const values = props.values.slice();
25894
+ const choicesSelected = [...values];
26095
25895
  const countChoices = props.countChoices;
26096
25896
  const numCorrect = props.numCorrect;
25897
+ const valuesLength = values.length;
26097
25898
 
26098
- for (let i = 0; i < props.values.length; i++) {
25899
+ for (let i = 0; i < valuesLength; i++) {
26099
25900
  const index = props.choices[i].originalIndex;
26100
- values[index] = props.values[i];
25901
+ choicesSelected[index] = values[i];
26101
25902
 
26102
25903
  if (props.choices[i].isNoneOfTheAbove) {
26103
25904
  noneOfTheAboveIndex = index;
26104
25905
 
26105
- if (values[i]) {
25906
+ if (choicesSelected[i]) {
26106
25907
  noneOfTheAboveSelected = true;
26107
25908
  }
26108
25909
  }
26109
25910
  }
26110
25911
 
26111
25912
  return {
26112
- choicesSelected: values,
25913
+ choicesSelected,
26113
25914
  noneOfTheAboveIndex,
26114
25915
  noneOfTheAboveSelected,
26115
25916
  countChoices,
@@ -26119,15 +25920,14 @@ class Radio$1 extends React__namespace.Component {
26119
25920
 
26120
25921
 
26121
25922
  return {
26122
- choicesSelected: ___default["default"].map(props.choices, () => false)
25923
+ choicesSelected: props.choices.map(() => false)
26123
25924
  };
26124
25925
  }
26125
25926
 
26126
25927
  }
26127
25928
 
26128
25929
  _defineProperty(Radio$1, "defaultProps", {
26129
- choices: [{}],
26130
- displayCount: null,
25930
+ choices: [],
26131
25931
  multipleSelect: false,
26132
25932
  countChoices: false,
26133
25933
  deselectEnabled: false,
@@ -26158,14 +25958,14 @@ const _choiceTransform = (widgetOptions, problemNum) => {
26158
25958
 
26159
25959
  const _addNoneOfAbove = function (choices) {
26160
25960
  let noneOfTheAbove = null;
26161
-
26162
- const newChoices = ___default["default"].reject(choices, function (choice, index) {
25961
+ const newChoices = choices.filter((choice, index) => {
26163
25962
  if (choice.isNoneOfTheAbove) {
26164
25963
  noneOfTheAbove = choice;
26165
- return true;
25964
+ return false;
26166
25965
  }
26167
- }); // Place the "None of the above" options last
26168
25966
 
25967
+ return true;
25968
+ }); // Place the "None of the above" options last
26169
25969
 
26170
25970
  if (noneOfTheAbove) {
26171
25971
  newChoices.push(noneOfTheAbove);
@@ -26185,12 +25985,11 @@ const _choiceTransform = (widgetOptions, problemNum) => {
26185
25985
  }; // Add meta-information to choices
26186
25986
 
26187
25987
 
26188
- let choices = widgetOptions.choices.slice();
26189
- choices = ___default["default"].map(choices, (choice, i) => {
26190
- return ___default["default"].extend({}, choice, {
25988
+ const choices = widgetOptions.choices.map((choice, i) => {
25989
+ return { ...choice,
26191
25990
  originalIndex: i,
26192
25991
  correct: Boolean(choice.correct)
26193
- });
25992
+ };
26194
25993
  }); // Apply all the transforms. Note that the order we call these is
26195
25994
  // important!
26196
25995
  // 3) finally add "None of the above" to the bottom
@@ -40994,43 +40793,23 @@ const styles$9 = aphrodite.StyleSheet.create({
40994
40793
  }
40995
40794
  });
40996
40795
 
40997
- const getLineHeightForNode = ($line1, $line2) => {
40998
- return $line2.offset().top - $line1.offset().top;
40796
+ const getLineHeightForNode = (line1, line2) => {
40797
+ return $__default["default"](line2).offset().top - $__default["default"](line1).offset().top;
40999
40798
  };
41000
40799
 
41001
- const START_REF_PREFIX = "start-ref-";
41002
- const END_REF_PREFIX = "end-ref-";
41003
- const REF_STYLE = {
41004
- display: "inline-block",
41005
- width: 0,
41006
- visibility: "hidden"
41007
- };
41008
- const LABEL_OUTER_STYLE = {
41009
- // for some reason we need these to keep the nbsp from wrapping when the
41010
- // inner circle/square is display: inline-block
41011
- display: "inline",
41012
- whiteSpace: "nowrap"
41013
- };
41014
- const SQUARE_LABEL_STYLE = {
41015
- display: "inline-block",
41016
- color: "rgb(255, 255, 255)",
41017
- backgroundColor: "rgb(90, 90, 90)",
41018
- paddingLeft: 10,
41019
- paddingRight: 10,
41020
- userSelect: "none",
41021
- WebkitUserSelect: "none"
41022
- };
41023
- const CIRCLE_LABEL_STYLE = {
41024
- display: "inline-block",
41025
- color: "rgb(255, 255, 255)",
41026
- backgroundColor: "rgb(90, 90, 90)",
41027
- userSelect: "none",
41028
- WebkitUserSelect: "none",
41029
- width: 22,
41030
- height: 22,
41031
- borderRadius: "50%",
41032
- textAlign: "center"
41033
- };
40800
+ function getInitialParseState() {
40801
+ return {
40802
+ currentRef: [],
40803
+ useRefs: true,
40804
+ lastRef: 0,
40805
+ firstSentenceRef: null,
40806
+ firstQuestionRef: null,
40807
+ lastFootnote: {
40808
+ id: 0,
40809
+ text: ""
40810
+ }
40811
+ };
40812
+ }
41034
40813
 
41035
40814
  class RefStart extends React__namespace.Component {
41036
40815
  constructor() {
@@ -41049,10 +40828,6 @@ class RefStart extends React__namespace.Component {
41049
40828
 
41050
40829
  }
41051
40830
 
41052
- _defineProperty(RefStart, "propTypes", {
41053
- refContent: PropTypes__default["default"].node.isRequired
41054
- });
41055
-
41056
40831
  class RefEnd extends React__namespace.Component {
41057
40832
  render() {
41058
40833
  return /*#__PURE__*/React__namespace.createElement("span", {
@@ -41063,20 +40838,11 @@ class RefEnd extends React__namespace.Component {
41063
40838
  }
41064
40839
 
41065
40840
  const rules = {
41066
- // $FlowFixMe[prop-missing]
41067
- // $FlowFixMe[incompatible-use]
41068
40841
  newline: SimpleMarkdown__default["default"].defaultRules.newline,
41069
- // $FlowFixMe[prop-missing]
41070
- // $FlowFixMe[incompatible-use]
41071
40842
  paragraph: SimpleMarkdown__default["default"].defaultRules.paragraph,
41072
- // $FlowFixMe[prop-missing]
41073
- // $FlowFixMe[incompatible-use]
41074
40843
  escape: SimpleMarkdown__default["default"].defaultRules.escape,
41075
40844
  passageFootnote: {
41076
- // $FlowFixMe[prop-missing]
41077
- // $FlowFixMe[incompatible-use]
41078
40845
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.1,
41079
- // $FlowFixMe[prop-missing]
41080
40846
  match: SimpleMarkdown__default["default"].inlineRegex(/^\^/),
41081
40847
  parse: (capture, parse, state) => {
41082
40848
  // if no footnotes have been seen, we're id 1. otherwise,
@@ -41105,10 +40871,8 @@ const rules = {
41105
40871
  }
41106
40872
  },
41107
40873
  refStart: {
41108
- // $FlowFixMe[prop-missing]
41109
- // $FlowFixMe[incompatible-use]
41110
40874
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.2,
41111
- match: function (source, state) {
40875
+ match: function (source) {
41112
40876
  const capture = /^\{\{/.exec(source);
41113
40877
 
41114
40878
  if (capture) {
@@ -41139,12 +40903,14 @@ const rules = {
41139
40903
  }
41140
40904
 
41141
40905
  closeIndex++;
41142
- }
41143
-
41144
- const refText = source.slice(2, closeIndex); // A "magic" capture that matches the opening {{
40906
+ } // A "magic" capture that matches the opening {{
41145
40907
  // but captures the full ref text internally :D
40908
+ // unfortunately the "magic" makes Flow upset,
40909
+ // so there's this questionable workaround
41146
40910
 
41147
- return [capture[0], refText];
40911
+
40912
+ capture[1] = source.slice(2, closeIndex);
40913
+ return capture;
41148
40914
  }
41149
40915
 
41150
40916
  return null;
@@ -41167,33 +40933,34 @@ const rules = {
41167
40933
  // to make those refs into react refs on the
41168
40934
  // passage, for instance!
41169
40935
  useRefs: false
41170
- }, INITIAL_PARSE_STATE));
40936
+ }, getInitialParseState()));
41171
40937
  return {
41172
40938
  ref: ref,
41173
40939
  refContent: refContent
41174
40940
  };
41175
40941
  },
41176
40942
  react: (node, output, state) => {
41177
- if (node.ref == null) {
40943
+ const ref = node.ref;
40944
+
40945
+ if (ref == null) {
41178
40946
  return null;
41179
40947
  } // We don't pass state here because this is parsed
41180
40948
  // and output out-of-band. We don't want to affect
41181
40949
  // our state by the double-output here :).
41182
40950
 
41183
40951
 
41184
- const refContent = output(node.refContent, {});
40952
+ const refContent = output(node.refContent, {}); // note(matthewc) the refs created here become the refs
40953
+ // pulled from `this.refs` in passage.jsx
40954
+
41185
40955
  return /*#__PURE__*/React__namespace.createElement(RefStart, {
41186
- ref: START_REF_PREFIX + node.ref,
41187
- key: START_REF_PREFIX + node.ref,
40956
+ ref: START_REF_PREFIX + ref,
40957
+ key: START_REF_PREFIX + ref,
41188
40958
  refContent: refContent
41189
40959
  });
41190
40960
  }
41191
40961
  },
41192
40962
  refEnd: {
41193
- // $FlowFixMe[prop-missing]
41194
- // $FlowFixMe[incompatible-use]
41195
40963
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.3,
41196
- // $FlowFixMe[prop-missing]
41197
40964
  match: SimpleMarkdown__default["default"].inlineRegex(/^\}\}/),
41198
40965
  parse: (capture, parse, state) => {
41199
40966
  if (!state.useRefs) {
@@ -41209,6 +40976,8 @@ const rules = {
41209
40976
  },
41210
40977
  react: (node, output, state) => {
41211
40978
  if (node.ref != null) {
40979
+ // note(matthewc) the refs created here become the refs
40980
+ // pulled from `this.refs` in passage.jsx
41212
40981
  return /*#__PURE__*/React__namespace.createElement(RefEnd, {
41213
40982
  ref: END_REF_PREFIX + node.ref,
41214
40983
  key: END_REF_PREFIX + node.ref
@@ -41222,10 +40991,7 @@ const rules = {
41222
40991
  }
41223
40992
  },
41224
40993
  squareLabel: {
41225
- // $FlowFixMe[prop-missing]
41226
- // $FlowFixMe[incompatible-use]
41227
40994
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.4,
41228
- // $FlowFixMe[prop-missing]
41229
40995
  match: SimpleMarkdown__default["default"].inlineRegex(/^\[\[(\w+)\]\]( *)/),
41230
40996
  parse: (capture, parse, state) => {
41231
40997
  if (!state.firstQuestionRef) {
@@ -41254,10 +41020,7 @@ const rules = {
41254
41020
  }
41255
41021
  },
41256
41022
  circleLabel: {
41257
- // $FlowFixMe[prop-missing]
41258
- // $FlowFixMe[incompatible-use]
41259
41023
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.5,
41260
- // $FlowFixMe[prop-missing]
41261
41024
  match: SimpleMarkdown__default["default"].inlineRegex(/^\(\((\w+)\)\)( *)/),
41262
41025
  parse: (capture, parse, state) => {
41263
41026
  return {
@@ -41282,10 +41045,7 @@ const rules = {
41282
41045
  }
41283
41046
  },
41284
41047
  squareBracketRef: {
41285
- // $FlowFixMe[prop-missing]
41286
- // $FlowFixMe[incompatible-use]
41287
41048
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.6,
41288
- // $FlowFixMe[prop-missing]
41289
41049
  match: SimpleMarkdown__default["default"].inlineRegex(/^\[(\d+)\]( *)/),
41290
41050
  parse: (capture, parse, state) => {
41291
41051
  if (!state.firstSentenceRef) {
@@ -41311,10 +41071,7 @@ const rules = {
41311
41071
  }
41312
41072
  },
41313
41073
  highlight: {
41314
- // $FlowFixMe[prop-missing]
41315
- // $FlowFixMe[incompatible-use]
41316
41074
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.7,
41317
- // $FlowFixMe[prop-missing]
41318
41075
  match: SimpleMarkdown__default["default"].inlineRegex(/^{highlighting.start}(.+?){highlighting.end}/),
41319
41076
  parse: (capture, parse, state) => {
41320
41077
  return {
@@ -41329,10 +41086,7 @@ const rules = {
41329
41086
  }
41330
41087
  },
41331
41088
  reviewHighlight: {
41332
- // $FlowFixMe[prop-missing]
41333
- // $FlowFixMe[incompatible-use]
41334
41089
  order: SimpleMarkdown__default["default"].defaultRules.escape.order + 0.7,
41335
- // $FlowFixMe[prop-missing]
41336
41090
  match: SimpleMarkdown__default["default"].inlineRegex(/^{review-highlighting.start}(.+?){review-highlighting.end}/),
41337
41091
  parse: (capture, parse, state) => {
41338
41092
  return {
@@ -41346,21 +41100,11 @@ const rules = {
41346
41100
  }, node.content)];
41347
41101
  }
41348
41102
  },
41349
- // $FlowFixMe[prop-missing]
41350
- // $FlowFixMe[incompatible-use]
41351
41103
  strong: SimpleMarkdown__default["default"].defaultRules.strong,
41352
- // $FlowFixMe[prop-missing]
41353
- // $FlowFixMe[incompatible-use]
41354
41104
  u: SimpleMarkdown__default["default"].defaultRules.u,
41355
- // $FlowFixMe[prop-missing]
41356
- // $FlowFixMe[incompatible-use]
41357
41105
  em: SimpleMarkdown__default["default"].defaultRules.em,
41358
- // $FlowFixMe[prop-missing]
41359
- // $FlowFixMe[incompatible-use]
41360
41106
  del: SimpleMarkdown__default["default"].defaultRules.del,
41361
- text: { // $FlowFixMe[prop-missing]
41362
- // $FlowFixMe[incompatible-use]
41363
- ...SimpleMarkdown__default["default"].defaultRules.text,
41107
+ text: { ...SimpleMarkdown__default["default"].defaultRules.text,
41364
41108
 
41365
41109
  react(node, output, state) {
41366
41110
  return /*#__PURE__*/React__namespace.createElement("span", {
@@ -41370,32 +41114,54 @@ const rules = {
41370
41114
 
41371
41115
  }
41372
41116
  };
41373
- const INITIAL_PARSE_STATE = {
41374
- currentRef: [],
41375
- useRefs: true,
41376
- lastRef: 0,
41377
- lastFootnote: {
41378
- id: 0,
41379
- text: ""
41380
- }
41381
- }; // $FlowFixMe[prop-missing]
41382
-
41117
+ const START_REF_PREFIX = "start-ref-";
41118
+ const END_REF_PREFIX = "end-ref-";
41119
+ const REF_STYLE = {
41120
+ display: "inline-block",
41121
+ width: 0,
41122
+ visibility: "hidden"
41123
+ };
41124
+ const LABEL_OUTER_STYLE = {
41125
+ // for some reason we need these to keep the nbsp from wrapping when the
41126
+ // inner circle/square is display: inline-block
41127
+ display: "inline",
41128
+ whiteSpace: "nowrap"
41129
+ };
41130
+ const SQUARE_LABEL_STYLE = {
41131
+ display: "inline-block",
41132
+ color: "rgb(255, 255, 255)",
41133
+ backgroundColor: "rgb(90, 90, 90)",
41134
+ paddingLeft: 10,
41135
+ paddingRight: 10,
41136
+ userSelect: "none",
41137
+ WebkitUserSelect: "none"
41138
+ };
41139
+ const CIRCLE_LABEL_STYLE = {
41140
+ display: "inline-block",
41141
+ color: "rgb(255, 255, 255)",
41142
+ backgroundColor: "rgb(90, 90, 90)",
41143
+ userSelect: "none",
41144
+ WebkitUserSelect: "none",
41145
+ width: 22,
41146
+ height: 22,
41147
+ borderRadius: "50%",
41148
+ textAlign: "center"
41149
+ };
41383
41150
  const builtParser = SimpleMarkdown__default["default"].parserFor(rules);
41384
41151
 
41385
41152
  const parse = (source, state) => {
41386
41153
  state = state || {};
41387
41154
  const paragraphedSource = source + "\n\n";
41388
- return builtParser(paragraphedSource, ___default["default"].extend(state, INITIAL_PARSE_STATE));
41155
+ return builtParser(paragraphedSource, ___default["default"].extend(state, getInitialParseState()));
41389
41156
  };
41390
41157
 
41158
+ const output = SimpleMarkdown__default["default"].reactFor(SimpleMarkdown__default["default"].ruleOutput(rules, "react"));
41391
41159
  var PassageMarkdown = {
41392
- parse: parse,
41393
- // $FlowFixMe[prop-missing]
41394
- output: SimpleMarkdown__default["default"].reactFor( // $FlowFixMe[prop-missing]
41395
- SimpleMarkdown__default["default"].ruleOutput(rules, "react")),
41396
- START_REF_PREFIX: START_REF_PREFIX,
41397
- END_REF_PREFIX: END_REF_PREFIX,
41398
- _rulesForTesting: rules
41160
+ parse,
41161
+ output,
41162
+ START_REF_PREFIX,
41163
+ END_REF_PREFIX,
41164
+ getInitialParseState
41399
41165
  };
41400
41166
 
41401
41167
  // A fake paragraph to measure the line height of the passage. In CSS we always
@@ -41408,9 +41174,9 @@ class LineHeightMeasurer extends React__namespace.Component {
41408
41174
 
41409
41175
  _defineProperty(this, "_cachedLineHeight", void 0);
41410
41176
 
41411
- _defineProperty(this, "$body", void 0);
41177
+ _defineProperty(this, "body", void 0);
41412
41178
 
41413
- _defineProperty(this, "$end", void 0);
41179
+ _defineProperty(this, "end", void 0);
41414
41180
  }
41415
41181
 
41416
41182
  measureLineHeight() {
@@ -41422,23 +41188,30 @@ class LineHeightMeasurer extends React__namespace.Component {
41422
41188
  }
41423
41189
 
41424
41190
  forceMeasureLineHeight() {
41425
- // Add some text which magically fills an entire line.
41426
- this.$body.text(" \u0080"); // Now, the line height is the difference between the top of the
41191
+ const body = this.body;
41192
+ const end = this.end;
41193
+
41194
+ if (!body || !end) {
41195
+ return;
41196
+ } // Add some text which magically fills an entire line.
41197
+
41198
+
41199
+ body.textContent = " \u0080"; // Now, the line height is the difference between the top of the
41427
41200
  // second line and the top of the first line.
41428
41201
 
41429
- this._cachedLineHeight = getLineHeightForNode(this.$body, this.$end); // Clear out the first line so it doesn't overlap the passage.
41202
+ this._cachedLineHeight = getLineHeightForNode(body, end); // Clear out the first line so it doesn't overlap the passage.
41430
41203
 
41431
- this.$body.text("");
41204
+ body.textContent = "";
41432
41205
  }
41433
41206
 
41434
41207
  render() {
41435
41208
  return /*#__PURE__*/React__namespace.createElement("div", {
41436
41209
  className: aphrodite.css(styles$8.measurer)
41437
41210
  }, /*#__PURE__*/React__namespace.createElement("div", null, /*#__PURE__*/React__namespace.createElement("div", {
41438
- ref: e => this.$body = $__default["default"](e),
41211
+ ref: ref => this.body = ref,
41439
41212
  className: "paragraph"
41440
41213
  }), /*#__PURE__*/React__namespace.createElement("div", {
41441
- ref: e => this.$end = $__default["default"](e)
41214
+ ref: ref => this.end = ref
41442
41215
  })));
41443
41216
  }
41444
41217
 
@@ -41457,9 +41230,11 @@ class Passage extends React__namespace.Component {
41457
41230
  constructor() {
41458
41231
  super(...arguments);
41459
41232
 
41460
- _defineProperty(this, "_onResize", void 0);
41233
+ _defineProperty(this, "_contentRef", void 0);
41461
41234
 
41462
- _defineProperty(this, "_lineHeightMeasurer", void 0);
41235
+ _defineProperty(this, "_lineHeightMeasurerRef", void 0);
41236
+
41237
+ _defineProperty(this, "_onResize", void 0);
41463
41238
 
41464
41239
  _defineProperty(this, "_stylesAppiedTimer", void 0);
41465
41240
 
@@ -41480,6 +41255,8 @@ class Passage extends React__namespace.Component {
41480
41255
  this._updateState();
41481
41256
 
41482
41257
  this._onResize = ___default["default"].throttle(() => {
41258
+ var _this$_lineHeightMeas;
41259
+
41483
41260
  // If we're rendering JIPT text, we won't have line numbers or a
41484
41261
  // line height measurer, so skip handling this resize.
41485
41262
  if (this.shouldRenderJipt()) {
@@ -41490,7 +41267,7 @@ class Passage extends React__namespace.Component {
41490
41267
  // listen for resize events.
41491
41268
 
41492
41269
 
41493
- this._lineHeightMeasurer.forceMeasureLineHeight();
41270
+ (_this$_lineHeightMeas = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas === void 0 ? void 0 : _this$_lineHeightMeas.forceMeasureLineHeight();
41494
41271
 
41495
41272
  this._updateState();
41496
41273
  }, 500);
@@ -41553,9 +41330,8 @@ class Passage extends React__namespace.Component {
41553
41330
  }
41554
41331
 
41555
41332
  _measureLines() {
41556
- // eslint-disable-next-line react/no-string-refs
41557
- const $renderer = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.content));
41558
- const contentsHeight = $renderer.height();
41333
+ const renderer = ReactDOM__default["default"].findDOMNode(this._contentRef);
41334
+ const contentsHeight = $__default["default"](renderer).height();
41559
41335
 
41560
41336
  const lineHeight = this._getLineHeight();
41561
41337
 
@@ -41582,7 +41358,9 @@ class Passage extends React__namespace.Component {
41582
41358
  }
41583
41359
 
41584
41360
  _getLineHeight() {
41585
- return this._lineHeightMeasurer.measureLineHeight();
41361
+ var _this$_lineHeightMeas2;
41362
+
41363
+ return ((_this$_lineHeightMeas2 = this._lineHeightMeasurerRef) === null || _this$_lineHeightMeas2 === void 0 ? void 0 : _this$_lineHeightMeas2.measureLineHeight()) || 0;
41586
41364
  }
41587
41365
 
41588
41366
  getLineCount() {
@@ -41598,12 +41376,16 @@ class Passage extends React__namespace.Component {
41598
41376
  * These are functions to support the passage refs inter-widgets feature
41599
41377
  * where other widgets can fetch the line numbers of a reference inside of
41600
41378
  * a passage.
41379
+ *
41380
+ * todo(matthewc): The refs are created by PassageMarkdown's refStart and refEnd,
41381
+ * somehow bubbling up to Passage's `this.refs`. This runs against
41382
+ * current best practices for refs by using string refs, but also
41383
+ * by breaking our expectation of explicit data flow.
41601
41384
  */
41602
41385
 
41603
41386
 
41604
41387
  _getStartRefLineNumber(referenceNumber) {
41605
- const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41606
-
41388
+ const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41607
41389
  const ref = this.refs[refRef];
41608
41390
 
41609
41391
  if (!ref) {
@@ -41626,8 +41408,7 @@ class Passage extends React__namespace.Component {
41626
41408
  }
41627
41409
 
41628
41410
  _getEndRefLineNumber(referenceNumber) {
41629
- const refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41630
-
41411
+ const refRef = PassageMarkdown.END_REF_PREFIX + referenceNumber;
41631
41412
  const ref = this.refs[refRef];
41632
41413
 
41633
41414
  if (!ref) {
@@ -41664,9 +41445,8 @@ class Passage extends React__namespace.Component {
41664
41445
  }
41665
41446
 
41666
41447
  _convertPosToLineNumber(absoluteVPos) {
41667
- // eslint-disable-next-line react/no-string-refs
41668
- const $content = $__default["default"](ReactDOM__default["default"].findDOMNode(this.refs.content));
41669
- const relativeVPos = absoluteVPos - $content.offset().top;
41448
+ const content = ReactDOM__default["default"].findDOMNode(this._contentRef);
41449
+ const relativeVPos = absoluteVPos - $__default["default"](content).offset().top;
41670
41450
 
41671
41451
  const lineHeight = this._getLineHeight();
41672
41452
 
@@ -41675,8 +41455,7 @@ class Passage extends React__namespace.Component {
41675
41455
  }
41676
41456
 
41677
41457
  _getRefContent(referenceNumber) {
41678
- const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber; // eslint-disable-next-line react/no-string-refs
41679
-
41458
+ const refRef = PassageMarkdown.START_REF_PREFIX + referenceNumber;
41680
41459
  const ref = this.refs[refRef];
41681
41460
 
41682
41461
  if (!ref) {
@@ -41782,12 +41561,9 @@ class Passage extends React__namespace.Component {
41782
41561
  onSerializedHighlightsUpdate: this._handleSerializedHighlightsUpdate,
41783
41562
  serializedHighlights: this.props.highlights
41784
41563
  }, /*#__PURE__*/React__namespace.createElement("div", {
41785
- ref: "content"
41786
- }, /*#__PURE__*/React__namespace.createElement(LineHeightMeasurer // TODO(mdr): We found a new Flow error when upgrading:
41787
- // "e (null) This type is incompatible with this._lineHeightMeasurer (LineHeightMeasurer)"
41788
- // $FlowFixMe[incompatible-type](0.52.0->0.53.0)
41789
- , {
41790
- ref: e => this._lineHeightMeasurer = e
41564
+ ref: ref => this._contentRef = ref
41565
+ }, /*#__PURE__*/React__namespace.createElement(LineHeightMeasurer, {
41566
+ ref: ref => this._lineHeightMeasurerRef = ref
41791
41567
  }), PassageMarkdown.output(parsed)));
41792
41568
  }
41793
41569
 
@@ -41830,10 +41606,7 @@ class Passage extends React__namespace.Component {
41830
41606
  });
41831
41607
  }
41832
41608
 
41833
- const parseState = {
41834
- firstSentenceRef: null,
41835
- firstQuestionRef: null
41836
- }; // Replace the vertical double quote characters quoting text with
41609
+ const parseState = PassageMarkdown.getInitialParseState(); // Replace the vertical double quote characters quoting text with
41837
41610
  // an unicode left and right double quote characters. This would
41838
41611
  // avoid rendering right or left unicode double quotes on both
41839
41612
  // ends of a sentence wrapped by double quotes.