@khanacademy/perseus 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/es/index.css +15 -8
  3. package/dist/es/index.js +225 -194
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +15 -8
  6. package/dist/index.js +229 -196
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/number-input.jsx +2 -2
  10. package/src/components/simple-keypad-input.jsx +18 -21
  11. package/src/index.js +1 -1
  12. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +356 -65
  13. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  14. package/src/perseus-types.js +18 -7
  15. package/src/styles/perseus-renderer.less +8 -2
  16. package/src/styles/widgets/passage.less +9 -5
  17. package/src/util/answer-types.js +2 -3
  18. package/src/util/math.js +3 -10
  19. package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
  20. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  21. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +226 -65
  22. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +2581 -679
  23. package/src/widgets/__tests__/group_test.jsx +4 -4
  24. package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
  25. package/src/widgets/__tests__/passage_test.jsx +81 -0
  26. package/src/widgets/__tests__/radio/base-radio_test.jsx +55 -7
  27. package/src/widgets/__tests__/radio/choice_test.jsx +164 -63
  28. package/src/widgets/__tests__/radio_test.jsx +39 -15
  29. package/src/widgets/definition.jsx +1 -1
  30. package/src/widgets/group.jsx +84 -102
  31. package/src/widgets/input-number.jsx +6 -1
  32. package/src/widgets/numeric-input.jsx +87 -80
  33. package/src/widgets/radio/base-radio.jsx +3 -1
  34. package/src/widgets/radio/choice-a11y-text.js +62 -0
  35. package/src/widgets/radio/choice-icon/choice-icon.jsx +0 -68
  36. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +5 -10
  37. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +3 -12
  38. package/src/widgets/radio/choice.jsx +46 -3
