@khanacademy/perseus 34.0.0 → 34.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 (30) hide show
  1. package/dist/es/index.js +287 -165
  2. package/dist/es/index.js.map +1 -1
  3. package/dist/index.js +303 -161
  4. package/dist/index.js.map +1 -1
  5. package/dist/perseus-api.d.ts +1 -0
  6. package/dist/perseus-types.d.ts +1 -0
  7. package/dist/server-item-renderer.d.ts +1 -0
  8. package/dist/types.d.ts +1 -1
  9. package/dist/widgets/__shared__/noop-validator.d.ts +2 -2
  10. package/dist/widgets/categorizer/categorizer-validator.d.ts +5 -0
  11. package/dist/widgets/categorizer/categorizer.d.ts +1 -4
  12. package/dist/widgets/categorizer/categorizer.types.d.ts +5 -0
  13. package/dist/widgets/definition/definition.d.ts +3 -3
  14. package/dist/widgets/explanation/explanation.d.ts +3 -5
  15. package/dist/widgets/expression/expression.d.ts +6 -0
  16. package/dist/widgets/image/image.d.ts +3 -3
  17. package/dist/widgets/input-number/input-number-validator.d.ts +41 -0
  18. package/dist/widgets/input-number/input-number.d.ts +1 -1
  19. package/dist/widgets/input-number/input-number.types.d.ts +2 -0
  20. package/dist/widgets/interaction/interaction.d.ts +3 -3
  21. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +7 -5
  22. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +16 -1
  23. package/dist/widgets/interactive-graphs/types.d.ts +3 -0
  24. package/dist/widgets/label-image/label-image.d.ts +2 -0
  25. package/dist/widgets/molecule/molecule.d.ts +2 -2
  26. package/dist/widgets/passage/passage.d.ts +3 -3
  27. package/dist/widgets/passage-ref/passage-ref.d.ts +3 -3
  28. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +2 -2
  29. package/dist/widgets/video/video.d.ts +3 -4
  30. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21972,6 +21972,48 @@ const answerTypes = {
21972
21972
  forms: "pi"
21973
21973
  }
21974
21974
  };
