@khanacademy/perseus 34.0.0 → 35.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 (66) hide show
  1. package/dist/es/index.js +641 -525
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/es/strings.js +8 -2
  4. package/dist/es/strings.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +573 -447
  7. package/dist/index.js.map +1 -1
  8. package/dist/mixins/widget-prop-denylist.d.ts +0 -1
  9. package/dist/perseus-api.d.ts +4 -5
  10. package/dist/perseus-types.d.ts +6 -18
  11. package/dist/renderer.d.ts +4 -4
  12. package/dist/server-item-renderer.d.ts +4 -9
  13. package/dist/strings.d.ts +3 -0
  14. package/dist/strings.js +8 -2
  15. package/dist/strings.js.map +1 -1
  16. package/dist/types.d.ts +1 -3
  17. package/dist/validation.types.d.ts +111 -0
  18. package/dist/widgets/__shared__/noop-validator.d.ts +2 -2
  19. package/dist/widgets/categorizer/categorizer-validator.d.ts +5 -0
  20. package/dist/widgets/categorizer/categorizer.d.ts +6 -9
  21. package/dist/widgets/cs-program/cs-program.d.ts +4 -9
  22. package/dist/widgets/definition/definition.d.ts +6 -7
  23. package/dist/widgets/dropdown/dropdown-validator.d.ts +2 -2
  24. package/dist/widgets/dropdown/dropdown.d.ts +5 -5
  25. package/dist/widgets/explanation/explanation.d.ts +6 -7
  26. package/dist/widgets/expression/expression-validator.d.ts +2 -2
  27. package/dist/widgets/expression/expression.d.ts +38 -26
  28. package/dist/widgets/expression/expression.types.d.ts +0 -1
  29. package/dist/widgets/graded-group/graded-group.d.ts +2 -2
  30. package/dist/widgets/graded-group-set/graded-group-set.d.ts +2 -2
  31. package/dist/widgets/grapher/grapher.d.ts +6 -7
  32. package/dist/widgets/group/group.d.ts +3 -2
  33. package/dist/widgets/iframe/iframe.d.ts +4 -9
  34. package/dist/widgets/image/image.d.ts +6 -7
  35. package/dist/widgets/input-number/input-number-validator.d.ts +41 -0
  36. package/dist/widgets/input-number/input-number.d.ts +7 -10
  37. package/dist/widgets/interaction/interaction.d.ts +6 -6
  38. package/dist/widgets/interactive-graph.d.ts +9 -12
  39. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +15 -7
  40. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +16 -1
  41. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +5 -2
  42. package/dist/widgets/interactive-graphs/types.d.ts +3 -0
  43. package/dist/widgets/label-image/label-image-validator.d.ts +10 -0
  44. package/dist/widgets/label-image/label-image.d.ts +13 -10
  45. package/dist/widgets/label-image/types.d.ts +0 -4
  46. package/dist/widgets/matcher/matcher.d.ts +4 -4
  47. package/dist/widgets/matrix/matrix.d.ts +5 -5
  48. package/dist/widgets/molecule/molecule.d.ts +4 -3
  49. package/dist/widgets/number-line/number-line.d.ts +5 -3
  50. package/dist/widgets/numeric-input/numeric-input-validator.d.ts +2 -2
  51. package/dist/widgets/numeric-input/numeric-input.d.ts +7 -7
  52. package/dist/widgets/orderer/orderer.d.ts +4 -4
  53. package/dist/widgets/passage/passage.d.ts +6 -7
  54. package/dist/widgets/passage-ref/passage-ref.d.ts +6 -14
  55. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +4 -3
  56. package/dist/widgets/phet-simulation/phet-simulation.d.ts +2 -2
  57. package/dist/widgets/plotter/plotter.d.ts +5 -5
  58. package/dist/widgets/radio/radio-component.d.ts +6 -13
  59. package/dist/widgets/radio/radio-validator.d.ts +5 -0
  60. package/dist/widgets/sorter/sorter-validator.d.ts +4 -0
  61. package/dist/widgets/sorter/sorter.d.ts +7 -9
  62. package/dist/widgets/table/table.d.ts +5 -5
  63. package/dist/widgets/video/video.d.ts +5 -6
  64. package/package.json +1 -1
  65. package/dist/widgets/dropdown/dropdown.types.d.ts +0 -5
  66. package/dist/widgets/numeric-input/numeric-input.types.d.ts +0 -5
package/dist/index.js CHANGED
@@ -366,7 +366,10 @@ const mockStrings = {
366
366
  cos: "cos",
367
367
  tan: "tan",
368
368
  simulationLoadFail: "Sorry, this simulation cannot load.",
369
- simulationLocaleWarning: "Sorry, this simulation isn't available in your language."
369
+ simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
370
+ addPoint: "Add Point",
371
+ removePoint: "Remove Point",
372
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph"
370
373
  };
371
374
 
372
375
  /**
@@ -862,7 +865,7 @@ const denylist = [
862
865
  // (technically the renderer still adds them)
863
866
  "key", "ref",
864
867
  // added by src/renderer.jsx
865
- "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "questionCompleted", "findWidgets",
868
+ "containerSizeClass", "widgetId", "onChange", "problemNum", "apiOptions", "widgetIsOpen", "questionCompleted", "findWidgets",
866
869
  // added by src/editor.jsx, for widgets removing themselves
867
870
  // this is soooo not the right place for this, but alas.
868
871
  "onRemove",
@@ -870,17 +873,6 @@ const denylist = [
870
873
  "id",
871
874
  // Callbacks and items for interaction handling
872
875
  "onBlur", "onFocus", "trackInteraction", "keypadElement"];
873
- const removeDenylistProps = props => {
874
- const newProps = {
875
- ...props
876
- };
877
- for (const prop of denylist) {
878
- if (prop in newProps) {
879
- delete newProps[prop];
880
- }
881
- }
882
- return newProps;
883
- };
884
876
 
885
877
  /* eslint-disable @babel/no-invalid-this */
886
878
  const USAGE = "Usage:\n" + " this.change({propName: 5}, callback);\n" + ' this.change("propName", 5, callback);\n' + ' this.change("propName")';