package/dist/index.js CHANGED
@@ -1417,8 +1417,7 @@ const KhanAnswerTypes = {
1417
1417
  */
1418
1418
  number: {
1419
1419
  convertToPredicate: function (correctAnswer, options) {
1420
- // TODO(alpert): Don't think this $.trim is necessary
1421
- const correctFloat = parseFloat($__default["default"].trim(correctAnswer));
1420
+ const correctFloat = parseFloat(correctAnswer);
1422
1421
  return [function (guess, maxError) {
1423
1422
  return Math.abs(guess - correctFloat) < maxError;
1424
1423
  }, $__default["default"].extend({}, options, {
@@ -22850,35 +22849,36 @@ _defineProperty(PossibleAnswers, "propTypes", {
22850
22849
  answers: PropTypes__default["default"].arrayOf(PropTypes__default["default"].string)
22851
22850
  });
22852
22851
 
22853
- const SimpleKeypadInput = createReactClass__default["default"]({
22854
- displayName: "SimpleKeypadInput",
22855
- propTypes: {
22856
- keypadElement: mathInput.keypadElementPropType,
22857
- onFocus: PropTypes__default["default"].func,
22858
- value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
22859
- },
22860
- componentDidMount: function () {
22852
+ class SimpleKeypadInput extends React__namespace.Component {
22853
+ constructor() {
22854
+ super(...arguments);
22855
+
22856
+ _defineProperty(this, "_isMounted", false);
22857
+ }
22858
+
22859
+ componentDidMount() {
22861
22860
  // TODO(scottgrant): This is a hack to remove the deprecated call to
22862
22861
  // this.isMounted() but is still considered an anti-pattern.
22863
22862
  this._isMounted = true;
22864
- },
22865
- componentWillUnmount: function () {
22863
+ }
22864
+
22865
+ componentWillUnmount() {
22866
22866
  this._isMounted = false;
22867
- },
22867
+ }
22868
22868
 
22869
22869
  focus() {
22870
22870
  // $FlowFixMe[object-this-reference]
22871
22871
  this.refs.input.focus(); // eslint-disable-line react/no-string-refs
22872
- },
22872
+ }
22873
22873
 
22874
22874
  blur() {
22875
22875
  // $FlowFixMe[object-this-reference]
22876
22876
  this.refs.input.blur(); // eslint-disable-line react/no-string-refs
22877
- },
22877
+ }
22878
22878
 
22879
- getValue: function () {
22879
+ getValue() {
22880
22880
  return this.props.value;
22881
- },
22881
+ }
22882
22882
 
22883
22883
  render() {
22884
22884
  // $FlowFixMe[object-this-reference]
@@ -22915,7 +22915,12 @@ const SimpleKeypadInput = createReactClass__default["default"]({
22915
22915
  }, rest));
22916
22916
  }
22917
22917
 
22918
- });
22918
+ }
22919
+ SimpleKeypadInput.propTypes = {
22920
+ keypadElement: mathInput.keypadElementPropType,
22921
+ onFocus: PropTypes__default["default"].func,
22922
+ value: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number])
22923
+ };
22919
22924
 
22920
22925
  const ParseTex$1 = TexWrangler.parseTex;
22921
22926
  const answerTypes = {
@@ -23170,9 +23175,13 @@ class InputNumber extends React__namespace.Component {
23170
23175
 
23171
23176
  if (rubric.answerType == null) {
23172
23177
  rubric.answerType = "number";
23173
- }
23178
+ } // note(matthewc): this will get immediately parsed again by
23179
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
23180
+ // expected here
23181
+
23174
23182
 
23175
- const val = KhanAnswerTypes.number.createValidatorFunctional(rubric.value, {
23183
+ const stringValue = "".concat(rubric.value);
23184
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
23176
23185
  simplify: rubric.simplify,
23177
23186
  inexact: rubric.inexact || undefined,
23178
23187
  maxError: rubric.maxError,
@@ -23298,6 +23307,8 @@ class NumericInput extends React__namespace.Component {
23298
23307
  constructor() {
23299
23308
  super(...arguments);
23300
23309
 
23310
+ _defineProperty(this, "inputRef", void 0);
23311
+
23301
23312
  _defineProperty(this, "state", {
23302
23313
  // keeps track of the other set of values when switching
23303
23314
  // between 0 and finite solutions
@@ -23316,9 +23327,8 @@ class NumericInput extends React__namespace.Component {
23316
23327
  correct = score.type === "points" && score.earned === score.total;
23317
23328
 
23318
23329
  if (!correct) {
23319
- const correctAnswers = ___default["default"].filter(rubric.answers, answer => answer.status === "correct");
23320
-
23321
- const answerStrings = ___default["default"].map(correctAnswers, answer => {
23330
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23331
+ const answerStrings = correctAnswers.map(answer => {
23322
23332
  // Figure out how this answer is supposed to be
23323
23333
  // displayed
23324
23334
  let format = "decimal";
@@ -23339,7 +23349,6 @@ class NumericInput extends React__namespace.Component {
23339
23349
 
23340
23350
  return answerString;
23341
23351
  });
23342
-
23343
23352
  answerBlurb = /*#__PURE__*/React__namespace.createElement(PossibleAnswers, {
23344
23353
  answers: answerStrings
23345
23354
  });
@@ -23353,7 +23362,7 @@ class NumericInput extends React__namespace.Component {
23353
23362
  var _this$props$answerFor;
23354
23363
 
23355
23364
  // if the set of specified forms are empty, allow all forms
23356
- const forms = ((_this$props$answerFor = this.props.answerForms) === null || _this$props$answerFor === void 0 ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : ___default["default"].map(___default["default"].keys(formExamples), name => {
23365
+ const forms = ((_this$props$answerFor = this.props.answerForms) === null || _this$props$answerFor === void 0 ? void 0 : _this$props$answerFor.length) !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
23357
23366
  return {
23358
23367
  name: name,
23359
23368
  simplify: "required"
@@ -23379,8 +23388,7 @@ class NumericInput extends React__namespace.Component {
23379
23388
 
23380
23389
  const answerFormNames = ___default["default"].uniq((_this$props$answerFor3 = this.props.answerForms) === null || _this$props$answerFor3 === void 0 ? void 0 : _this$props$answerFor3.map(form => form.name));
23381
23390
 
23382
- const allFormsAccepted = answerFormNames.length >= ___default["default"].size(formExamples);
23383
-
23391
+ const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
23384
23392
  return !noFormsAccepted && !allFormsAccepted;
23385
23393
  });
23386
23394
 
@@ -23389,20 +23397,22 @@ class NumericInput extends React__namespace.Component {
23389
23397
  });
23390
23398
 
23391
23399
  _defineProperty(this, "focus", () => {
23392
- // eslint-disable-next-line react/no-string-refs
23393
- this.refs.input.focus();
23400
+ var _this$inputRef;
23401
+
23402
+ (_this$inputRef = this.inputRef) === null || _this$inputRef === void 0 ? void 0 : _this$inputRef.focus();
23394
23403
  return true;
23395
23404
  });
23396
23405
 
23397
- _defineProperty(this, "focusInputPath", inputPath => {
23398
- /* istanbul ignore next */
23399
- // eslint-disable-next-line react/no-string-refs
23400
- this.refs.input.focus();
23406
+ _defineProperty(this, "focusInputPath", () => {
23407
+ var _this$inputRef2;
23408
+
23409
+ (_this$inputRef2 = this.inputRef) === null || _this$inputRef2 === void 0 ? void 0 : _this$inputRef2.focus();
23401
23410
  });
23402
23411
 
23403
- _defineProperty(this, "blurInputPath", inputPath => {
23404
- // eslint-disable-next-line react/no-string-refs
23405
- this.refs.input.blur();
23412
+ _defineProperty(this, "blurInputPath", () => {
23413
+ var _this$inputRef3;
23414
+
23415
+ (_this$inputRef3 = this.inputRef) === null || _this$inputRef3 === void 0 ? void 0 : _this$inputRef3.blur();
23406
23416
  });
23407
23417
 
23408
23418
  _defineProperty(this, "getInputPaths", () => {
@@ -23470,9 +23480,8 @@ class NumericInput extends React__namespace.Component {
23470
23480
  }
23471
23481
 
23472
23482
  static getOneCorrectAnswerFromRubric(rubric) {
23473
- const correctAnswers = ___default["default"].filter(rubric.answers, answer => answer.status === "correct");
23474
-
23475
- const answerStrings = ___default["default"].map(correctAnswers, answer => {
23483
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23484
+ const answerStrings = correctAnswers.map(answer => {
23476
23485
  // Figure out how this answer is supposed to be
23477
23486
  // displayed
23478
23487
  let format = "decimal";
@@ -23502,31 +23511,30 @@ class NumericInput extends React__namespace.Component {
23502
23511
  }
23503
23512
 
23504
23513
  static validate(useInput, rubric) {
23505
- const allAnswerForms = ___default["default"].pluck(answerFormButtons, "value");
23506
-
23507
- const createValidator = answer => KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
23508
- message: answer.message,
23509
- simplify: answer.status === "correct" ? answer.simplify : "optional",
23510
- inexact: true,
23511
- // TODO(merlob) backfill / delete
23512
- maxError: answer.maxError,
23513
- forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23514
- }); // We may have received TeX; try to parse it before grading.
23514
+ const allAnswerForms = answerFormButtons.map(e => e["value"]);
23515
+
23516
+ const createValidator = answer => {
23517
+ const stringAnswer = "".concat(answer.value);
23518
+ return KhanAnswerTypes.number.createValidatorFunctional(stringAnswer, {
23519
+ message: answer.message,
23520
+ simplify: answer.status === "correct" ? answer.simplify : "optional",
23521
+ inexact: true,
23522
+ // TODO(merlob) backfill / delete
23523
+ maxError: answer.maxError,
23524
+ forms: answer.strict && answer.answerForms && answer.answerForms.length !== 0 ? answer.answerForms : allAnswerForms
23525
+ });
23526
+ }; // We may have received TeX; try to parse it before grading.
23515
23527
  // If `currentValue` is not TeX, this should be a no-op.
23516
23528
 
23517
23529
 
23518
23530
  const currentValue = ParseTex(useInput.currentValue);
23519
-
23520
- const correctAnswers = ___default["default"].where(rubric.answers, {
23521
- status: "correct"
23522
- });
23523
-
23531
+ const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
23524
23532
  const normalizedAnswerExpected = correctAnswers.every(answer => Math.abs(answer.value) <= 1); // Look through all correct answers for one that matches either
23525
23533
  // precisely or approximately and return the appropriate message:
23526
23534
  // - if precise, return the message that the answer came with
23527
23535
  // - if it needs to be simplified, etc., show that message
23528
23536
 
23529
- let result = ___default["default"].find(___default["default"].map(correctAnswers, answer => {
23537
+ let result = correctAnswers.map(answer => {
23530
23538
  // The coefficient is an attribute of the widget
23531
23539
  let localValue = currentValue;
23532
23540
 
@@ -23540,22 +23548,17 @@ class NumericInput extends React__namespace.Component {
23540
23548
 
23541
23549
  const validate = createValidator(answer);
23542
23550
  return validate(maybeParsePercentInput(localValue, normalizedAnswerExpected));
23543
- }), match => match.correct || match.empty);
23551
+ }).find(match => match.correct || match.empty);
23544
23552
 
23545
23553
  if (!result) {
23546
23554
  // Otherwise, if the guess is not correct
23547
- const otherAnswers = [].concat(___default["default"].where(rubric.answers, {
23548
- status: "ungraded"
23549
- }), ___default["default"].where(rubric.answers, {
23550
- status: "wrong"
23551
- })); // Look through all other answers and if one matches either
23555
+ const otherAnswers = [].concat(rubric.answers.filter(answer => answer.status === "ungraded"), rubric.answers.filter(answer => answer.status === "wrong")); // Look through all other answers and if one matches either
23552
23556
  // precisely or approximately return the answer's message
23553
23557
 
23554
- const match = ___default["default"].find(otherAnswers, answer => {
23558
+ const match = otherAnswers.find(answer => {
23555
23559
  const validate = createValidator(answer);
23556
23560
  return validate(maybeParsePercentInput(currentValue, normalizedAnswerExpected)).correct;
23557
23561
  });
23558
-
23559
23562
  result = {
23560
23563
  empty: match ? match.status === "ungraded" : false,
23561
23564
  correct: match ? match.status === "correct" : false,
@@ -23602,9 +23605,8 @@ class NumericInput extends React__namespace.Component {
23602
23605
 
23603
23606
  if (this.props.apiOptions.customKeypad) {
23604
23607
  // TODO(charlie): Support "Review Mode".
23605
- return maybeRightAlignKeypadInput( /*#__PURE__*/React__namespace.createElement(SimpleKeypadInput // eslint-disable-next-line react/no-string-refs
23606
- , {
23607
- ref: "input",
23608
+ return maybeRightAlignKeypadInput( /*#__PURE__*/React__namespace.createElement(SimpleKeypadInput, {
23609
+ ref: ref => this.inputRef = ref,
23608
23610
  value: this.props.currentValue,
23609
23611
  keypadElement: this.props.keypadElement,
23610
23612
  onChange: this.handleChange,
@@ -23613,9 +23615,8 @@ class NumericInput extends React__namespace.Component {
23613
23615
  }));
23614
23616
  }
23615
23617
 
23616
- const input = /*#__PURE__*/React__namespace.createElement(InputWithExamples // eslint-disable-next-line react/no-string-refs
23617
- , {
23618
- ref: "input",
23618
+ const input = /*#__PURE__*/React__namespace.createElement(InputWithExamples, {
23619
+ ref: ref => this.inputRef = ref,
23619
23620
  value: this.props.currentValue,
23620
23621
  onChange: this.handleChange,
23621
23622
  className: classNames__default["default"](classes),
@@ -23682,7 +23683,7 @@ const unionAnswerForms = function (answerFormsList) {
23682
23683
  // two elements are equal, and returns a list of unique elements. This is
23683
23684
  // just a helper function here, but works generally.
23684
23685
  const uniqueBy = function (list, iteratee) {
23685
- return ___default["default"].reduce(list, (uniqueList, element) => {
23686
+ return list.reduce((uniqueList, element) => {
23686
23687
  // For each element, decide whether it's already in the list of
23687
23688
  // unique items.
23688
23689
  const inList = ___default["default"].find(uniqueList, iteratee.bind(null, element));
@@ -23696,13 +23697,13 @@ const unionAnswerForms = function (answerFormsList) {
23696
23697
  }; // Pull out all of the forms from the different lists.
23697
23698
 
23698
23699
 
23699
- const allForms = ___default["default"].flatten(answerFormsList); // Pull out the unique forms using uniqueBy.
23700
-
23700
+ const allForms = answerFormsList.flat(); // Pull out the unique forms using uniqueBy.
23701
23701
 
23702
23702
  const uniqueForms = uniqueBy(allForms, ___default["default"].isEqual); // Sort them by the order they appear in the `formExamples` list.
23703
23703
 
23704
+ const formExampleKeys = Object.keys(formExamples);
23704
23705
  return ___default["default"].sortBy(uniqueForms, form => {
23705
- return ___default["default"].keys(formExamples).indexOf(form.name);
23706
+ return formExampleKeys.indexOf(form.name);
23706
23707
  });
23707
23708
  };
23708
23709
  // This function checks if the user inputted a percent value, parsing
@@ -23711,7 +23712,7 @@ const unionAnswerForms = function (answerFormsList) {
23711
23712
  // can accept several input forms with or without "%", the decision
23712
23713
  // to parse based on the presence of "%" in the input, is so that we
23713
23714
  // don't accidently scale the user typed value before grading, CP-930.
23714
- const maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23715
+ function maybeParsePercentInput(inputValue, normalizedAnswerExpected) {
23715
23716
  // If the input value is not a string ending with "%", then there's
23716
23717
  // nothing more to do. The value will be graded as inputted by user.
23717
23718
  if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
@@ -23736,13 +23737,13 @@ const maybeParsePercentInput = (inputValue, normalizedAnswerExpected) => {
23736
23737
 
23737
23738
 
23738
23739
  return value;
23739
- };
23740
+ }
23740
23741
 
23741
23742
  const propsTransform = function (widgetOptions) {
23742
23743
  const rendererProps = ___default["default"].extend(___default["default"].omit(widgetOptions, "answers"), {
23743
23744
  answerForms: unionAnswerForms( // Pull out the name of each form and whether that form has
23744
23745
  // required simplification.
23745
- ___default["default"].map(widgetOptions.answers, answer => {
23746
+ widgetOptions.answers.map(answer => {
23746
23747
  return ___default["default"].map(answer.answerForms, form => {
23747
23748
  return {
23748
23749
  simplify: answer.simplify,
@@ -24285,6 +24286,66 @@ _defineProperty(Icon, "defaultProps", {
24285
24286
  color: "currentColor"
24286
24287
  });
24287
24288
 
24289
+ function getA11yText(letter, checked, correct, crossedOut, showCorrectness) {
24290
+ // There are two pieces of metadata we want to add to each a11yText:
24291
+ // whether the answer was checked/crossed-out/neither, and whether the
24292
+ // answer is correct/incorrect/not-yet-revealed.
24293
+ //
24294
+ // Translation is tricky for cross-product situations like this, so
24295
+ // we've just enumerated all 9 possibilities as separate strings.
24296
+ if (showCorrectness && correct) {
24297
+ if (checked) {
24298
+ return i18n__namespace._("(Choice %(letter)s, Checked, Correct)", {
24299
+ letter
24300
+ });
24301
+ }
24302
+
24303
+ if (crossedOut) {
24304
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Correct)", {
24305
+ letter
24306
+ });
24307
+ }
24308
+
24309
+ return i18n__namespace._("(Choice %(letter)s, Correct)", {
24310
+ letter
24311
+ });
24312
+ }
24313
+
24314
+ if (showCorrectness && !correct) {
24315
+ if (checked) {
24316
+ return i18n__namespace._("(Choice %(letter)s, Checked, Incorrect)", {
24317
+ letter
24318
+ });
24319
+ }
24320
+
24321
+ if (crossedOut) {
24322
+ return i18n__namespace._("(Choice %(letter)s, Crossed out, Incorrect)", {
24323
+ letter
24324
+ });
24325
+ }
24326
+
24327
+ return i18n__namespace._("(Choice %(letter)s, Incorrect)", {
24328
+ letter
24329
+ });
24330
+ }
24331
+
24332
+ if (checked) {
24333
+ return i18n__namespace._("(Choice %(letter)s, Checked)", {
24334
+ letter
24335
+ });
24336
+ }
24337
+
24338
+ if (crossedOut) {
24339
+ return i18n__namespace._("(Choice %(letter)s, Crossed out)", {
24340
+ letter
24341
+ });
24342
+ }
24343
+
24344
+ return i18n__namespace._("(Choice %(letter)s)", {
24345
+ letter
24346
+ });
24347
+ }
24348
+
24288
24349
  /**
24289
24350
  * Given a choice's position in the radio widget, return the corresponding
24290
24351
  * "choice letter". (For example, in English, `getChoiceLetter(0)` is "A",
@@ -24471,7 +24532,6 @@ function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, pri
24471
24532
 
24472
24533
  function LibraryChoiceIcon(props) {
24473
24534
  const {
24474
- a11yText,
24475
24535
  checked,
24476
24536
  crossedOut,
24477
24537
  showCorrectness,
@@ -24499,15 +24559,11 @@ function LibraryChoiceIcon(props) {
24499
24559
  // radio icon
24500
24560
  ,
24501
24561
  "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
24562
  }, /*#__PURE__*/React__namespace.createElement(ChoiceInner, {
24507
24563
  letter: letter,
24508
24564
  showCorrectness: showCorrectness,
24509
24565
  correct: correct
24510
- })))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24566
+ }))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24511
24567
  color: dynamicStyles.borderColor
24512
24568
  }));
24513
24569
  }
@@ -24597,7 +24653,6 @@ function constructSATStyles(reviewMode, correct, checked, multipleSelect) {
24597
24653
  function SATChoiceIcon(props) {
24598
24654
  const {
24599
24655
  letter,
24600
- a11yText,
24601
24656
  crossedOut,
24602
24657
  reviewMode,
24603
24658
  correct,
@@ -24625,11 +24680,7 @@ function SATChoiceIcon(props) {
24625
24680
  color
24626
24681
  },
24627
24682
  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, {
24683
+ }, /*#__PURE__*/React__namespace.createElement("span", null, letter)), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
24633
24684
  color: borderColor,
24634
24685
  sat: true
24635
24686
  }));
@@ -24666,66 +24717,6 @@ const styles$u = aphrodite.StyleSheet.create({
24666
24717
 
24667
24718
  /* Component for rendering a letter icon in radio choice */
24668
24719
 
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
24720
  function ChoiceIcon(props) {
24730
24721
  const {
24731
24722
  pos,
@@ -24744,12 +24735,10 @@ function ChoiceIcon(props) {
24744
24735
  transparentBackground
24745
24736
  } = props;
24746
24737
  const letter = getChoiceLetter(pos);
24747
- const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
24748
24738
 
24749
24739
  if (product === "sat") {
24750
24740
  return /*#__PURE__*/React__namespace.createElement(SATChoiceIcon, {
24751
24741
  letter: letter,
24752
- a11yText: a11yText,
24753
24742
  reviewMode: reviewMode,
24754
24743
  checked: checked,
24755
24744
  correct: correct,
@@ -24760,7 +24749,6 @@ function ChoiceIcon(props) {
24760
24749
 
24761
24750
  return /*#__PURE__*/React__namespace.createElement(LibraryChoiceIcon, {
24762
24751
  letter: letter,
24763
- a11yText: a11yText,
24764
24752
  reviewMode: reviewMode,
24765
24753
  checked: checked,
24766
24754
  crossedOut: crossedOut,
@@ -24871,6 +24859,15 @@ const ellipsisHorizontalIcon = {
24871
24859
  width: 100,
24872
24860
  height: 27.284
24873
24861
  };
24862
+ // Note(TB): Received errors when using useUniqueIdWithMock
24863
+ // so created this workaround function. Will update when
24864
+ // useUniqueIdWithMock is available.
24865
+ // https://khanacademy.atlassian.net/browse/FEI-4861?atlOrigin=eyJpIjoiNDJlZWMwNjM1NWJhNDBkMWFjY2FmN2I0ZjcxZmQxOGUiLCJwIjoiaiJ9
24866
+ let id = 0;
24867
+
24868
+ function uniqueId() {
24869
+ return "choice-".concat(id++);
24870
+ }
24874
24871
 
24875
24872
  function Choice(props) {
24876
24873
  const {
@@ -24927,6 +24924,9 @@ function Choice(props) {
24927
24924
  // content library.
24928
24925
 
24929
24926
  const showDimmed = !apiOptions.satStyling && !reviewMode && apiOptions.readOnly || crossedOut;
24927
+ const letter = getChoiceLetter(pos);
24928
+ const a11yText = getA11yText(letter, checked, correct, crossedOut, showCorrectness);
24929
+ const choiceId = uniqueId();
24930
24930
  return /*#__PURE__*/React__namespace.createElement("div", {
24931
24931
  style: {
24932
24932
  dispay: "flex",
@@ -24939,7 +24939,26 @@ function Choice(props) {
24939
24939
  width: "100%",
24940
24940
  opacity: showDimmed ? 0.5 : 1.0
24941
24941
  }
24942
- }, /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
24942
+ }, /*#__PURE__*/React__namespace.createElement("div", {
24943
+ className: "perseus-sr-only"
24944
+ }, /*#__PURE__*/React__namespace.createElement("input", {
24945
+ type: multipleSelect ? "checkbox" : "radio",
24946
+ id: choiceId,
24947
+ checked: checked,
24948
+ onClick: () => {
24949
+ // If we're checking a crossed-out option, let's
24950
+ // also uncross it.
24951
+ sendChange({
24952
+ checked: !checked,
24953
+ crossedOut: false
24954
+ });
24955
+ },
24956
+ onChange: () => {},
24957
+ disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
24958
+ tabIndex: -1
24959
+ }), /*#__PURE__*/React__namespace.createElement("label", {
24960
+ htmlFor: choiceId
24961
+ }, a11yText, " \xA0 ", content)), /*#__PURE__*/React__namespace.createElement(Clickable__default["default"], {
24943
24962
  onClick: () => {
24944
24963
  // If we're checking a crossed-out option, let's
24945
24964
  // also uncross it.
@@ -24950,13 +24969,11 @@ function Choice(props) {
24950
24969
  },
24951
24970
  className: descriptionClassName,
24952
24971
  disabled: disabled || apiOptions.staticRender || apiOptions.readOnly,
24953
- "aria-label": "Select Choice ".concat(getChoiceLetter(pos)),
24954
- "aria-checked": checked ? "true" : "false",
24955
- role: "checkbox",
24956
24972
  style: {
24957
24973
  flex: 1
24958
24974
  },
24959
- ref: forwardedRef
24975
+ ref: forwardedRef,
24976
+ "aria-hidden": "true"
24960
24977
  }, _ref => {
24961
24978
  var _apiOptions$styling;
24962
24979
 
@@ -25297,7 +25314,8 @@ function BaseRadio(props) {
25297
25314
  }, /*#__PURE__*/React__namespace.createElement("legend", {
25298
25315
  className: "perseus-sr-only"
25299
25316
  }, instructions), shouldShowInstructions && /*#__PURE__*/React__namespace.createElement("div", {
25300
- className: instructionsClassName
25317
+ className: instructionsClassName,
25318
+ "aria-hidden": "true"
25301
25319
  }, instructions), /*#__PURE__*/React__namespace.createElement("ul", {
25302
25320
  className: className,
25303
25321
  style: {
@@ -26574,7 +26592,7 @@ class Definition extends React__namespace.Component {
26574
26592
  }, /*#__PURE__*/React__namespace.createElement("span", {
26575
26593
  className: "perseus-widget-definition"
26576
26594
  }, /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
26577
- size: "small",
26595
+ size: "medium",
26578
26596
  kind: "tertiary",
26579
26597
  onClick: () => {
26580
26598
  this.props.trackInteraction();
@@ -28783,6 +28801,8 @@ class Group extends React__namespace.Component {
28783
28801
  super(...arguments);
28784
28802
  _this = this;
28785
28803
 
28804
+ _defineProperty(this, "rendererRef", void 0);
28805
+
28786
28806
  _defineProperty(this, "change", function () {
28787
28807
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
28788
28808
  args[_key] = arguments[_key];
@@ -28792,56 +28812,66 @@ class Group extends React__namespace.Component {
28792
28812
  });
28793
28813
 
28794
28814
  _defineProperty(this, "getUserInput", () => {
28795
- // eslint-disable-next-line react/no-string-refs
28796
- return this.refs.renderer.getUserInput();
28815
+ var _this$rendererRef;
28816
+
28817
+ return (_this$rendererRef = this.rendererRef) === null || _this$rendererRef === void 0 ? void 0 : _this$rendererRef.getUserInput();
28797
28818
  });
28798
28819
 
28799
28820
  _defineProperty(this, "getSerializedState", () => {
28800
- // eslint-disable-next-line react/no-string-refs
28801
- return this.refs.renderer.getSerializedState();
28821
+ var _this$rendererRef2;
28822
+
28823
+ return (_this$rendererRef2 = this.rendererRef) === null || _this$rendererRef2 === void 0 ? void 0 : _this$rendererRef2.getSerializedState();
28802
28824
  });
28803
28825
 
28804
28826
  _defineProperty(this, "restoreSerializedState", (state, callback) => {
28805
- // eslint-disable-next-line react/no-string-refs
28806
- this.refs.renderer.restoreSerializedState(state, callback); // Tell our renderer that we have no props to change
28827
+ var _this$rendererRef3;
28828
+
28829
+ (_this$rendererRef3 = this.rendererRef) === null || _this$rendererRef3 === void 0 ? void 0 : _this$rendererRef3.restoreSerializedState(state, callback); // Tell our renderer that we have no props to change
28807
28830
  // (all our changes were in state):
28808
28831
 
28809
28832
  return null;
28810
28833
  });
28811
28834
 
28812
28835
  _defineProperty(this, "simpleValidate", rubric => {
28813
- // eslint-disable-next-line react/no-string-refs
28814
- return this.refs.renderer.score();
28836
+ var _this$rendererRef4;
28837
+
28838
+ return (_this$rendererRef4 = this.rendererRef) === null || _this$rendererRef4 === void 0 ? void 0 : _this$rendererRef4.score();
28815
28839
  });
28816
28840
 
28817
28841
  _defineProperty(this, "getInputPaths", () => {
28818
- // eslint-disable-next-line react/no-string-refs
28819
- return this.refs.renderer.getInputPaths();
28842
+ var _this$rendererRef5;
28843
+
28844
+ return (_this$rendererRef5 = this.rendererRef) === null || _this$rendererRef5 === void 0 ? void 0 : _this$rendererRef5.getInputPaths();
28820
28845
  });
28821
28846
 
28822
- _defineProperty(this, "setInputValue", (path, newValue, cb) => {
28823
- // eslint-disable-next-line react/no-string-refs
28824
- return this.refs.renderer.setInputValue(path, newValue, cb);
28847
+ _defineProperty(this, "setInputValue", (path, newValue, callback) => {
28848
+ var _this$rendererRef6;
28849
+
28850
+ return (_this$rendererRef6 = this.rendererRef) === null || _this$rendererRef6 === void 0 ? void 0 : _this$rendererRef6.setInputValue(path, newValue, callback);
28825
28851
  });
28826
28852
 
28827
28853
  _defineProperty(this, "focus", () => {
28828
- // eslint-disable-next-line react/no-string-refs
28829
- return this.refs.renderer.focus();
28854
+ var _this$rendererRef7;
28855
+
28856
+ return (_this$rendererRef7 = this.rendererRef) === null || _this$rendererRef7 === void 0 ? void 0 : _this$rendererRef7.focus();
28830
28857
  });
28831
28858
 
28832
28859
  _defineProperty(this, "focusInputPath", path => {
28833
- // eslint-disable-next-line react/no-string-refs
28834
- this.refs.renderer.focusPath(path);
28860
+ var _this$rendererRef8;
28861
+
28862
+ (_this$rendererRef8 = this.rendererRef) === null || _this$rendererRef8 === void 0 ? void 0 : _this$rendererRef8.focusPath(path);
28835
28863
  });
28836
28864
 
28837
28865
  _defineProperty(this, "blurInputPath", path => {
28838
- // eslint-disable-next-line react/no-string-refs
28839
- this.refs.renderer.blurPath(path);
28866
+ var _this$rendererRef9;
28867
+
28868
+ (_this$rendererRef9 = this.rendererRef) === null || _this$rendererRef9 === void 0 ? void 0 : _this$rendererRef9.blurPath(path);
28840
28869
  });
28841
28870
 
28842
28871
  _defineProperty(this, "showRationalesForCurrentlySelectedChoices", () => {
28843
- // eslint-disable-next-line react/no-string-refs
28844
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
28872
+ var _this$rendererRef10;
28873
+
28874
+ (_this$rendererRef10 = this.rendererRef) === null || _this$rendererRef10 === void 0 ? void 0 : _this$rendererRef10.showRationalesForCurrentlySelectedChoices();
28845
28875
  });
28846
28876
  }
28847
28877
 
@@ -28853,7 +28883,10 @@ class Group extends React__namespace.Component {
28853
28883
  }
28854
28884
 
28855
28885
  render() {
28856
- const apiOptions = ___default["default"].extend({}, ApiOptions.defaults, this.props.apiOptions, {
28886
+ var _this$rendererRef11;
28887
+
28888
+ const apiOptions = { ...ApiOptions.defaults,
28889
+ ...this.props.apiOptions,
28857
28890
  // Api Rewriting to support correct onFocus/onBlur
28858
28891
  // events for the mobile API
28859
28892
  onFocusChange: (newFocus, oldFocus) => {
@@ -28865,7 +28898,7 @@ class Group extends React__namespace.Component {
28865
28898
  this.props.onFocus(newFocus);
28866
28899
  }
28867
28900
  }
28868
- }); // Allow a problem number annotation to be added.
28901
+ }; // Allow a problem number annotation to be added.
28869
28902
  // This is cyclical and should probably be reconsidered. In order to
28870
28903
  // render the annotation ("Question 3 of 10"), we call findWidgets to
28871
28904
  // figure out our index in the list of all fellow group widgets. On
@@ -28874,9 +28907,8 @@ class Group extends React__namespace.Component {
28874
28907
  // componentDidMount so that we can number ourselves properly. But,
28875
28908
  // really we should have a more unidirectional flow. TODO(marcia): fix.
28876
28909
 
28877
-
28878
- const number = ___default["default"].indexOf(this.props.findWidgets("group"), this);
28879
-
28910
+ const groupWidgets = this.props.findWidgets("group");
28911
+ const number = groupWidgets.indexOf(this);
28880
28912
  const problemNumComponent = this.props.apiOptions.groupAnnotator(number, this.props.widgetId); // This is a little strange because the id of the widget that actually
28881
28913
  // changed is going to be lost in favor of the group widget's id. The
28882
28914
  // widgets prop also wasn't actually changed, and this only serves to
@@ -28884,15 +28916,12 @@ class Group extends React__namespace.Component {
28884
28916
  // has occurred.
28885
28917
 
28886
28918
  const onInteractWithWidget = id => {
28887
- // eslint-disable-next-line react/no-string-refs
28888
- if (this.refs.renderer) {
28889
- // eslint-disable-next-line react/no-string-refs
28890
- this.change("widgets", this.refs.renderer.props.widgets);
28919
+ if (this.rendererRef) {
28920
+ this.change("widgets", this.rendererRef.props.widgets);
28891
28921
  }
28892
- }; // eslint-disable-next-line react/no-string-refs
28893
-
28922
+ };
28894
28923
 
28895
- const score = this.refs.renderer && this.refs.renderer.score();
28924
+ const score = (_this$rendererRef11 = this.rendererRef) === null || _this$rendererRef11 === void 0 ? void 0 : _this$rendererRef11.score();
28896
28925
  const isValid = score && score.type !== "invalid";
28897
28926
  const isInvalid = score && score.type === "invalid"; // TODO(mdr): Widgets inside this Renderer are not discoverable through
28898
28927
  // the parent Renderer's `findWidgets` function.
@@ -28903,15 +28932,17 @@ class Group extends React__namespace.Component {
28903
28932
  "perseus-group-valid-answer": isValid,
28904
28933
  "perseus-group-invalid-answer": isInvalid
28905
28934
  })
28906
- }, problemNumComponent, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, this.props, {
28907
- // eslint-disable-next-line react/no-string-refs
28908
- ref: "renderer",
28935
+ }, problemNumComponent, /*#__PURE__*/React__namespace.createElement(Renderer, {
28936
+ content: this.props.content,
28937
+ widgets: this.props.widgets,
28938
+ images: this.props.images,
28939
+ ref: ref => this.rendererRef = ref,
28909
28940
  apiOptions: apiOptions,
28910
28941
  findExternalWidgets: this.props.findWidgets,
28911
28942
  reviewMode: !!this.props.reviewModeRubric,
28912
28943
  onInteractWithWidget: onInteractWithWidget,
28913
28944
  linterContext: this.props.linterContext
28914
- })), this.props.icon && /*#__PURE__*/React__namespace.createElement("div", {
28945
+ }), this.props.icon && /*#__PURE__*/React__namespace.createElement("div", {
28915
28946
  className: "group-icon"
28916
28947
  }, this.props.icon));
28917
28948
  }
@@ -28926,7 +28957,9 @@ _defineProperty(Group, "defaultProps", {
28926
28957
  });
28927
28958
 
28928
28959
  const traverseChildWidgets$1 = function (props, traverseRenderer) {
28929
- return ___default["default"].extend({}, props, traverseRenderer(props));
28960
+ return { ...props,
28961
+ ...traverseRenderer(props)
28962
+ };
28930
28963
  };
28931
28964
 
28932
28965
  var Group$1 = {