21975
+ function inputNumberValidator(state, rubric, strings) {
21976
+ let onInputError = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : () => {};
21977
+ if (rubric.answerType == null) {
21978
+ rubric.answerType = "number";
21979
+ }
21980
+
21981
+ // note(matthewc): this will get immediately parsed again by
21982
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21983
+ // expected here
21984
+ const stringValue = `${rubric.value}`;
21985
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21986
+ simplify: rubric.simplify,
21987
+ inexact: rubric.inexact || undefined,
21988
+ maxError: rubric.maxError,
21989
+ forms: answerTypes[rubric.answerType].forms
21990
+ }, strings);
21991
+
21992
+ // We may have received TeX; try to parse it before grading.
21993
+ // If `currentValue` is not TeX, this should be a no-op.
21994
+ const currentValue = ParseTex$1(state.currentValue);
21995
+ const result = val(currentValue);
21996
+
21997
+ // TODO(eater): Seems silly to translate result to this invalid/points
21998
+ // thing and immediately translate it back in ItemRenderer.scoreInput()
21999
+ if (result.empty) {
22000
+ // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
22001
+ const apiResult = onInputError?.(null,
22002
+ // reserved for some widget identifier
22003
+ state.currentValue, result.message);
22004
+ return {
22005
+ type: "invalid",
22006
+ message: apiResult === false ? null : result.message
22007
+ };
22008
+ }
22009
+ return {
22010
+ type: "points",
22011
+ earned: result.correct ? 1 : 0,
22012
+ total: 1,
22013
+ message: result.message
22014
+ };
22015
+ }
22016
+
21975
22017
  const formExamples$1 = {
21976
22018
  integer: function (options, strings) {
21977
22019
  return strings.integerExample;
@@ -22015,44 +22057,7 @@ class InputNumber extends React__namespace.Component {
22015
22057
  };
22016
22058
  static validate(state, rubric, strings) {
22017
22059
  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
- };
22060
+ return inputNumberValidator(state, rubric, strings, onInputError);
22056
22061
  }
22057
22062
  static getUserInputFromProps(props) {
22058
22063
  return {
@@ -22126,7 +22131,7 @@ class InputNumber extends React__namespace.Component {
22126
22131
  };
22127
22132
  simpleValidate = (rubric, onInputError) => {
22128
22133
  onInputError = onInputError || function () {};
22129
- return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
22134
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings, onInputError);
22130
22135
  };
22131
22136
  examples = () => {
22132
22137
  const {
@@ -22635,15 +22640,16 @@ var NumericInput$1 = {
22635
22640
  * Several widgets don't have "right"/"wrong" validation logic,
22636
22641
  * so this just says to move on past those widgets
22637
22642
  *
22638
- * TODO: widgets that use this probably shouldn't have any
22643
+ * TODO(LEMS-2396) widgets that use this probably shouldn't have any
22639
22644
  * validation logic and the thing validating an exercise
22640
22645
  * should just know to skip these
22641
22646
  */
22642
22647
  function noopValidator() {
22648
+ let points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
22643
22649
  return {
22644
22650
  type: "points",
22645
- earned: 0,
22646
- total: 0,
22651
+ earned: points,
22652
+ total: points,
22647
22653
  message: null
22648
22654
  };
22649
22655
  }
@@ -22665,7 +22671,9 @@ class PassageRef extends React__namespace.Component {
22665
22671
  lineRange: null,
22666
22672
  content: null
22667
22673
  };
22668
- static validate(userInput, rubric) {
22674
+
22675
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22676
+ static validate() {
22669
22677
  return noopValidator();
22670
22678
  }
22671
22679
  componentDidMount() {
@@ -22727,9 +22735,11 @@ class PassageRef extends React__namespace.Component {
22727
22735
  }
22728
22736
  }
22729
22737
  };
22730
- simpleValidate = rubric => {
22731
- return PassageRef.validate(this.getUserInput(), rubric);
22732
- };
22738
+
22739
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22740
+ simpleValidate() {
22741
+ return noopValidator();
22742
+ }
22733
22743
  render() {
22734
22744
  const {
22735
22745
  strings
@@ -24676,6 +24686,31 @@ var Radio = {
24676
24686
  /* eslint-disable import/no-named-as-default */
24677
24687
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
24678
24688
 
24689
+ function categorizerValidator(userInput, rubric, strings) {
24690
+ let completed = true;
24691
+ let allCorrect = true;
24692
+ rubric.values.forEach((value, i) => {
24693
+ if (userInput.values[i] == null) {
24694
+ completed = false;
24695
+ }
24696
+ if (userInput.values[i] !== value) {
24697
+ allCorrect = false;
24698
+ }
24699
+ });
24700
+ if (!completed) {
24701
+ return {
24702
+ type: "invalid",
24703
+ message: strings.invalidSelection
24704
+ };
24705
+ }
24706
+ return {
24707
+ type: "points",
24708
+ earned: allCorrect ? 1 : 0,
24709
+ total: 1,
24710
+ message: null
24711
+ };
24712
+ }
24713
+
24679
24714
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
24680
24715
  class Categorizer extends React__namespace.Component {
24681
24716
  static contextType = PerseusI18nContext;
@@ -24689,28 +24724,7 @@ class Categorizer extends React__namespace.Component {
24689
24724
  uniqueId: ___default["default"].uniqueId("perseus_radio_")
24690
24725
  };
24691
24726
  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
- };
24727
+ return categorizerValidator(userInput, rubric, strings);
24714
24728
  }
24715
24729
  static getUserInputFromProps(props) {
24716
24730
  return {
@@ -24738,7 +24752,7 @@ class Categorizer extends React__namespace.Component {
24738
24752
  this.props.trackInteraction();
24739
24753
  }
24740
24754
  simpleValidate = rubric => {
24741
- return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24755
+ return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24742
24756
  };
24743
24757
  render() {
24744
24758
  const self = this;
@@ -25108,15 +25122,19 @@ class Definition extends React__namespace.Component {
25108
25122
  togglePrompt: "define me",
25109
25123
  definition: "definition goes here"
25110
25124
  };
25111
- static validate(userInput, rubric) {
25125
+
25126
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25127
+ static validate() {
25112
25128
  return noopValidator();
25113
25129
  }
25114
25130
  getUserInput = () => {
25115
25131
  return {};
25116
25132
  };
25117
- simpleValidate = rubric => {
25118
- return Definition.validate(this.getUserInput(), rubric);
25119
- };
25133
+
25134
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25135
+ simpleValidate() {
25136
+ return noopValidator();
25137
+ }
25120
25138
  render() {
25121
25139
  return /*#__PURE__*/React__namespace.createElement(DefinitionConsumer, null, _ref => {
25122
25140
  let {
@@ -25182,18 +25200,13 @@ class DeprecatedStandin extends React__default["default"].Component {
25182
25200
  static validate(userInput, rubric) {
25183
25201
  // Since this mean to replace an existing widget the learner
25184
25202
  // WILL earn points for this widget
25185
- return {
25186
- type: "points",
25187
- earned: 1,
25188
- total: 1,
25189
- message: null
25190
- };
25203
+ return noopValidator(1);
25191
25204
  }
25192
25205
  getUserInput = () => {
25193
25206
  return {};
25194
25207
  };
25195
25208
  simpleValidate = rubric => {
25196
- return DeprecatedStandin.validate(this.getUserInput(), rubric);
25209
+ return noopValidator(1);
25197
25210
  };
25198
25211
  render() {
25199
25212
  return /*#__PURE__*/React__default["default"].createElement("div", {
@@ -25331,7 +25344,9 @@ class Explanation extends React__namespace.Component {
25331
25344
  widgets: {},
25332
25345
  linterContext: PerseusLinter.linterContextDefault
25333
25346
  };
25334
- static validate(userInput, rubric) {
25347
+
25348
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25349
+ static validate() {
25335
25350
  return noopValidator();
25336
25351
  }
25337
25352
  componentDidMount() {
@@ -25358,9 +25373,11 @@ class Explanation extends React__namespace.Component {
25358
25373
  getUserInput = () => {
25359
25374
  return {};
25360
25375
  };
25361
- simpleValidate = rubric => {
25362
- return Explanation.validate(this.getUserInput(), rubric);
25363
- };
25376
+
25377
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25378
+ simpleValidate() {
25379
+ return noopValidator();
25380
+ }
25364
25381
  render() {
25365
25382
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
25366
25383
  const caretIcon = this.state.expanded ? caretUp__default["default"] : caretDown__default["default"];
@@ -27538,7 +27555,9 @@ class ImageWidget extends React__namespace.Component {
27538
27555
  caption: "",
27539
27556
  linterContext: PerseusLinter.linterContextDefault
27540
27557
  };
27541
- static validate(userInput, rubric) {
27558
+
27559
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27560
+ static validate() {
27542
27561
  return noopValidator();
27543
27562
  }
27544
27563
  change = (() => {
@@ -27553,9 +27572,11 @@ class ImageWidget extends React__namespace.Component {
27553
27572
  getUserInput = () => {
27554
27573
  return null;
27555
27574
  };
27556
- simpleValidate = rubric => {
27557
- return ImageWidget.validate(this.getUserInput(), rubric);
27558
- };
27575
+
27576
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27577
+ simpleValidate() {
27578
+ return noopValidator();
27579
+ }
27559
27580
  focus = () => {}; // no-op
27560
27581
 
27561
27582
  render() {
@@ -27781,7 +27802,9 @@ class Interaction extends React__namespace.Component {
27781
27802
  },
27782
27803
  elements: []
27783
27804
  };
27784
- static validate(state, rubric) {
27805
+
27806
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27807
+ static validate() {
27785
27808
  return noopValidator();
27786
27809
  }
27787
27810
  state = {
@@ -27898,9 +27921,11 @@ class Interaction extends React__namespace.Component {
27898
27921
  // anything.
27899
27922
  return {};
27900
27923
  };
27901
- simpleValidate = rubric => {
27902
- return Interaction.validate(this.getUserInput(), rubric);
27903
- };
27924
+
27925
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27926
+ simpleValidate() {
27927
+ return noopValidator();
27928
+ }
27904
27929
  render() {
27905
27930
  const range = this.props.graph.range;
27906
27931
  let labels = this.props.graph.labels;
@@ -29963,12 +29988,20 @@ function GraphLockedLabelsLayer(props) {
29963
29988
  key: `label-${i}`
29964
29989
  }, figure));
29965
29990
  }
29966
- if (flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point") {
29991
+ if (
29992
+ // Point flag + point type
29993
+ flags?.["mafs"]?.["locked-point-labels"] && figure.type === "point" ||
29994
+ // Line flag + line type
29995
+ flags?.["mafs"]?.["locked-line-labels"] && figure.type === "line") {
29967
29996
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, {
29968
- key: `point-${i}`
29997
+ key: i
29969
29998
  }, figure.labels.map((label, j) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
29970
- key: `locked-figure-${i}-label-${j}`
29971
- }, label))));
29999
+ key: `${i}-label-${j}`
30000
+ }, 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({
30001
+ key: `locked-figure-${i}-point-1-label-${k}`
30002
+ }, label))), figure.points[1].labels.map((label, k) => /*#__PURE__*/React__namespace.createElement(LockedLabel, _extends({
30003
+ key: `locked-figure-${i}-point-2-label-${k}`
30004
+ }, label)))));
29972
30005
  }
29973
30006
  return null;
29974
30007
  });
@@ -30308,7 +30341,9 @@ const GraphLockedLayer = props => {
30308
30341
 
30309
30342
  const actions = {
30310
30343
  global: {
30311
- deleteIntent
30344
+ deleteIntent,
30345
+ changeInteractionMode,
30346
+ changeKeyboardInvitationVisibility
30312
30347
  },
30313
30348
  angle: {
30314
30349
  movePoint
@@ -30400,6 +30435,20 @@ function clickPoint(index) {
30400
30435
  index
30401
30436
  };
30402
30437
  }
30438
+ const CHANGE_INTERACTION_MODE = "point-change-interaction-mode";
30439
+ function changeInteractionMode(mode) {
30440
+ return {
30441
+ type: CHANGE_INTERACTION_MODE,
30442
+ mode
30443
+ };
30444
+ }
30445
+ const CHANGE_KEYBOARD_INVITATION_VISIBILITY = "change-keyboard-interaction-invitation-visibility";
30446
+ function changeKeyboardInvitationVisibility(shouldShow) {
30447
+ return {
30448
+ type: CHANGE_KEYBOARD_INVITATION_VISIBILITY,
30449
+ shouldShow
30450
+ };
30451
+ }
30403
30452
  const MOVE_ALL = "move-all";
30404
30453
  function moveAll(delta) {
30405
30454
  return {
@@ -32257,15 +32306,18 @@ const MafsGraph = props => {
32257
32306
  height
32258
32307
  },
32259
32308
  onKeyUp: event => {
32260
- if (event.key === "Backspace") {
32261
- dispatch(actions.global.deleteIntent());
32262
- graphRef.current?.focus();
32263
- }
32309
+ handleKeyboardEvent(event, state, dispatch);
32264
32310
  },
32265
32311
  "aria-label": fullGraphAriaLabel,
32266
32312
  "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
32267
32313
  ref: graphRef,
32268
- tabIndex: 0
32314
+ tabIndex: 0,
32315
+ onFocus: event => {
32316
+ handleFocusEvent(event, state, dispatch);
32317
+ },
32318
+ onBlur: event => {
32319
+ handleBlurEvent(event, state, dispatch);
32320
+ }
32269
32321
  }, fullGraphAriaDescription && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32270
32322
  id: descriptionId,
32271
32323
  tabIndex: -1,
@@ -32327,30 +32379,63 @@ const MafsGraph = props => {
32327
32379
  }, /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React__namespace.createElement(Protractor, null), renderGraph({
32328
32380
  state,
32329
32381
  dispatch
32330
- })))))), renderGraphControls({
32382
+ }))))), state.type === "point" && state.showKeyboardInteractionInvitation && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32383
+ style: {
32384
+ textAlign: "center",
32385
+ backgroundColor: "white",
32386
+ border: "1px solid #21242C52",
32387
+ padding: "16px 0",
32388
+ boxShadow: "0px 8px 8px 0px #21242C14",
32389
+ // This translates the box to the center of the
32390
+ // graph Then backs it off by half of its
32391
+ // overall height so it's perfectly centered
32392
+ top: "50%",
32393
+ transform: "translateY(-50%)"
32394
+ }
32395
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelMedium, null, "Press ", /*#__PURE__*/React__namespace.createElement("strong", null, "Shift + Enter"), " to interact with the graph"))), renderGraphControls({
32331
32396
  state,
32332
32397
  dispatch,
32333
32398
  width
32334
32399
  })));
32335
32400
  };
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"));
32401
+ const renderPointGraphControls = props => {
32402
+ const {
32403
+ interactionMode,
32404
+ showRemovePointButton,
32405
+ focusedPointIndex
32406
+ } = props.state;
32407
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
32408
+ style: {
32409
+ flexDirection: "row",
32410
+ width: props.width
32411
+ }
32412
+ }, interactionMode === "keyboard" && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32413
+ kind: "secondary",
32414
+ style: {
32415
+ width: "100%",
32416
+ marginLeft: "20px"
32417
+ },
32418
+ tabIndex: 0,
32419
+ onClick: () => {
32420
+ props.dispatch(actions.pointGraph.addPoint([0, 0]));
32421
+ }
32422
+ }, "Add Point"), interactionMode === "mouse" && showRemovePointButton && focusedPointIndex !== null && /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
32423
+ id: REMOVE_BUTTON_ID,
32424
+ kind: "secondary",
32425
+ color: "destructive"
32426
+ // This button is meant to be interacted with by the mouse only
32427
+ // Never allow learners to tab to this button
32428
+ ,
32429
+ tabIndex: -1,
32430
+ style: {
32431
+ width: "100%",
32432
+ marginLeft: "20px"
32433
+ },
32434
+ onClick: event => {
32435
+ props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32436
+ }
32437
+ }, "Remove Point"));
32438
+ };
32354
32439
  const renderGraphControls = props => {
32355
32440
  const {
32356
32441
  state,
@@ -32374,6 +32459,34 @@ const renderGraphControls = props => {
32374
32459
  return null;
32375
32460
  }
32376
32461
  };
32462
+ function handleFocusEvent(event, state, dispatch) {
32463
+ if (state.type === "point" && state.numPoints === "unlimited") {
32464
+ if (event.target.classList.contains("mafs-graph") && state.interactionMode === "mouse") {
32465
+ dispatch(actions.global.changeKeyboardInvitationVisibility(true));
32466
+ }
32467
+ }
32468
+ }
32469
+ function handleBlurEvent(event, state, dispatch) {
32470
+ if (state.type === "point" && state.numPoints === "unlimited") {
32471
+ dispatch(actions.global.changeKeyboardInvitationVisibility(false));
32472
+ }
32473
+ }
32474
+ function handleKeyboardEvent(event, state, dispatch) {
32475
+ if (state.type === "point" && state.numPoints === "unlimited") {
32476
+ if (event.key === "Backspace") {
32477
+ dispatch(actions.global.deleteIntent());
32478
+
32479
+ // After removing a point blur
32480
+ // It would be nice if this could focus on the graph but doing so
32481
+ // would trigger the message to prompt a learner to enter keyboard mode
32482
+ document.activeElement.blur();
32483
+ } else if (event.shiftKey && event.key === "Enter") {
32484
+ dispatch(actions.global.changeInteractionMode("keyboard"));
32485
+ } else if (state.interactionMode === "keyboard" && event.key === "a") {
32486
+ dispatch(actions.pointGraph.addPoint([0, 0]));
32487
+ }
32488
+ }
32489
+ }
32377
32490
 
32378
32491
  // Calculate the difference between the min and max values of a range
32379
32492
  const getRangeDiff = range => {
@@ -32543,7 +32656,9 @@ function initializeGraphState(params) {
32543
32656
  coords: getPointCoords(graph, range, step),
32544
32657
  numPoints: graph.numPoints || 0,
32545
32658
  focusedPointIndex: null,
32546
- showRemovePointButton: false
32659
+ showRemovePointButton: false,
32660
+ interactionMode: "mouse",
32661
+ showKeyboardInteractionInvitation: false
32547
32662
  };
32548
32663
  case "circle":
32549
32664
  return {
@@ -32816,6 +32931,10 @@ function interactiveGraphReducer(state, action) {
32816
32931
  return doDeleteIntent(state);
32817
32932
  case CLICK_POINT:
32818
32933
  return doClickPoint(state, action);
32934
+ case CHANGE_INTERACTION_MODE:
32935
+ return doChangeInteractionMode(state, action);
32936
+ case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
32937
+ return doChangeKeyboardInvitationVisibility(state, action);
32819
32938
  default:
32820
32939
  throw new wonderStuffCore.UnreachableCaseError(action);
32821
32940
  }
@@ -32867,6 +32986,33 @@ function doClickPoint(state, action) {
32867
32986
  }
32868
32987
  return state;
32869
32988
  }
32989
+ function doChangeInteractionMode(state, action) {
32990
+ if (state.type !== "point") {
32991
+ return state;
32992
+ }
32993
+ if (state.numPoints === "unlimited") {
32994
+ const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
32995
+ return {
32996
+ ...state,
32997
+ interactionMode: action.mode,
32998
+ showKeyboardInteractionInvitation: nextKeyboardInvitation
32999
+ };
33000
+ }
33001
+ return state;
33002
+ }
33003
+ function doChangeKeyboardInvitationVisibility(state, action) {
33004
+ if (state.type !== "point") {
33005
+ return state;
33006
+ }
33007
+ if (state.numPoints === "unlimited") {
33008
+ return {
33009
+ ...state,
33010
+ showKeyboardInteractionInvitation: action.shouldShow,
33011
+ hasBeenInteractedWith: true
33012
+ };
33013
+ }
33014
+ return state;
33015
+ }
32870
33016
  function doMovePointInFigure(state, action) {
32871
33017
  switch (state.type) {
32872
33018
  case "segment":
@@ -33635,7 +33781,7 @@ const StatefulMafsGraph = /*#__PURE__*/React__namespace.forwardRef((props, ref)
33635
33781
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
33636
33782
  const showAngles = graph.type === "polygon" ? graph.showAngles : null;
33637
33783
  const showSides = graph.type === "polygon" ? graph.showSides : null;
33638
- const startCoords = "startCoords" in graph ? graph.startCoords : null;
33784
+ const startCoords = "startCoords" in graph ? graph.startCoords : undefined;
33639
33785
  const originalPropsRef = React.useRef(props);
33640
33786
  const latestPropsRef = wonderBlocksCore.useLatestRef(props);
33641
33787
  React.useEffect(() => {
@@ -38981,9 +39127,8 @@ const Measurer = createReactClass__default["default"]({
38981
39127
  getUserInput: function () {
38982
39128
  return {};
38983
39129
  },
38984
- simpleValidate: function (rubric) {
38985
- // TODO(joel) - I don't understand how this is useful!
38986
- return Measurer.validate(this.getUserInput(), rubric);
39130
+ simpleValidate: function () {
39131
+ return noopValidator(1);
38987
39132
  },
38988
39133
  focus: $__default["default"].noop,
38989
39134
  render: function () {
@@ -39012,14 +39157,7 @@ const Measurer = createReactClass__default["default"]({
39012
39157
  }
39013
39158
  });
39014
39159
  ___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
- }
39160
+ validate: noopValidator(1)
39023
39161
  });
39024
39162
  const propUpgrades = {
39025
39163
  "1": v0props => {
@@ -39914,23 +40052,19 @@ class MoleculeWidget extends React__namespace.Component {
39914
40052
  static defaultProps = {
39915
40053
  rotationAngle: 0
39916
40054
  };
39917
- simpleValidate = () => {
39918
- return {
39919
- type: "points",
39920
- earned: 0,
39921
- total: 0,
39922
- message: null
39923
- };
39924
- };
40055
+
40056
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
40057
+ simpleValidate() {
40058
+ return noopValidator();
40059
+ }
39925
40060
  getUserInput = () => {
39926
40061
  return [];
39927
40062
  };
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.
40063
+
40064
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
40065
+ validate() {
39932
40066
  return noopValidator();
39933
- };
40067
+ }
39934
40068
  render() {
39935
40069
  return /*#__PURE__*/React__namespace.createElement(Molecule, {
39936
40070
  widgetId: this.props.widgetId,
@@ -42896,7 +43030,9 @@ class Passage extends React__namespace.Component {
42896
43030
  startLineNumbersAfter: 0,
42897
43031
  stylesAreApplied: false
42898
43032
  };
42899
- static validate(state, rubric) {
43033
+
43034
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43035
+ static validate() {
42900
43036
  return noopValidator();
42901
43037
  }
42902
43038
  componentDidMount() {
@@ -43116,8 +43252,10 @@ class Passage extends React__namespace.Component {
43116
43252
  getUserInput() {
43117
43253
  return null;
43118
43254
  }
43119
- simpleValidate(rubric) {
43120
- return Passage.validate(this.getUserInput(), rubric);
43255
+
43256
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43257
+ simpleValidate() {
43258
+ return noopValidator();
43121
43259
  }
43122
43260
 
43123
43261
  /**
@@ -43273,7 +43411,9 @@ class PassageRefTarget extends React__namespace.Component {
43273
43411
  content: "",
43274
43412
  linterContext: PerseusLinter.linterContextDefault
43275
43413
  };
43276
- static validate(state, rubric) {
43414
+
43415
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43416
+ static validate() {
43277
43417
  return noopValidator();
43278
43418
  }
43279
43419
 
@@ -43293,10 +43433,10 @@ class PassageRefTarget extends React__namespace.Component {
43293
43433
  };
43294
43434
  })();
43295
43435
 
43296
- // TODO passage-ref-target isn't interactive; remove
43297
- simpleValidate = rubric => {
43298
- return PassageRefTarget.validate(this.getUserInput(), rubric);
43299
- };
43436
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43437
+ simpleValidate() {
43438
+ return noopValidator();
43439
+ }
43300
43440
  render() {
43301
43441
  return /*#__PURE__*/React__namespace.createElement(Renderer, {
43302
43442
  content: this.props.content
@@ -44880,16 +45020,18 @@ class Video extends React__namespace.Component {
44880
45020
  * This is the widget's grading function.
44881
45021
  * Points for videos are tallied by the embedded video itself, in the case
44882
45022
  * of Khan Academy videos.
44883
- */
44884
- static validate(userInput, rubric) {
45023
+ */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45024
+ static validate() {
44885
45025
  return noopValidator();
44886
45026
  }
44887
45027
  getUserInput = () => {
44888
45028
  return null;
44889
45029
  };
44890
- simpleValidate = rubric => {
44891
- return Video.validate(null, rubric);
44892
- };
45030
+
45031
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
45032
+ simpleValidate() {
45033
+ return noopValidator();
45034
+ }
44893
45035
  change = (() => {
44894
45036
  var _this = this;
44895
45037
  return function () {
@@ -45002,7 +45144,7 @@ const init = function (options) {
45002
45144
 
45003
45145
  // This file is processed by a Rollup plugin (replace) to inject the production
45004
45146
  const libName = "@khanacademy/perseus";
45005
- const libVersion = "34.0.0";
45147
+ const libVersion = "34.1.0";
45006
45148
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45007
45149
 
45008
45150
  const apiVersion = {