@@ -1142,8 +1134,6 @@ class StubTagEditor extends React__namespace.Component {
1142
1134
  * * nothing if it is purely a bug fix.
1143
1135
  *
1144
1136
  * Callbacks passed to Renderer/ItemRenderer:
1145
- * * onInputError:
1146
- * Called when there is an error grading a widget
1147
1137
  * * onFocusChange: (newFocusPath, oldFocusPath, keypadDOMNode)
1148
1138
  * Called when the user focus changes. The first two parameters are `path`
1149
1139
  * arrays uniquely identifying the respect inputs. The third parameter,
@@ -1166,7 +1156,6 @@ class StubTagEditor extends React__namespace.Component {
1166
1156
  const ApiOptions = {
1167
1157
  propTypes: PropTypes__default["default"].shape({
1168
1158
  isArticle: PropTypes__default["default"].bool.isRequired,
1169
- onInputError: PropTypes__default["default"].func.isRequired,
1170
1159
  onFocusChange: PropTypes__default["default"].func.isRequired,
1171
1160
  GroupMetadataEditor: PropTypes__default["default"].func.isRequired,
1172
1161
  showAlignmentOptions: PropTypes__default["default"].bool.isRequired,
@@ -1250,7 +1239,6 @@ const ApiOptions = {
1250
1239
  defaults: {
1251
1240
  isArticle: false,
1252
1241
  isMobile: false,
1253
- onInputError: function () {},
1254
1242
  onFocusChange: function () {},
1255
1243
  GroupMetadataEditor: StubTagEditor,
1256
1244
  showAlignmentOptions: false,
@@ -2262,10 +2250,7 @@ const getDecimalSeparator = locale => {
2262
2250
  * show the user an error. TODO(joel) - what error?
2263
2251
  * - Otherwise, pass through the resulting points and message.
2264
2252
  */
2265
- function expressionValidator(userInput, rubric) {
2266
- let onInputError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
2267
- let strings = arguments.length > 3 ? arguments[3] : undefined;
2268
- let locale = arguments.length > 4 ? arguments[4] : undefined;
2253
+ function expressionValidator(userInput, rubric, strings, locale) {
2269
2254
  const options = ___default["default"].clone(rubric);
2270
2255
  ___default["default"].extend(options, {
2271
2256
  decimal_separator: getDecimalSeparator(locale)
@@ -2365,14 +2350,9 @@ function expressionValidator(userInput, rubric) {
2365
2350
  };
2366
2351
  }
2367
2352
  if (matchingAnswerForm.considered === "ungraded") {
2368
- // We matched an ungraded answer form - return "invalid", which
2369
- // will let the user try again with no penalty
2370
- const apiResult = onInputError(null,
2371
- // Reserved for some widget identifier
2372
- userInput, matchMessage);
2373
2353
  return {
2374
2354
  type: "invalid",
2375
- message: apiResult === false ? null : matchMessage
2355
+ message: matchMessage
2376
2356
  };
2377
2357
  }
2378
2358
  // We matched a graded answer form, so we can now tell the user
@@ -2422,11 +2402,8 @@ class Expression extends React__namespace.Component {
2422
2402
  _isMounted = false;
2423
2403
 
2424
2404
  // TODO remove this in favor of just using expressionValidator
2425
- static validate(userInput, rubric) {
2426
- let onInputError = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
2427
- let strings = arguments.length > 3 ? arguments[3] : undefined;
2428
- let locale = arguments.length > 4 ? arguments[4] : undefined;
2429
- return expressionValidator(userInput, rubric, onInputError, strings, locale);
2405
+ static validate(userInput, rubric, strings, locale) {
2406
+ return expressionValidator(userInput, rubric, strings, locale);
2430
2407
  }
2431
2408
  static getUserInputFromProps(props) {
2432
2409
  return normalizeTex(props.value);
@@ -2487,14 +2464,9 @@ class Expression extends React__namespace.Component {
2487
2464
  showErrorStyle: false
2488
2465
  });
2489
2466
  if (!this.parse(this.props.value, this.props).parsed) {
2490
- const apiResult = this.props.apiOptions.onInputError(null,
2491
- // reserved for some widget identifier
2492
- this.props.value, this.context.strings.ERROR_TITLE);
2493
- if (apiResult !== false) {
2494
- this.setState({
2495
- invalid: true
2496
- });
2497
- }
2467
+ this.setState({
2468
+ invalid: true
2469
+ });
2498
2470
  }
2499
2471
  }
2500
2472
  };
@@ -2508,12 +2480,12 @@ class Expression extends React__namespace.Component {
2508
2480
  });
2509
2481
  }
2510
2482
  };
2511
- simpleValidate = (_ref, onInputError) => {
2483
+ simpleValidate = _ref => {
2512
2484
  let {
2513
2485
  scoring,
2514
2486
  ...rubric
2515
2487
  } = _ref;
2516
- const score = expressionValidator(this.getUserInput(), rubric, onInputError || function () {}, this.context.strings, this.context.locale);
2488
+ const score = expressionValidator(this.getUserInput(), rubric, this.context.strings, this.context.locale);
2517
2489
 
2518
2490
  // "scoring" is a flag that indicates when we are checking answers.
2519
2491
  // otherwise, we may just be checking validity after changes.
@@ -2528,9 +2500,9 @@ class Expression extends React__namespace.Component {
2528
2500
  }
2529
2501
  return score;
2530
2502
  };
2531
- getUserInput = () => {
2503
+ getUserInput() {
2532
2504
  return Expression.getUserInputFromProps(this.props);
2533
- };
2505
+ }
2534
2506
  change = (() => {
2535
2507
  var _this = this;
2536
2508
  return function () {
@@ -14815,7 +14787,7 @@ const pluck = function (table, subKey) {
14815
14787
  * });
14816
14788
  * {a: 2, b: 3}
14817
14789
  */
14818
- const mapObject$2 = function (obj, lambda) {
14790
+ const mapObject$1 = function (obj, lambda) {
14819
14791
  const result = {};
14820
14792
  ___default["default"].each(___default["default"].keys(obj), function (key) {
14821
14793
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
@@ -14841,7 +14813,7 @@ const mapObjectFromArray$1 = function (arr, lambda) {
14841
14813
  };
14842
14814
  var objective_ = {
14843
14815
  pluck: pluck,
14844
- mapObject: mapObject$2,
14816
+ mapObject: mapObject$1,
14845
14817
  mapObjectFromArray: mapObjectFromArray$1
14846
14818
  };
14847
14819
 
@@ -19789,7 +19761,7 @@ class WidgetContainer extends React__namespace.Component {
19789
19761
  }
19790
19762
 
19791
19763
  const {
19792
- mapObject: mapObject$1,
19764
+ mapObject,
19793
19765
  mapObjectFromArray
19794
19766
  } = objective_;
19795
19767
  const rContainsNonWhitespace = /\S/;
@@ -19817,9 +19789,6 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19817
19789
  });
19818
19790
  };
19819
19791
 
19820
- // The return type for getUserInput. Widgets have full control of what is
19821
- // returned so it's not easily typed (some widgets return a scalar (string),
19822
- // some return a custom-built object
19823
19792
  /**
19824
19793
  * The Widget type represents the common API that the Renderer uses to interact
19825
19794
  * with all widgets. All widgets must implement the methods in this API, unless
@@ -19829,6 +19798,7 @@ const isIdPathPrefix = function (prefixArray, wholeArray) {
19829
19798
  * communicate with the individual widgets to coordinate actions such as
19830
19799
  * scoring, state serialization/deserialization, and focus management.
19831
19800
  */
19801
+
19832
19802
  class Renderer extends React__namespace.Component {
19833
19803
  _currentFocus;
19834
19804
  // @ts-expect-error - TS2564 - Property '_foundTextNodes' has no initializer and is not definitely assigned in the constructor.
@@ -20025,7 +19995,7 @@ class Renderer extends React__namespace.Component {
20025
19995
 
20026
19996
  // @ts-expect-error - TS2322 - Type '(props: Props) => Partial<Record<string, CategorizerWidget | CSProgramWidget | DefinitionWidget | DropdownWidget | ... 35 more ... | VideoWidget>>' is not assignable to type '(props: Props) => { [key: string]: PerseusWidget; }'.
20027
19997
  _getAllWidgetsInfo = props => {
20028
- return mapObject$1(props.widgets, (widgetInfo, widgetId) => {
19998
+ return mapObject(props.widgets, (widgetInfo, widgetId) => {
20029
19999
  if (!widgetInfo.type || !widgetInfo.alignment) {
20030
20000
  const newValues = {};
20031
20001
  if (!widgetInfo.type) {
@@ -20172,7 +20142,7 @@ class Renderer extends React__namespace.Component {
20172
20142
  * state from that instead of the current widget props.
20173
20143
  */
20174
20144
  getSerializedState = widgetProps => {
20175
- return mapObject$1(widgetProps || this.state.widgetProps, (props, widgetId) => {
20145
+ return mapObject(widgetProps || this.state.widgetProps, (props, widgetId) => {
20176
20146
  const widget = this.getWidgetInstance(widgetId);
20177
20147
  if (widget && widget.getSerializedState) {
20178
20148
  return widget.getSerializedState();
@@ -20208,7 +20178,7 @@ class Renderer extends React__namespace.Component {
20208
20178
  }
20209
20179
  };
20210
20180
  this.setState({
20211
- widgetProps: mapObject$1(serializedState, (props, widgetId) => {
20181
+ widgetProps: mapObject(serializedState, (props, widgetId) => {
20212
20182
  const widget = this.getWidgetInstance(widgetId);
20213
20183
  if (widget && widget.restoreSerializedState) {
20214
20184
  // Note that we probably can't call
@@ -21083,7 +21053,6 @@ class Renderer extends React__namespace.Component {
21083
21053
  */
21084
21054
  scoreWidgets = () => {
21085
21055
  const widgetProps = this.state.widgetInfo;
21086
- const onInputError = this.getApiOptions().onInputError;
21087
21056
  const gradedWidgetIds = ___default["default"].filter(this.widgetIds, id => {
21088
21057
  const props = widgetProps[id];
21089
21058
  const widgetIsGraded = props?.graded == null || props.graded;
@@ -21100,7 +21069,7 @@ class Renderer extends React__namespace.Component {
21100
21069
  widgetScores[id] = widget.simpleValidate({
21101
21070
  ...props?.options,
21102
21071
  scoring: true
21103
- }, onInputError);
21072
+ });
21104
21073
  }
21105
21074
  });
21106
21075
  return widgetScores;
@@ -21972,6 +21941,43 @@ const answerTypes = {
21972
21941
  forms: "pi"
21973
21942
  }
21974
21943
  };
21944
+ function inputNumberValidator(state, rubric, strings) {
21945
+ if (rubric.answerType == null) {
21946
+ rubric.answerType = "number";
21947
+ }
21948
+
21949
+ // note(matthewc): this will get immediately parsed again by
21950
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21951
+ // expected here
21952
+ const stringValue = `${rubric.value}`;
21953
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21954
+ simplify: rubric.simplify,
21955
+ inexact: rubric.inexact || undefined,
21956
+ maxError: rubric.maxError,
21957
+ forms: answerTypes[rubric.answerType].forms
21958
+ }, strings);
21959
+
21960
+ // We may have received TeX; try to parse it before grading.
21961
+ // If `currentValue` is not TeX, this should be a no-op.
21962
+ const currentValue = ParseTex$1(state.currentValue);
21963
+ const result = val(currentValue);
21964
+
21965
+ // TODO(eater): Seems silly to translate result to this invalid/points
21966
+ // thing and immediately translate it back in ItemRenderer.scoreInput()
21967
+ if (result.empty) {
21968
+ return {
21969
+ type: "invalid",
21970
+ message: result.message
21971
+ };
21972
+ }
21973
+ return {
21974
+ type: "points",
21975
+ earned: result.correct ? 1 : 0,
21976
+ total: 1,
21977
+ message: result.message
21978
+ };
21979
+ }
21980
+
21975
21981
  const formExamples$1 = {
21976
21982
  integer: function (options, strings) {
21977
21983
  return strings.integerExample;
@@ -22014,45 +22020,7 @@ class InputNumber extends React__namespace.Component {
22014
22020
  linterContext: PerseusLinter.linterContextDefault
22015
22021
  };
22016
22022
  static validate(state, rubric, strings) {
22017
- let onInputError = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : () => {};
22018
- if (rubric.answerType == null) {
22019
- rubric.answerType = "number";
22020
- }
22021
-
22022
- // note(matthewc): this will get immediately parsed again by
22023
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
22024
- // expected here
22025
- const stringValue = `${rubric.value}`;
22026
- const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
22027
- simplify: rubric.simplify,
22028
- inexact: rubric.inexact || undefined,
22029
- maxError: rubric.maxError,
22030
- forms: answerTypes[rubric.answerType].forms
22031
- }, strings);
22032
-
22033
- // We may have received TeX; try to parse it before grading.
22034
- // If `currentValue` is not TeX, this should be a no-op.
22035
- const currentValue = ParseTex$1(state.currentValue);
22036
- const result = val(currentValue);
22037
-
22038
- // TODO(eater): Seems silly to translate result to this invalid/points
22039
- // thing and immediately translate it back in ItemRenderer.scoreInput()
22040
- if (result.empty) {
22041
- // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
22042
- const apiResult = onInputError?.(null,
22043
- // reserved for some widget identifier
22044
- state.currentValue, result.message);
22045
- return {
22046
- type: "invalid",
22047
- message: apiResult === false ? null : result.message
22048
- };
22049
- }
22050
- return {
22051
- type: "points",
22052
- earned: result.correct ? 1 : 0,
22053
- total: 1,
22054
- message: result.message
22055
- };
22023
+ return inputNumberValidator(state, rubric, strings);
22056
22024
  }
22057
22025
  static getUserInputFromProps(props) {
22058
22026
  return {
@@ -22121,13 +22089,12 @@ class InputNumber extends React__namespace.Component {
22121
22089
  currentValue: newValue
22122
22090
  }, cb);
22123
22091
  };
22124
- getUserInput = () => {
22092
+ getUserInput() {
22125
22093
  return InputNumber.getUserInputFromProps(this.props);
22126
- };
22127
- simpleValidate = (rubric, onInputError) => {
22128
- onInputError = onInputError || function () {};
22129
- return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
22130
- };
22094
+ }
22095
+ simpleValidate(rubric) {
22096
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings);
22097
+ }
22131
22098
  examples = () => {
22132
22099
  const {
22133
22100
  strings
@@ -22472,9 +22439,9 @@ class NumericInput extends React__namespace.Component {
22472
22439
  const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
22473
22440
  return !noFormsAccepted && !allFormsAccepted;
22474
22441
  };
22475
- simpleValidate = rubric => {
22442
+ simpleValidate(rubric) {
22476
22443
  return numericInputValidator(this.getUserInput(), rubric, this.context.strings);
22477
- };
22444
+ }
22478
22445
  focus = () => {
22479
22446
  this.inputRef?.focus();
22480
22447
  return true;
@@ -22501,9 +22468,9 @@ class NumericInput extends React__namespace.Component {
22501
22468
  currentValue: newValue
22502
22469
  }, cb);
22503
22470
  };
22504
- getUserInput = () => {
22471
+ getUserInput() {
22505
22472
  return NumericInput.getUserInputFromProps(this.props);
22506
- };
22473
+ }
22507
22474
  handleChange = (newValue, cb) => {
22508
22475
  this.props.onChange({
22509
22476
  currentValue: newValue
@@ -22635,15 +22602,16 @@ var NumericInput$1 = {
22635
22602
  * Several widgets don't have "right"/"wrong" validation logic,
22636
22603
  * so this just says to move on past those widgets
22637
22604
  *
22638
- * TODO: widgets that use this probably shouldn't have any
22605
+ * TODO(LEMS-2396) widgets that use this probably shouldn't have any
22639
22606
  * validation logic and the thing validating an exercise
22640
22607
  * should just know to skip these
22641
22608
  */
22642
22609
  function noopValidator() {
22610
+ let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
22643
22611
  return {
22644
22612
  type: "points",
22645
- earned: 0,
22646
- total: 0,
22613
+ earned: points,
22614
+ total: points,
22647
22615
  message: null
22648
22616
  };
22649
22617
  }
@@ -22665,7 +22633,9 @@ class PassageRef extends React__namespace.Component {
22665
22633
  lineRange: null,
22666
22634
  content: null
22667
22635
  };
22668
- static validate(userInput, rubric) {
22636
+
22637
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22638
+ static validate() {
22669
22639
  return noopValidator();
22670
22640
  }
22671
22641
  componentDidMount() {
@@ -22687,11 +22657,10 @@ class PassageRef extends React__namespace.Component {
22687
22657
  this._isMounted = false;
22688
22658
  }
22689
22659
 
22690
- // TODO(tamarab): getUserInput needs to be updated to only return
22691
- // props input by the user. Currently returns all the widget's props.
22692
- getUserInput = () => {
22693
- return removeDenylistProps(this.props);
22694
- };
22660
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22661
+ getUserInput() {
22662
+ return null;
22663
+ }
22695
22664
  change = (() => {
22696
22665
  var _this = this;
22697
22666
  return function () {
@@ -22727,9 +22696,11 @@ class PassageRef extends React__namespace.Component {
22727
22696
  }
22728
22697
  }
22729
22698
  };
22730
- simpleValidate = rubric => {
22731
- return PassageRef.validate(this.getUserInput(), rubric);
22732
- };
22699
+
22700
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22701
+ simpleValidate() {
22702
+ return noopValidator();
22703
+ }
22733
22704
  render() {
22734
22705
  const {
22735
22706
  strings
@@ -24133,6 +24104,56 @@ const styles$t = aphrodite.StyleSheet.create({
24133
24104
  }
24134
24105
  });
24135
24106
 
24107
+ function radioValidator(userInput, rubric, strings) {
24108
+ const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24109
+ return sum + (selected ? 1 : 0);
24110
+ }, 0);
24111
+ if (numSelected === 0) {
24112
+ return {
24113
+ type: "invalid",
24114
+ message: null
24115
+ };
24116
+ }
24117
+
24118
+ // TODO: should numCorrect actually be on the rubric
24119
+ // instead of the userInput?
24120
+ if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24121
+ return {
24122
+ type: "invalid",
24123
+ message: strings.chooseCorrectNum
24124
+ };
24125
+ // If NOTA and some other answer are checked, ...
24126
+ }
24127
+
24128
+ // TODO: should noneOfTheAboveSelected be replaced with a
24129
+ // combination of choicesSelected and noneOfTheAboveIndex?
24130
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24131
+ return {
24132
+ type: "invalid",
24133
+ message: strings.notNoneOfTheAbove
24134
+ };
24135
+ }
24136
+ const correct = userInput.choicesSelected.every((selected, i) => {
24137
+ let isCorrect;
24138
+ // TODO: should noneOfTheAboveIndex actually be on the rubric
24139
+ // instead of the userInput?
24140
+ if (userInput.noneOfTheAboveIndex === i) {
24141
+ isCorrect = rubric.choices.every((choice, j) => {
24142
+ return i === j || !choice.correct;
24143
+ });
24144
+ } else {
24145
+ isCorrect = !!rubric.choices[i].correct;
24146
+ }
24147
+ return isCorrect === selected;
24148
+ });
24149
+ return {
24150
+ type: "points",
24151
+ earned: correct ? 1 : 0,
24152
+ total: 1,
24153
+ message: null
24154
+ };
24155
+ }
24156
+
24136
24157
  // RenderProps is the return type for radio.jsx#transform
24137
24158
 
24138
24159
  class Radio$1 extends React__namespace.Component {
@@ -24148,46 +24169,7 @@ class Radio$1 extends React__namespace.Component {
24148
24169
  showSolutions: "none"
24149
24170
  };
24150
24171
  static validate(userInput, rubric, strings) {
24151
- const numSelected = userInput.choicesSelected.reduce((sum, selected) => {
24152
- return sum + (selected ? 1 : 0);
24153
- }, 0);
24154
- if (numSelected === 0) {
24155
- return {
24156
- type: "invalid",
24157
- message: null
24158
- };
24159
- }
24160
- if (userInput.numCorrect && userInput.numCorrect > 1 && numSelected !== userInput.numCorrect) {
24161
- return {
24162
- type: "invalid",
24163
- message: strings.chooseCorrectNum
24164
- };
24165
- // If NOTA and some other answer are checked, ...
24166
- }
24167
-
24168
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
24169
- return {
24170
- type: "invalid",
24171
- message: strings.notNoneOfTheAbove
24172
- };
24173
- }
24174
- const correct = userInput.choicesSelected.every((selected, i) => {
24175
- let isCorrect;
24176
- if (userInput.noneOfTheAboveIndex === i) {
24177
- isCorrect = rubric.choices.every((choice, j) => {
24178
- return i === j || !choice.correct;
24179
- });
24180
- } else {
24181
- isCorrect = !!rubric.choices[i].correct;
24182
- }
24183
- return isCorrect === selected;
24184
- });
24185
- return {
24186
- type: "points",
24187
- earned: correct ? 1 : 0,
24188
- total: 1,
24189
- message: null
24190
- };
24172
+ return radioValidator(userInput, rubric, strings);
24191
24173
  }
24192
24174
  static getUserInputFromProps(props) {
24193
24175
  // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
@@ -24372,12 +24354,12 @@ class Radio$1 extends React__namespace.Component {
24372
24354
  });
24373
24355
  this.props.trackInteraction();
24374
24356
  };
24375
- getUserInput = () => {
24357
+ getUserInput() {
24376
24358
  return Radio$1.getUserInputFromProps(this.props);
24377
- };
24378
- simpleValidate = rubric => {
24379
- return Radio$1.validate(this.getUserInput(), rubric, this.context.strings);
24380
- };
24359
+ }
24360
+ simpleValidate(rubric) {
24361
+ return radioValidator(this.getUserInput(), rubric, this.context.strings);
24362
+ }
24381
24363
 
24382
24364
  /**
24383
24365
  * Turn on rationale display for the currently selected choices. Note that
@@ -24676,6 +24658,31 @@ var Radio = {
24676
24658
  /* eslint-disable import/no-named-as-default */
24677
24659
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
24678
24660
 
24661
+ function categorizerValidator(userInput, rubric, strings) {
24662
+ let completed = true;
24663
+ let allCorrect = true;
24664
+ rubric.values.forEach((value, i) => {
24665
+ if (userInput.values[i] == null) {
24666
+ completed = false;
24667
+ }
24668
+ if (userInput.values[i] !== value) {
24669
+ allCorrect = false;
24670
+ }
24671
+ });
24672
+ if (!completed) {
24673
+ return {
24674
+ type: "invalid",
24675
+ message: strings.invalidSelection
24676
+ };
24677
+ }
24678
+ return {
24679
+ type: "points",
24680
+ earned: allCorrect ? 1 : 0,
24681
+ total: 1,
24682
+ message: null
24683
+ };
24684
+ }
24685
+
24679
24686
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
24680
24687
  class Categorizer extends React__namespace.Component {
24681
24688
  static contextType = PerseusI18nContext;
@@ -24689,28 +24696,7 @@ class Categorizer extends React__namespace.Component {
24689
24696
  uniqueId: ___default["default"].uniqueId("perseus_radio_")
24690
24697
  };
24691
24698
  static validate(userInput, rubric, strings) {
24692
- let completed = true;
24693
- let allCorrect = true;
24694
- rubric.values.forEach((value, i) => {
24695
- if (userInput.values[i] == null) {
24696
- completed = false;
24697
- }
24698
- if (userInput.values[i] !== value) {
24699
- allCorrect = false;
24700
- }
24701
- });
24702
- if (!completed) {
24703
- return {
24704
- type: "invalid",
24705
- message: strings.invalidSelection
24706
- };
24707
- }
24708
- return {
24709
- type: "points",
24710
- earned: allCorrect ? 1 : 0,
24711
- total: 1,
24712
- message: null
24713
- };
24699
+ return categorizerValidator(userInput, rubric, strings);
24714
24700
  }
24715
24701
  static getUserInputFromProps(props) {
24716
24702
  return {
@@ -24727,9 +24713,9 @@ class Categorizer extends React__namespace.Component {
24727
24713
  return change.apply(_this, args);
24728
24714
  };
24729
24715
  })();
24730
- getUserInput = () => {
24716
+ getUserInput() {
24731
24717
  return Categorizer.getUserInputFromProps(this.props);
24732
- };
24718
+ }
24733
24719
  onChange(itemNum, catNum) {
24734
24720
  const values = [...this.props.values];
24735
24721
  // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'never'.
@@ -24737,9 +24723,9 @@ class Categorizer extends React__namespace.Component {
24737
24723
  this.change("values", values);
24738
24724
  this.props.trackInteraction();
24739
24725
  }
24740
- simpleValidate = rubric => {
24741
- return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24742
- };
24726
+ simpleValidate(rubric) {
24727
+ return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24728
+ }
24743
24729
  render() {
24744
24730
  const self = this;
24745
24731
 
@@ -25108,15 +25094,19 @@ class Definition extends React__namespace.Component {
25108
25094
  togglePrompt: "define me",
25109
25095
  definition: "definition goes here"
25110
25096
  };
25111
- static validate(userInput, rubric) {
25097
+
25098
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25099
+ static validate() {
25112
25100
  return noopValidator();
25113
25101
  }
25114
- getUserInput = () => {
25102
+ getUserInput() {
25115
25103
  return {};
25116
- };
25117
- simpleValidate = rubric => {
25118
- return Definition.validate(this.getUserInput(), rubric);
25119
- };
25104
+ }
25105
+
25106
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25107
+ simpleValidate() {
25108
+ return noopValidator();
25109
+ }
25120
25110
  render() {
25121
25111
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
25122
25112
  let {
@@ -25182,18 +25172,13 @@ class DeprecatedStandin extends React__default["default"].Component {
25182
25172
  static validate(userInput, rubric) {
25183
25173
  // Since this mean to replace an existing widget the learner
25184
25174
  // WILL earn points for this widget
25185
- return {
25186
- type: "points",
25187
- earned: 1,
25188
- total: 1,
25189
- message: null
25190
- };
25175
+ return noopValidator(1);
25191
25176
  }
25192
25177
  getUserInput = () => {
25193
25178
  return {};
25194
25179
  };
25195
25180
  simpleValidate = rubric => {
25196
- return DeprecatedStandin.validate(this.getUserInput(), rubric);
25181
+ return noopValidator(1);
25197
25182
  };
25198
25183
  render() {
25199
25184
  return /*#__PURE__*/React__default["default"].createElement("div", {
@@ -25259,14 +25244,14 @@ class Dropdown extends React__namespace.Component {
25259
25244
  selected: selected
25260
25245
  });
25261
25246
  };
25262
- getUserInput = () => {
25247
+ getUserInput() {
25263
25248
  return {
25264
25249
  value: this.props.selected
25265
25250
  };
25266
- };
25267
- simpleValidate = rubric => {
25251
+ }
25252
+ simpleValidate(rubric) {
25268
25253
  return dropdownValidator(this.getUserInput(), rubric);
25269
- };
25254
+ }
25270
25255
  render() {
25271
25256
  const children = [/*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.OptionItem, {
25272
25257
  key: "placeholder",
@@ -25331,7 +25316,9 @@ class Explanation extends React__namespace.Component {
25331
25316
  widgets: {},
25332
25317
  linterContext: PerseusLinter.linterContextDefault
25333
25318
  };
25334
- static validate(userInput, rubric) {
25319
+
25320
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25321
+ static validate() {
25335
25322
  return noopValidator();
25336
25323
  }
25337
25324
  componentDidMount() {
@@ -25355,12 +25342,14 @@ class Explanation extends React__namespace.Component {
25355
25342
  });
25356
25343
  this.props.trackInteraction();
25357
25344
  };
25358
- getUserInput = () => {
25345
+ getUserInput() {
25359
25346
  return {};
25360
- };
25361
- simpleValidate = rubric => {
25362
- return Explanation.validate(this.getUserInput(), rubric);
25363
- };
25347
+ }
25348
+
25349
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25350
+ simpleValidate() {
25351
+ return noopValidator();
25352
+ }
25364
25353
  render() {
25365
25354
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
25366
25355
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -26991,9 +26980,6 @@ class FunctionGrapher extends React__namespace.Component {
26991
26980
  }), this.props.model && this.renderPlot(), this.props.model && this.renderAsymptote(), this.props.model && points)));
26992
26981
  }
26993
26982
  }
26994
-
26995
- // Really this is props.plot, I think
26996
-
26997
26983
  /* Widget and editor. */
26998
26984
  class Grapher extends React__namespace.Component {
26999
26985
  horizHairline;
@@ -27228,9 +27214,9 @@ class Group extends React__namespace.Component {
27228
27214
  return change.apply(_this, args);
27229
27215
  };
27230
27216
  })();
27231
- getUserInput = () => {
27217
+ getUserInput() {
27232
27218
  return this.rendererRef?.getUserInput();
27233
- };
27219
+ }
27234
27220
  getSerializedState = () => {
27235
27221
  return this.rendererRef?.getSerializedState();
27236
27222
  };
@@ -27240,9 +27226,9 @@ class Group extends React__namespace.Component {
27240
27226
  // (all our changes were in state):
27241
27227
  return null;
27242
27228
  };
27243
- simpleValidate = rubric => {
27229
+ simpleValidate() {
27244
27230
  return this.rendererRef?.score();
27245
- };
27231
+ }
27246
27232
 
27247
27233
  // Mobile API:
27248
27234
  getInputPaths = () => {
@@ -27538,7 +27524,9 @@ class ImageWidget extends React__namespace.Component {
27538
27524
  caption: "",
27539
27525
  linterContext: PerseusLinter.linterContextDefault
27540
27526
  };
27541
- static validate(userInput, rubric) {
27527
+
27528
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27529
+ static validate() {
27542
27530
  return noopValidator();
27543
27531
  }
27544
27532
  change = (() => {
@@ -27550,12 +27538,14 @@ class ImageWidget extends React__namespace.Component {
27550
27538
  return change.apply(_this, args);
27551
27539
  };
27552
27540
  })();
27553
- getUserInput = () => {
27541
+ getUserInput() {
27554
27542
  return null;
27555
- };
27556
- simpleValidate = rubric => {
27557
- return ImageWidget.validate(this.getUserInput(), rubric);
27558
- };
27543
+ }
27544
+
27545
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27546
+ simpleValidate() {
27547
+ return noopValidator();
27548
+ }
27559
27549
  focus = () => {}; // no-op
27560
27550
 
27561
27551
  render() {
@@ -27781,7 +27771,9 @@ class Interaction extends React__namespace.Component {
27781
27771
  },
27782
27772
  elements: []
27783
27773
  };
27784
- static validate(state, rubric) {
27774
+
27775
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27776
+ static validate() {
27785
27777
  return noopValidator();
27786
27778
  }
27787
27779
  state = {
@@ -27891,16 +27883,18 @@ class Interaction extends React__namespace.Component {
27891
27883
  return change.apply(_this, args);
27892
27884
  };
27893
27885
  })();
27894
- getUserInput = () => {
27886
+ getUserInput() {
27895
27887
  // TODO(eater): Perhaps we want to be able to record the state of the
27896
27888
  // user's interaction. Unfortunately sending all the props will
27897
27889
  // probably make the attempt payload too large. So for now, don't send
27898
27890
  // anything.
27899
27891
  return {};
27900
- };
27901
- simpleValidate = rubric => {
27902
- return Interaction.validate(this.getUserInput(), rubric);
27903
- };
27892
+ }
27893
+
27894
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27895
+ simpleValidate() {
27896
+ return noopValidator();
27897
+ }
27904
27898
  render() {
27905
27899
  const range = this.props.graph.range;
27906
27900
  let labels = this.props.graph.labels;
@@ -29963,12 +29957,26 @@ function GraphLockedLabelsLayer(props) {
29963
29957
  key: `label-${i}`
29964
29958
  }, figure));
29965
29959
  }
29966
- if (flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point") {
29960
+ if (
29961
+ // Point flag + point type
29962
+ flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point" ||
29963
+ // Line flag + line type
29964
+ flags?.["mafs"]?.["locked-line-labels"] && figure.type === "line" ||
29965
+ // Vector flag + vector type
29966
+ flags?.["mafs"]?.["locked-vector-labels"] && figure.type === "vector" ||
29967
+ // Ellipse flag + ellipse type
29968
+ flags?.["mafs"]?.["locked-ellipse-labels"] && figure.type === "ellipse" ||
29969
+ // Function flag + function type
29970
+ flags?.["mafs"]?.["locked-function-labels"] && figure.type === "function") {
29967
29971
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, {
29968
- key: `point-${i}`
29969
- }, figure.labels.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29970
- key: `locked-figure-${i}-label-${j}`
29971
- }, label))));
29972
+ key: i
29973
+ }, figure.labels?.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29974
+ key: `${i}-label-${j}`
29975
+ }, label))), figure.type === "line" && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, figure.points[0].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29976
+ key: `locked-figure-${i}-point-1-label-${k}`
29977
+ }, label))), figure.points[1].labels?.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29978
+ key: `locked-figure-${i}-point-2-label-${k}`
29979
+ }, label)))));
29972
29980
  }
29973
29981
  return null;
29974
29982
  });
@@ -30308,7 +30316,9 @@ const GraphLockedLayer = props => {
30308
30316
 
30309
30317
  const actions = {
30310
30318
  global: {
30311
- deleteIntent
30319
+ deleteIntent,
30320
+ changeInteractionMode,
30321
+ changeKeyboardInvitationVisibility
30312
30322
  },
30313
30323
  angle: {
30314
30324
  movePoint
@@ -30400,6 +30410,20 @@ function clickPoint(index) {
30400
30410
  index
30401
30411
  };
30402
30412
  }
30413
+ const CHANGE_INTERACTION_MODE = "point-change-interaction-mode";
30414
+ function changeInteractionMode(mode) {
30415
+ return {
30416
+ type: CHANGE_INTERACTION_MODE,
30417
+ mode
30418
+ };
30419
+ }
30420
+ const CHANGE_KEYBOARD_INVITATION_VISIBILITY = "change-keyboard-interaction-invitation-visibility";
30421
+ function changeKeyboardInvitationVisibility(shouldShow) {
30422
+ return {
30423
+ type: CHANGE_KEYBOARD_INVITATION_VISIBILITY,
30424
+ shouldShow
30425
+ };
30426
+ }
30403
30427
  const MOVE_ALL = "move-all";
30404
30428
  function moveAll(delta) {
30405
30429
  return {
@@ -32068,7 +32092,7 @@ function Protractor() {
32068
32092
  const [[xMin, xMax], [yMin, yMax]] = range;
32069
32093
  // Position the protractor in the center of the graph (horizontally), and
32070
32094
  // 95% of the way to the bottom of the graph (vertically).
32071
- const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.05)];
32095
+ const initialCenter = [lerp(xMin, xMax, 0.5), lerp(yMin, yMax, 0.25)];
32072
32096
  const [center, setCenter] = React.useState(initialCenter);
32073
32097
  const [rotationHandleOffset, setRotationHandleOffset] = React.useState(centerToRotationHandle);
32074
32098
  const draggableRef = React.useRef(null);
@@ -32229,6 +32253,9 @@ const MafsGraph = props => {
32229
32253
  x: viewboxX,
32230
32254
  y: viewboxY
32231
32255
  };
32256
+ const {
32257
+ strings
32258
+ } = usePerseusI18n();
32232
32259
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
32233
32260
  value: {
32234
32261
  range: state.range,
@@ -32257,15 +32284,18 @@ const MafsGraph = props => {
32257
32284
  height
32258
32285
  },
32259
32286
  onKeyUp: event => {
32260
- if (event.key === "Backspace") {
32261
- dispatch(actions.global.deleteIntent());
32262
- graphRef.current?.focus();
32263
- }
32287
+ handleKeyboardEvent(event, state, dispatch);
32264
32288
  },
32265
32289
  "aria-label": fullGraphAriaLabel,
32266
32290
  "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
32267
32291
  ref: graphRef,
32268
- tabIndex: 0
32292
+ tabIndex: 0,
32293
+ onFocus: event => {
32294
+ handleFocusEvent(event, state, dispatch);
32295
+ },
32296
+ onBlur: event => {
32297
+ handleBlurEvent(event, state, dispatch);
32298
+ }
32269
32299
  }, fullGraphAriaDescription && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32270
32300
  id: descriptionId,
32271
32301
  tabIndex: -1,
@@ -32327,35 +32357,75 @@ const MafsGraph = props => {
32327
32357
  }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
32328
32358
  state,
32329
32359
  dispatch
32330
- })))))), renderGraphControls({
32360
+ }))))), state.type === "point" && state.showKeyboardInteractionInvitation && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32361
+ style: {
32362
+ textAlign: "center",
32363
+ backgroundColor: "white",
32364
+ border: "1px solid #21242C52",
32365
+ padding: "16px 0",
32366
+ boxShadow: "0px 8px 8px 0px #21242C14",
32367
+ // This translates the box to the center of the
32368
+ // graph Then backs it off by half of its
32369
+ // overall height so it's perfectly centered
32370
+ top: "50%",
32371
+ transform: "translateY(-50%)"
32372
+ }
32373
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, strings.graphKeyboardPrompt))), renderGraphControls({
32331
32374
  state,
32332
32375
  dispatch,
32333
- width
32376
+ width,
32377
+ strings
32334
32378
  })));
32335
32379
  };
32336
- const renderPointGraphControls = props => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32337
- style: {
32338
- flexDirection: "row",
32339
- width: props.width
32340
- }
32341
- }, props.state.showRemovePointButton && props.state.focusedPointIndex !== null && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32342
- id: REMOVE_BUTTON_ID,
32343
- kind: "secondary",
32344
- color: "destructive",
32345
- tabIndex: -1,
32346
- style: {
32347
- width: "100%",
32348
- marginLeft: "20px"
32349
- },
32350
- onClick: event => {
32351
- props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32352
- }
32353
- }, "Remove Point"));
32380
+ const renderPointGraphControls = props => {
32381
+ const {
32382
+ interactionMode,
32383
+ showRemovePointButton,
32384
+ focusedPointIndex
32385
+ } = props.state;
32386
+ const {
32387
+ strings
32388
+ } = props;
32389
+ const shouldShowRemoveButton = showRemovePointButton && focusedPointIndex !== null;
32390
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32391
+ style: {
32392
+ flexDirection: "row",
32393
+ width: props.width
32394
+ }
32395
+ }, interactionMode === "keyboard" && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32396
+ kind: "secondary",
32397
+ style: {
32398
+ width: "100%",
32399
+ marginLeft: "20px"
32400
+ },
32401
+ tabIndex: 0,
32402
+ onClick: () => {
32403
+ props.dispatch(actions.pointGraph.addPoint([0, 0]));
32404
+ }
32405
+ }, strings.addPoint), interactionMode === "mouse" && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32406
+ id: REMOVE_BUTTON_ID,
32407
+ kind: "secondary",
32408
+ color: "destructive"
32409
+ // This button is meant to be interacted with by the mouse only
32410
+ // Never allow learners to tab to this button
32411
+ ,
32412
+ tabIndex: -1,
32413
+ style: {
32414
+ width: "100%",
32415
+ marginLeft: "20px",
32416
+ visibility: shouldShowRemoveButton ? "visible" : "hidden"
32417
+ },
32418
+ onClick: event => {
32419
+ props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32420
+ }
32421
+ }, strings.removePoint));
32422
+ };
32354
32423
  const renderGraphControls = props => {
32355
32424
  const {
32356
32425
  state,
32357
32426
  dispatch,
32358
- width
32427
+ width,
32428
+ strings
32359
32429
  } = props;
32360
32430
  const {
32361
32431
  type
@@ -32366,7 +32436,8 @@ const renderGraphControls = props => {
32366
32436
  return renderPointGraphControls({
32367
32437
  state,
32368
32438
  dispatch,
32369
- width
32439
+ width,
32440
+ strings
32370
32441
  });
32371
32442
  }
32372
32443
  return null;
@@ -32374,6 +32445,34 @@ const renderGraphControls = props => {
32374
32445
  return null;
32375
32446
  }
32376
32447
  };
32448
+ function handleFocusEvent(event, state, dispatch) {
32449
+ if (state.type === "point" && state.numPoints === "unlimited") {
32450
+ if (event.target.classList.contains("mafs-graph") && state.interactionMode === "mouse") {
32451
+ dispatch(actions.global.changeKeyboardInvitationVisibility(true));
32452
+ }
32453
+ }
32454
+ }
32455
+ function handleBlurEvent(event, state, dispatch) {
32456
+ if (state.type === "point" && state.numPoints === "unlimited") {
32457
+ dispatch(actions.global.changeKeyboardInvitationVisibility(false));
32458
+ }
32459
+ }
32460
+ function handleKeyboardEvent(event, state, dispatch) {
32461
+ if (state.type === "point" && state.numPoints === "unlimited") {
32462
+ if (event.key === "Backspace") {
32463
+ dispatch(actions.global.deleteIntent());
32464
+
32465
+ // After removing a point blur
32466
+ // It would be nice if this could focus on the graph but doing so
32467
+ // would trigger the message to prompt a learner to enter keyboard mode
32468
+ document.activeElement.blur();
32469
+ } else if (event.shiftKey && event.key === "Enter") {
32470
+ dispatch(actions.global.changeInteractionMode("keyboard"));
32471
+ } else if (state.interactionMode === "keyboard" && event.key === "a") {
32472
+ dispatch(actions.pointGraph.addPoint([0, 0]));
32473
+ }
32474
+ }
32475
+ }
32377
32476
 
32378
32477
  // Calculate the difference between the min and max values of a range
32379
32478
  const getRangeDiff = range => {
@@ -32543,7 +32642,9 @@ function initializeGraphState(params) {
32543
32642
  coords: getPointCoords(graph, range, step),
32544
32643
  numPoints: graph.numPoints || 0,
32545
32644
  focusedPointIndex: null,
32546
- showRemovePointButton: false
32645
+ showRemovePointButton: false,
32646
+ interactionMode: "mouse",
32647
+ showKeyboardInteractionInvitation: false
32547
32648
  };
32548
32649
  case "circle":
32549
32650
  return {
@@ -32816,6 +32917,10 @@ function interactiveGraphReducer(state, action) {
32816
32917
  return doDeleteIntent(state);
32817
32918
  case CLICK_POINT:
32818
32919
  return doClickPoint(state, action);
32920
+ case CHANGE_INTERACTION_MODE:
32921
+ return doChangeInteractionMode(state, action);
32922
+ case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
32923
+ return doChangeKeyboardInvitationVisibility(state, action);
32819
32924
  default:
32820
32925
  throw new wonderStuffCore.UnreachableCaseError(action);
32821
32926
  }
@@ -32867,6 +32972,33 @@ function doClickPoint(state, action) {
32867
32972
  }
32868
32973
  return state;
32869
32974
  }
32975
+ function doChangeInteractionMode(state, action) {
32976
+ if (state.type !== "point") {
32977
+ return state;
32978
+ }
32979
+ if (state.numPoints === "unlimited") {
32980
+ const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
32981
+ return {
32982
+ ...state,
32983
+ interactionMode: action.mode,
32984
+ showKeyboardInteractionInvitation: nextKeyboardInvitation
32985
+ };
32986
+ }
32987
+ return state;
32988
+ }
32989
+ function doChangeKeyboardInvitationVisibility(state, action) {
32990
+ if (state.type !== "point") {
32991
+ return state;
32992
+ }
32993
+ if (state.numPoints === "unlimited") {
32994
+ return {
32995
+ ...state,
32996
+ showKeyboardInteractionInvitation: action.shouldShow,
32997
+ hasBeenInteractedWith: true
32998
+ };
32999
+ }
33000
+ return state;
33001
+ }
32870
33002
  function doMovePointInFigure(state, action) {
32871
33003
  switch (state.type) {
32872
33004
  case "segment":
@@ -33635,7 +33767,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33635
33767
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
33636
33768
  const showAngles = graph.type === "polygon" ? graph.showAngles : null;
33637
33769
  const showSides = graph.type === "polygon" ? graph.showSides : null;
33638
- const startCoords = "startCoords" in graph ? graph.startCoords : null;
33770
+ const startCoords = "startCoords" in graph ? graph.startCoords : undefined;
33639
33771
  const originalPropsRef = React.useRef(props);
33640
33772
  const latestPropsRef = wonderBlocksCore.useLatestRef(props);
33641
33773
  React.useEffect(() => {
@@ -34923,17 +35055,17 @@ class InteractiveGraph extends React__namespace.Component {
34923
35055
  type: "linear"
34924
35056
  }
34925
35057
  };
34926
- getUserInput = () => {
34927
- if (this.mafsRef.current) {
34928
- return this.mafsRef.current.getUserInput?.();
35058
+ getUserInput() {
35059
+ if (this.mafsRef.current?.getUserInput) {
35060
+ return this.mafsRef.current.getUserInput();
34929
35061
  }
34930
- if (this.legacyGraphRef.current) {
35062
+ if (this.legacyGraphRef.current?.getUserInput) {
34931
35063
  return this.legacyGraphRef.current.getUserInput();
34932
35064
  }
34933
35065
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
34934
- };
35066
+ }
34935
35067
  simpleValidate(rubric) {
34936
- return InteractiveGraph.validate(this.getUserInput?.(), rubric, this);
35068
+ return InteractiveGraph.validate(this.getUserInput(), rubric, this);
34937
35069
  }
34938
35070
  render() {
34939
35071
  // Mafs shim
@@ -36600,6 +36732,57 @@ const styles$h = aphrodite.StyleSheet.create({
36600
36732
  }
36601
36733
  });
36602
36734
 
36735
+ // Question state for marker as result of user selected answers.
36736
+
36737
+ function scoreMarker(marker) {
36738
+ const score = {
36739
+ hasAnswers: false,
36740
+ isCorrect: false
36741
+ };
36742
+ if (marker.selected && marker.selected.length > 0) {
36743
+ score.hasAnswers = true;
36744
+ }
36745
+ if (marker.answers.length > 0) {
36746
+ if (marker.selected && marker.selected.length === marker.answers.length) {
36747
+ // All correct answers are selected by the user.
36748
+ score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36749
+ }
36750
+ } else if (!marker.selected || marker.selected.length === 0) {
36751
+ // Correct as no answers should be selected by the user.
36752
+ score.isCorrect = true;
36753
+ }
36754
+ return score;
36755
+ }
36756
+ function labelImageValidator(state, rubric) {
36757
+ let numAnswered = 0;
36758
+ let numCorrect = 0;
36759
+ for (const marker of state.markers) {
36760
+ const score = scoreMarker(marker);
36761
+ if (score.hasAnswers) {
36762
+ numAnswered++;
36763
+ }
36764
+ if (score.isCorrect) {
36765
+ numCorrect++;
36766
+ }
36767
+ }
36768
+
36769
+ // We expect all question markers to be answered before grading.
36770
+ if (numAnswered !== state.markers.length) {
36771
+ return {
36772
+ type: "invalid",
36773
+ message: null
36774
+ };
36775
+ }
36776
+ return {
36777
+ type: "points",
36778
+ // Markers with no expected answers are graded as correct if user
36779
+ // makes no answer selection.
36780
+ earned: numCorrect === state.markers.length ? 1 : 0,
36781
+ total: 1,
36782
+ message: null
36783
+ };
36784
+ }
36785
+
36603
36786
  const BringToFront = {
36604
36787
  boxShadow: `0 8px 8px ${wonderBlocksTokens.color.offBlack64}`,
36605
36788
  zIndex: 1000
@@ -36860,61 +37043,15 @@ const styles$f = aphrodite.StyleSheet.create({
36860
37043
  * No diagonals allowed.
36861
37044
  */
36862
37045
 
37046
+ // TODO: should this be using WidgetProps / PerseusLabelImageWidgetOptions?
37047
+
36863
37048
  class LabelImage extends React__namespace.Component {
36864
37049
  static contextType = PerseusI18nContext;
36865
37050
  // The rendered markers on the question image for labeling.
36866
37051
  _markers;
36867
37052
  _mounted = false;
36868
- static gradeMarker(marker) {
36869
- const score = {
36870
- hasAnswers: false,
36871
- isCorrect: false
36872
- };
36873
- if (marker.selected && marker.selected.length > 0) {
36874
- // @ts-expect-error - TS2540 - Cannot assign to 'hasAnswers' because it is a read-only property.
36875
- score.hasAnswers = true;
36876
- }
36877
- if (marker.answers.length > 0) {
36878
- if (marker.selected && marker.selected.length === marker.answers.length) {
36879
- // All correct answers are selected by the user.
36880
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36881
- score.isCorrect = marker.selected.every(choice => marker.answers.includes(choice));
36882
- }
36883
- } else if (!marker.selected || marker.selected.length === 0) {
36884
- // Correct as no answers should be selected by the user.
36885
- // @ts-expect-error - TS2540 - Cannot assign to 'isCorrect' because it is a read-only property.
36886
- score.isCorrect = true;
36887
- }
36888
- return score;
36889
- }
36890
37053
  static validate(state, rubric) {
36891
- let numAnswered = 0;
36892
- let numCorrect = 0;
36893
- for (const marker of state.markers) {
36894
- const score = LabelImage.gradeMarker(marker);
36895
- if (score.hasAnswers) {
36896
- numAnswered++;
36897
- }
36898
- if (score.isCorrect) {
36899
- numCorrect++;
36900
- }
36901
- }
36902
-
36903
- // We expect all question markers to be answered before grading.
36904
- if (numAnswered !== state.markers.length) {
36905
- return {
36906
- type: "invalid",
36907
- message: null
36908
- };
36909
- }
36910
- return {
36911
- type: "points",
36912
- // Markers with no expected answers are graded as correct if user
36913
- // makes no answer selection.
36914
- earned: numCorrect === state.markers.length ? 1 : 0,
36915
- total: 1,
36916
- message: null
36917
- };
37054
+ return labelImageValidator(state);
36918
37055
  }
36919
37056
 
36920
37057
  /**
@@ -37109,7 +37246,7 @@ class LabelImage extends React__namespace.Component {
37109
37246
  this._mounted = false;
37110
37247
  }
37111
37248
  simpleValidate(rubric) {
37112
- return LabelImage.validate(this.getUserInput(), rubric);
37249
+ return labelImageValidator(this.getUserInput());
37113
37250
  }
37114
37251
  getUserInput() {
37115
37252
  const {
@@ -37127,7 +37264,7 @@ class LabelImage extends React__namespace.Component {
37127
37264
  onChange
37128
37265
  } = this.props;
37129
37266
  const updatedMarkers = markers.map(marker => {
37130
- const score = LabelImage.gradeMarker(marker);
37267
+ const score = scoreMarker(marker);
37131
37268
  return {
37132
37269
  ...marker,
37133
37270
  // Reveal correctness state for markers with answers.
@@ -37276,7 +37413,7 @@ class LabelImage extends React__namespace.Component {
37276
37413
  bottom: "top"
37277
37414
  }[markerPosition];
37278
37415
  }
37279
- const score = LabelImage.gradeMarker(marker);
37416
+ const score = scoreMarker(marker);
37280
37417
  // Once the question is answered, show markers
37281
37418
  // with correct answers, otherwise passthrough
37282
37419
  // the correctness state.
@@ -37482,9 +37619,7 @@ const LabelImageWithDependencies = /*#__PURE__*/React__namespace.forwardRef((pro
37482
37619
  // methods and instead adjust Peresus to provide these facilities through
37483
37620
  // instance methods on our Renderers.
37484
37621
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
37485
- LabelImageWithDependencies.validate = LabelImage.validate;
37486
- // @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
37487
- LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
37622
+ LabelImageWithDependencies.validate = labelImageValidator;
37488
37623
  var LabelImage$1 = {
37489
37624
  name: "label-image",
37490
37625
  displayName: "Label Image",
@@ -38273,10 +38408,10 @@ class Matcher extends React__namespace.Component {
38273
38408
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
38274
38409
  this.refs.right.moveOptionToIndex(option, index);
38275
38410
  };
38276
- simpleValidate = rubric => {
38411
+ simpleValidate(rubric) {
38277
38412
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Matcher'.
38278
38413
  return Matcher.validate(this.getUserInput(), rubric);
38279
- };
38414
+ }
38280
38415
  render() {
38281
38416
  // To minimize layout shift, we display a spinner until our math
38282
38417
  // renderer is ready to render the math inside the matcher. To
@@ -38519,7 +38654,9 @@ class Matrix extends React__namespace.Component {
38519
38654
  hasEmptyCell = true;
38520
38655
  }
38521
38656
  if (!incorrectSize) {
38522
- const validator = createValidator(solution[row][col], {
38657
+ const validator = createValidator(
38658
+ // @ts-expect-error - TS2345 - Argument of type 'number' is not assignable to parameter of type 'string'.
38659
+ solution[row][col], {
38523
38660
  simplify: true
38524
38661
  }, strings);
38525
38662
  const result = validator(supplied[row][col]);
@@ -38691,14 +38828,14 @@ class Matrix extends React__namespace.Component {
38691
38828
  }, cb);
38692
38829
  this.props.trackInteraction();
38693
38830
  };
38694
- getUserInput = () => {
38831
+ getUserInput() {
38695
38832
  return {
38696
38833
  answers: this.props.answers
38697
38834
  };
38698
- };
38699
- simpleValidate = rubric => {
38835
+ }
38836
+ simpleValidate(rubric) {
38700
38837
  return Matrix.validate(this.getUserInput(), rubric, this.context.strings);
38701
- };
38838
+ }
38702
38839
  render() {
38703
38840
  // Set the input sizes through JS so we can control the size of the
38704
38841
  // brackets. (If we set them in CSS we won't know values until the
@@ -38981,9 +39118,8 @@ const Measurer = createReactClass__default["default"]({
38981
39118
  getUserInput: function () {
38982
39119
  return {};
38983
39120
  },
38984
- simpleValidate: function (rubric) {
38985
- // TODO(joel) - I don't understand how this is useful!
38986
- return Measurer.validate(this.getUserInput(), rubric);
39121
+ simpleValidate: function () {
39122
+ return noopValidator(1);
38987
39123
  },
38988
39124
  focus: $__default["default"].noop,
38989
39125
  render: function () {
@@ -39012,14 +39148,7 @@ const Measurer = createReactClass__default["default"]({
39012
39148
  }
39013
39149
  });
39014
39150
  ___default["default"].extend(Measurer, {
39015
- validate: function (state, rubric) {
39016
- return {
39017
- type: "points",
39018
- earned: 1,
39019
- total: 1,
39020
- message: null
39021
- };
39022
- }
39151
+ validate: noopValidator(1)
39023
39152
  });
39024
39153
  const propUpgrades = {
39025
39154
  "1": v0props => {
@@ -39914,23 +40043,19 @@ class MoleculeWidget extends React__namespace.Component {
39914
40043
  static defaultProps = {
39915
40044
  rotationAngle: 0
39916
40045
  };
39917
- simpleValidate = () => {
39918
- return {
39919
- type: "points",
39920
- earned: 0,
39921
- total: 0,
39922
- message: null
39923
- };
39924
- };
39925
- getUserInput = () => {
40046
+
40047
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
40048
+ simpleValidate() {
40049
+ return noopValidator();
40050
+ }
40051
+ getUserInput() {
39926
40052
  return [];
39927
- };
39928
- validate = (state, rubric) => {
39929
- // TODO(colin): this is here as part of the interface for a component.
39930
- // Figure out if there is something more appropriate that this should
39931
- // return.
40053
+ }
40054
+
40055
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
40056
+ validate() {
39932
40057
  return noopValidator();
39933
- };
40058
+ }
39934
40059
  render() {
39935
40060
  return /*#__PURE__*/React__namespace.createElement(Molecule, {
39936
40061
  widgetId: this.props.widgetId,
@@ -40071,6 +40196,9 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
40071
40196
  });
40072
40197
  return results;
40073
40198
  });
40199
+
40200
+ // TODO: most widgets use some like Widget<Something, PerseusNumberLineWidgetOptions>
40201
+ // should this one?
40074
40202
  class NumberLine extends React__namespace.Component {
40075
40203
  static contextType = PerseusI18nContext;
40076
40204
  static defaultProps = {
@@ -40372,18 +40500,18 @@ class NumberLine extends React__namespace.Component {
40372
40500
  arrows: "->"
40373
40501
  });
40374
40502
  };
40375
- getUserInput = () => {
40503
+ getUserInput() {
40376
40504
  return {
40377
40505
  numLinePosition: this.props.numLinePosition,
40378
40506
  rel: this.props.isInequality ? this.props.rel : "eq",
40379
40507
  numDivisions: this.props.numDivisions,
40380
40508
  divisionRange: this.props.divisionRange
40381
40509
  };
40382
- };
40383
- simpleValidate = rubric => {
40510
+ }
40511
+ simpleValidate(rubric) {
40384
40512
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof NumberLine'.
40385
40513
  return NumberLine.validate(this.getUserInput(), rubric);
40386
- };
40514
+ }
40387
40515
  render() {
40388
40516
  const {
40389
40517
  strings
@@ -40443,6 +40571,8 @@ ___default["default"].extend(NumberLine, {
40443
40571
  const correctRel = rubric.correctRel || "eq";
40444
40572
  const correctPos = kmath.number.equal(state.numLinePosition, rubric.correctX || 0);
40445
40573
  const outsideAllowedRange = state.numDivisions > divisionRange[1] || state.numDivisions < divisionRange[0];
40574
+
40575
+ // TODO: I don't think isTickCrtl is a thing anymore
40446
40576
  if (state.isTickCrtl && outsideAllowedRange) {
40447
40577
  return {
40448
40578
  type: "invalid",
@@ -40695,8 +40825,8 @@ class Card extends React__namespace.Component {
40695
40825
  }))));
40696
40826
  }
40697
40827
  }
40698
- const NORMAL = "normal",
40699
- HORIZONTAL = "horizontal";
40828
+ const NORMAL = "normal";
40829
+ const HORIZONTAL = "horizontal";
40700
40830
  class Orderer extends React__namespace.Component {
40701
40831
  static defaultProps = {
40702
40832
  current: [],
@@ -40951,17 +41081,17 @@ class Orderer extends React__namespace.Component {
40951
41081
  current: list
40952
41082
  });
40953
41083
  };
40954
- getUserInput = () => {
41084
+ getUserInput() {
40955
41085
  return {
40956
41086
  current: ___default["default"].map(this.props.current, function (v) {
40957
41087
  return v.content;
40958
41088
  })
40959
41089
  };
40960
- };
40961
- simpleValidate = rubric => {
41090
+ }
41091
+ simpleValidate(rubric) {
40962
41092
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Orderer'.
40963
41093
  return Orderer.validate(this.getUserInput(), rubric);
40964
- };
41094
+ }
40965
41095
  render() {
40966
41096
  // This is the card we are currently dragging
40967
41097
  const dragging = this.state.dragging && /*#__PURE__*/React__namespace.createElement(Card
@@ -41067,14 +41197,14 @@ class Orderer extends React__namespace.Component {
41067
41197
  }
41068
41198
  }
41069
41199
  ___default["default"].extend(Orderer, {
41070
- validate: function (state, rubric) {
41071
- if (state.current.length === 0) {
41200
+ validate: function (userInput, rubric) {
41201
+ if (userInput.current.length === 0) {
41072
41202
  return {
41073
41203
  type: "invalid",
41074
41204
  message: null
41075
41205
  };
41076
41206
  }
41077
- const correct = ___default["default"].isEqual(state.current, ___default["default"].pluck(rubric.correctOptions, "content"));
41207
+ const correct = ___default["default"].isEqual(userInput.current, ___default["default"].pluck(rubric.correctOptions, "content"));
41078
41208
  return {
41079
41209
  type: "points",
41080
41210
  earned: correct ? 1 : 0,
@@ -42896,7 +43026,9 @@ class Passage extends React__namespace.Component {
42896
43026
  startLineNumbersAfter: 0,
42897
43027
  stylesAreApplied: false
42898
43028
  };
42899
- static validate(state, rubric) {
43029
+
43030
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43031
+ static validate() {
42900
43032
  return noopValidator();
42901
43033
  }
42902
43034
  componentDidMount() {
@@ -43116,8 +43248,10 @@ class Passage extends React__namespace.Component {
43116
43248
  getUserInput() {
43117
43249
  return null;
43118
43250
  }
43119
- simpleValidate(rubric) {
43120
- return Passage.validate(this.getUserInput(), rubric);
43251
+
43252
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43253
+ simpleValidate() {
43254
+ return noopValidator();
43121
43255
  }
43122
43256
 
43123
43257
  /**
@@ -43273,14 +43407,16 @@ class PassageRefTarget extends React__namespace.Component {
43273
43407
  content: "",
43274
43408
  linterContext: PerseusLinter.linterContextDefault
43275
43409
  };
43276
- static validate(state, rubric) {
43410
+
43411
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43412
+ static validate() {
43277
43413
  return noopValidator();
43278
43414
  }
43279
43415
 
43280
43416
  // TODO passage-ref-target isn't interactive; remove
43281
- getUserInput = () => {
43282
- return;
43283
- };
43417
+ getUserInput() {
43418
+ return null;
43419
+ }
43284
43420
 
43285
43421
  // TODO passage-ref-target isn't interactive; remove
43286
43422
  change = (() => {
@@ -43293,10 +43429,10 @@ class PassageRefTarget extends React__namespace.Component {
43293
43429
  };
43294
43430
  })();
43295
43431
 
43296
- // TODO passage-ref-target isn't interactive; remove
43297
- simpleValidate = rubric => {
43298
- return PassageRefTarget.validate(this.getUserInput(), rubric);
43299
- };
43432
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43433
+ simpleValidate() {
43434
+ return noopValidator();
43435
+ }
43300
43436
  render() {
43301
43437
  return /*#__PURE__*/React__namespace.createElement(Renderer, {
43302
43438
  content: this.props.content
@@ -43332,9 +43468,6 @@ var PassageRefTarget$1 = {
43332
43468
  * This is a PhET simulation widget. It is used for rendering simulations
43333
43469
  * from https://phet.colorado.edu/.
43334
43470
  */
43335
-
43336
- // For returning user input, but currently the PhET widget
43337
- // does not support accessing user input
43338
43471
  // This renders the PhET sim
43339
43472
  class PhetSimulation extends React__namespace.Component {
43340
43473
  static contextType = PerseusI18nContext;
@@ -43357,6 +43490,8 @@ class PhetSimulation extends React__namespace.Component {
43357
43490
  await this.updateSimState(this.props.url);
43358
43491
  }
43359
43492
  }
43493
+
43494
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43360
43495
  getUserInput() {
43361
43496
  return null;
43362
43497
  }
@@ -44306,13 +44441,13 @@ class Plotter extends React__namespace.Component {
44306
44441
  });
44307
44442
  });
44308
44443
  };
44309
- getUserInput = () => {
44444
+ getUserInput() {
44310
44445
  return this.state.values;
44311
- };
44312
- simpleValidate = rubric => {
44446
+ }
44447
+ simpleValidate(rubric) {
44313
44448
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Plotter'.
44314
44449
  return Plotter.validate(this.getUserInput(), rubric);
44315
- };
44450
+ }
44316
44451
  render() {
44317
44452
  // TODO(kevinb) actually compute the size of the graphie correctly and
44318
44453
  // make it that size so we don't have to add extra padding. The value
@@ -44331,8 +44466,8 @@ class Plotter extends React__namespace.Component {
44331
44466
  }
44332
44467
  }
44333
44468
  ___default["default"].extend(Plotter, {
44334
- validate: function (guess, rubric) {
44335
- if (deepEq(guess, rubric.starting)) {
44469
+ validate: function (userInput, rubric) {
44470
+ if (deepEq(userInput, rubric.starting)) {
44336
44471
  return {
44337
44472
  type: "invalid",
44338
44473
  message: null
@@ -44340,7 +44475,7 @@ ___default["default"].extend(Plotter, {
44340
44475
  }
44341
44476
  return {
44342
44477
  type: "points",
44343
- earned: deepEq(guess, rubric.correct) ? 1 : 0,
44478
+ earned: deepEq(userInput, rubric.correct) ? 1 : 0,
44344
44479
  total: 1,
44345
44480
  message: null
44346
44481
  };
@@ -44403,6 +44538,28 @@ var PythonProgram$1 = {
44403
44538
  widget: PythonProgram
44404
44539
  };
44405
44540
 
44541
+ function sorterValidator(userInput, rubric) {
44542
+ // If the sorter widget hasn't been changed yet, we treat it as "empty" which
44543
+ // prevents the "Check" button from becoming active. We want the user
44544
+ // to make a change before trying to move forward. This makes an
44545
+ // assumption that the initial order isn't the correct order! However,
44546
+ // this should be rare if it happens, and interacting with the list
44547
+ // will enable the button, so they won't be locked out of progressing.
44548
+ if (!userInput.changed) {
44549
+ return {
44550
+ type: "invalid",
44551
+ message: null
44552
+ };
44553
+ }
44554
+ const correct = Util.deepEq(userInput.options, rubric.correct);
44555
+ return {
44556
+ type: "points",
44557
+ earned: correct ? 1 : 0,
44558
+ total: 1,
44559
+ message: null
44560
+ };
44561
+ }
44562
+
44406
44563
  const {
44407
44564
  shuffle
44408
44565
  } = Util;
@@ -44416,14 +44573,8 @@ class Sorter extends React__namespace.Component {
44416
44573
  onChange: function () {},
44417
44574
  linterContext: PerseusLinter.linterContextDefault
44418
44575
  };
44419
- static validate(state, rubric) {
44420
- const correct = ___default["default"].isEqual(state.options, rubric.correct);
44421
- return {
44422
- type: "points",
44423
- earned: correct ? 1 : 0,
44424
- total: 1,
44425
- message: null
44426
- };
44576
+ static validate(userInput, rubric) {
44577
+ return sorterValidator(userInput, rubric);
44427
44578
  }
44428
44579
  state = {
44429
44580
  changed: false
@@ -44453,33 +44604,23 @@ class Sorter extends React__namespace.Component {
44453
44604
  }, 0);
44454
44605
  });
44455
44606
  };
44456
- getUserInput = () => {
44607
+ getUserInput() {
44457
44608
  // eslint-disable-next-line react/no-string-refs
44458
44609
  // @ts-expect-error - TS2339 - Property 'getOptions' does not exist on type 'ReactInstance'.
44610
+ const options = this.refs.sortable.getOptions();
44459
44611
  return {
44460
- options: this.refs.sortable.getOptions()
44612
+ options,
44613
+ changed: this.state.changed
44461
44614
  };
44462
- };
44615
+ }
44463
44616
  moveOptionToIndex = (option, index) => {
44464
44617
  // eslint-disable-next-line react/no-string-refs
44465
44618
  // @ts-expect-error - TS2339 - Property 'moveOptionToIndex' does not exist on type 'ReactInstance'.
44466
44619
  this.refs.sortable.moveOptionToIndex(option, index);
44467
44620
  };
44468
- simpleValidate = rubric => {
44469
- // If this widget hasn't been changed yet, we treat it as "empty" which
44470
- // prevents the "Check" button from becoming active. We want the user
44471
- // to make a change before trying to move forward. This makes an
44472
- // assumption that the initial order isn't the correct order! However,
44473
- // this should be rare if it happens, and interacting with the list
44474
- // will enable the button, so they won't be locked out of progressing.
44475
- if (!this.state.changed) {
44476
- return {
44477
- type: "invalid",
44478
- message: null
44479
- };
44480
- }
44481
- return Sorter.validate(this.getUserInput(), rubric);
44482
- };
44621
+ simpleValidate(rubric) {
44622
+ return sorterValidator(this.getUserInput(), rubric);
44623
+ }
44483
44624
  render() {
44484
44625
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
44485
44626
  const {
@@ -44559,9 +44700,9 @@ class Table extends React__namespace.Component {
44559
44700
  _getColumns = () => {
44560
44701
  return this.props.answers[0].length;
44561
44702
  };
44562
- getUserInput = () => {
44703
+ getUserInput() {
44563
44704
  return ___default["default"].map(this.props.answers, ___default["default"].clone);
44564
- };
44705
+ }
44565
44706
  onValueChange = (row, column, eventOrValue) => {
44566
44707
  const answers = ___default["default"].map(this.props.answers, ___default["default"].clone);
44567
44708
 
@@ -44581,10 +44722,10 @@ class Table extends React__namespace.Component {
44581
44722
  headers: headers
44582
44723
  });
44583
44724
  };
44584
- simpleValidate = rubric => {
44725
+ simpleValidate(rubric) {
44585
44726
  // @ts-expect-error - TS2339 - Property 'validate' does not exist on type 'typeof Table'.
44586
44727
  return Table.validate(this.getUserInput(), rubric, this.context.strings);
44587
- };
44728
+ }
44588
44729
  _handleFocus = inputPath => {
44589
44730
  this.props.onFocus(inputPath);
44590
44731
  };
@@ -44880,16 +45021,18 @@ class Video extends React__namespace.Component {
44880
45021
  * This is the widget's grading function.
44881
45022
  * Points for videos are tallied by the embedded video itself, in the case
44882
45023
  * of Khan Academy videos.
44883
- */
44884
- static validate(userInput, rubric) {
45024
+ */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45025
+ static validate() {
44885
45026
  return noopValidator();
44886
45027
  }
44887
45028
  getUserInput = () => {
44888
45029
  return null;
44889
45030
  };
44890
- simpleValidate = rubric => {
44891
- return Video.validate(null, rubric);
44892
- };
45031
+
45032
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45033
+ simpleValidate() {
45034
+ return noopValidator();
45035
+ }
44893
45036
  change = (() => {
44894
45037
  var _this = this;
44895
45038
  return function () {
@@ -45002,7 +45145,7 @@ const init = function (options) {
45002
45145
 
45003
45146
  // This file is processed by a Rollup plugin (replace) to inject the production
45004
45147
  const libName = "@khanacademy/perseus";
45005
- const libVersion = "34.0.0";
45148
+ const libVersion = "35.0.0";
45006
45149
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45007
45150
 
45008
45151
  const apiVersion = {
@@ -45556,9 +45699,6 @@ const defaultContext = {
45556
45699
  };
45557
45700
  const context = /*#__PURE__*/React__namespace.createContext(defaultContext);
45558
45701
 
45559
- const {
45560
- mapObject
45561
- } = objective_;
45562
45702
  class ServerItemRenderer extends React__namespace.Component {
45563
45703
  static contextType = PerseusI18nContext;
45564
45704
  // @ts-expect-error - TS2564 - Property 'questionRenderer' has no initializer and is not definitely assigned in the constructor.
@@ -45774,20 +45914,6 @@ class ServerItemRenderer extends React__namespace.Component {
45774
45914
  return this.questionRenderer.getWidgetIds();
45775
45915
  }
45776
45916
 
45777
- /**
45778
- * Returns an object mapping from widget ID to KE-style score.
45779
- * The keys of this object are the values of the array returned
45780
- * from `getWidgetIds`.
45781
- */
45782
- scoreWidgets() {
45783
- const qScore = this.questionRenderer.scoreWidgets();
45784
- const qGuess = this.questionRenderer.getUserInputForWidgets();
45785
- const state = this.questionRenderer.getSerializedState();
45786
- return mapObject(qScore, (score, id) => {
45787
- return Util.keScoreFromPerseusScore(score, qGuess[id], state);
45788
- });
45789
- }
45790
-
45791
45917
  /**
45792
45918
  * Get a representation of the current state of the item.
45793
45919
  */