@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/es/index.js CHANGED
@@ -21738,6 +21738,47 @@ const answerTypes = {
21738
21738
  forms: "pi"
21739
21739
  }
21740
21740
  };
21741
+ function inputNumberValidator(state, rubric, strings, onInputError = () => {}) {
21742
+ if (rubric.answerType == null) {
21743
+ rubric.answerType = "number";
21744
+ }
21745
+
21746
+ // note(matthewc): this will get immediately parsed again by
21747
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21748
+ // expected here
21749
+ const stringValue = `${rubric.value}`;
21750
+ const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21751
+ simplify: rubric.simplify,
21752
+ inexact: rubric.inexact || undefined,
21753
+ maxError: rubric.maxError,
21754
+ forms: answerTypes[rubric.answerType].forms
21755
+ }, strings);
21756
+
21757
+ // We may have received TeX; try to parse it before grading.
21758
+ // If `currentValue` is not TeX, this should be a no-op.
21759
+ const currentValue = ParseTex$1(state.currentValue);
21760
+ const result = val(currentValue);
21761
+
21762
+ // TODO(eater): Seems silly to translate result to this invalid/points
21763
+ // thing and immediately translate it back in ItemRenderer.scoreInput()
21764
+ if (result.empty) {
21765
+ // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
21766
+ const apiResult = onInputError == null ? void 0 : onInputError(null,
21767
+ // reserved for some widget identifier
21768
+ state.currentValue, result.message);
21769
+ return {
21770
+ type: "invalid",
21771
+ message: apiResult === false ? null : result.message
21772
+ };
21773
+ }
21774
+ return {
21775
+ type: "points",
21776
+ earned: result.correct ? 1 : 0,
21777
+ total: 1,
21778
+ message: result.message
21779
+ };
21780
+ }
21781
+
21741
21782
  const formExamples$1 = {
21742
21783
  integer: function (options, strings) {
21743
21784
  return strings.integerExample;
@@ -21828,7 +21869,7 @@ class InputNumber extends React.Component {
21828
21869
  };
21829
21870
  this.simpleValidate = (rubric, onInputError) => {
21830
21871
  onInputError = onInputError || function () {};
21831
- return InputNumber.validate(this.getUserInput(), rubric, this.context.strings, onInputError);
21872
+ return inputNumberValidator(this.getUserInput(), rubric, this.context.strings, onInputError);
21832
21873
  };
21833
21874
  this.examples = () => {
21834
21875
  const {
@@ -21841,44 +21882,7 @@ class InputNumber extends React.Component {
21841
21882
  };
21842
21883
  }
21843
21884
  static validate(state, rubric, strings, onInputError = () => {}) {
21844
- if (rubric.answerType == null) {
21845
- rubric.answerType = "number";
21846
- }
21847
-
21848
- // note(matthewc): this will get immediately parsed again by
21849
- // `KhanAnswerTypes.number.convertToPredicate`, but a string is
21850
- // expected here
21851
- const stringValue = `${rubric.value}`;
21852
- const val = KhanAnswerTypes.number.createValidatorFunctional(stringValue, {
21853
- simplify: rubric.simplify,
21854
- inexact: rubric.inexact || undefined,
21855
- maxError: rubric.maxError,
21856
- forms: answerTypes[rubric.answerType].forms
21857
- }, strings);
21858
-
21859
- // We may have received TeX; try to parse it before grading.
21860
- // If `currentValue` is not TeX, this should be a no-op.
21861
- const currentValue = ParseTex$1(state.currentValue);
21862
- const result = val(currentValue);
21863
-
21864
- // TODO(eater): Seems silly to translate result to this invalid/points
21865
- // thing and immediately translate it back in ItemRenderer.scoreInput()
21866
- if (result.empty) {
21867
- // TODO(FEI-3867): remove null-check once we have APIOptionsInternal
21868
- const apiResult = onInputError == null ? void 0 : onInputError(null,
21869
- // reserved for some widget identifier
21870
- state.currentValue, result.message);
21871
- return {
21872
- type: "invalid",
21873
- message: apiResult === false ? null : result.message
21874
- };
21875
- }
21876
- return {
21877
- type: "points",
21878
- earned: result.correct ? 1 : 0,
21879
- total: 1,
21880
- message: result.message
21881
- };
21885
+ return inputNumberValidator(state, rubric, strings, onInputError);
21882
21886
  }
21883
21887
  static getUserInputFromProps(props) {
21884
21888
  return {
@@ -22412,15 +22416,15 @@ var NumericInput$1 = {
22412
22416
  * Several widgets don't have "right"/"wrong" validation logic,
22413
22417
  * so this just says to move on past those widgets
22414
22418
  *
22415
- * TODO: widgets that use this probably shouldn't have any
22419
+ * TODO(LEMS-2396) widgets that use this probably shouldn't have any
22416
22420
  * validation logic and the thing validating an exercise
22417
22421
  * should just know to skip these
22418
22422
  */
22419
- function noopValidator() {
22423
+ function noopValidator(points = 0) {
22420
22424
  return {
22421
22425
  type: "points",
22422
- earned: 0,
22423
- total: 0,
22426
+ earned: points,
22427
+ total: points,
22424
22428
  message: null
22425
22429
  };
22426
22430
  }
@@ -22472,11 +22476,9 @@ class PassageRef extends React.Component {
22472
22476
  }
22473
22477
  }
22474
22478
  };
22475
- this.simpleValidate = rubric => {
22476
- return PassageRef.validate(this.getUserInput(), rubric);
22477
- };
22478
22479
  }
22479
- static validate(userInput, rubric) {
22480
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22481
+ static validate() {
22480
22482
  return noopValidator();
22481
22483
  }
22482
22484
  componentDidMount() {
@@ -22497,6 +22499,10 @@ class PassageRef extends React.Component {
22497
22499
  window.removeEventListener("resize", this._throttledUpdateRange);
22498
22500
  this._isMounted = false;
22499
22501
  }
22502
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
22503
+ simpleValidate() {
22504
+ return noopValidator();
22505
+ }
22500
22506
  render() {
22501
22507
  const {
22502
22508
  strings
@@ -24426,6 +24432,31 @@ var Radio = {
24426
24432
  /* eslint-disable import/no-named-as-default */
24427
24433
  var basicWidgets = [Radio, InputNumber$1, NumericInput$1, Expression$1];
24428
24434
 
24435
+ function categorizerValidator(userInput, rubric, strings) {
24436
+ let completed = true;
24437
+ let allCorrect = true;
24438
+ rubric.values.forEach((value, i) => {
24439
+ if (userInput.values[i] == null) {
24440
+ completed = false;
24441
+ }
24442
+ if (userInput.values[i] !== value) {
24443
+ allCorrect = false;
24444
+ }
24445
+ });
24446
+ if (!completed) {
24447
+ return {
24448
+ type: "invalid",
24449
+ message: strings.invalidSelection
24450
+ };
24451
+ }
24452
+ return {
24453
+ type: "points",
24454
+ earned: allCorrect ? 1 : 0,
24455
+ total: 1,
24456
+ message: null
24457
+ };
24458
+ }
24459
+
24429
24460
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
24430
24461
  class Categorizer extends React.Component {
24431
24462
  constructor(..._args) {
@@ -24441,32 +24472,11 @@ class Categorizer extends React.Component {
24441
24472
  return Categorizer.getUserInputFromProps(this.props);
24442
24473
  };
24443
24474
  this.simpleValidate = rubric => {
24444
- return Categorizer.validate(this.getUserInput(), rubric, this.context.strings);
24475
+ return categorizerValidator(this.getUserInput(), rubric, this.context.strings);
24445
24476
  };
24446
24477
  }
24447
24478
  static validate(userInput, rubric, strings) {
24448
- let completed = true;
24449
- let allCorrect = true;
24450
- rubric.values.forEach((value, i) => {
24451
- if (userInput.values[i] == null) {
24452
- completed = false;
24453
- }
24454
- if (userInput.values[i] !== value) {
24455
- allCorrect = false;
24456
- }
24457
- });
24458
- if (!completed) {
24459
- return {
24460
- type: "invalid",
24461
- message: strings.invalidSelection
24462
- };
24463
- }
24464
- return {
24465
- type: "points",
24466
- earned: allCorrect ? 1 : 0,
24467
- total: 1,
24468
- message: null
24469
- };
24479
+ return categorizerValidator(userInput, rubric, strings);
24470
24480
  }
24471
24481
  static getUserInputFromProps(props) {
24472
24482
  return {
@@ -24851,11 +24861,13 @@ class Definition extends React.Component {
24851
24861
  this.getUserInput = () => {
24852
24862
  return {};
24853
24863
  };
24854
- this.simpleValidate = rubric => {
24855
- return Definition.validate(this.getUserInput(), rubric);
24856
- };
24857
24864
  }
24858
- static validate(userInput, rubric) {
24865
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24866
+ static validate() {
24867
+ return noopValidator();
24868
+ }
24869
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
24870
+ simpleValidate() {
24859
24871
  return noopValidator();
24860
24872
  }
24861
24873
  render() {
@@ -24924,18 +24936,13 @@ class DeprecatedStandin extends React__default.Component {
24924
24936
  return {};
24925
24937
  };
24926
24938
  this.simpleValidate = rubric => {
24927
- return DeprecatedStandin.validate(this.getUserInput(), rubric);
24939
+ return noopValidator(1);
24928
24940
  };
24929
24941
  }
24930
24942
  static validate(userInput, rubric) {
24931
24943
  // Since this mean to replace an existing widget the learner
24932
24944
  // WILL earn points for this widget
24933
- return {
24934
- type: "points",
24935
- earned: 1,
24936
- total: 1,
24937
- message: null
24938
- };
24945
+ return noopValidator(1);
24939
24946
  }
24940
24947
  render() {
24941
24948
  return /*#__PURE__*/React__default.createElement("div", {
@@ -25083,11 +25090,9 @@ class Explanation extends React.Component {
25083
25090
  this.getUserInput = () => {
25084
25091
  return {};
25085
25092
  };
25086
- this.simpleValidate = rubric => {
25087
- return Explanation.validate(this.getUserInput(), rubric);
25088
- };
25089
25093
  }
25090
- static validate(userInput, rubric) {
25094
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25095
+ static validate() {
25091
25096
  return noopValidator();
25092
25097
  }
25093
25098
  componentDidMount() {
@@ -25096,6 +25101,10 @@ class Explanation extends React.Component {
25096
25101
  componentWillUnmount() {
25097
25102
  this._mounted = false;
25098
25103
  }
25104
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
25105
+ simpleValidate() {
25106
+ return noopValidator();
25107
+ }
25099
25108
  render() {
25100
25109
  const promptText = this.state.expanded ? this.props.hidePrompt : this.props.showPrompt;
25101
25110
  const caretIcon = this.state.expanded ? caretUp : caretDown;
@@ -27252,12 +27261,14 @@ class ImageWidget extends React.Component {
27252
27261
  this.getUserInput = () => {
27253
27262
  return null;
27254
27263
  };
27255
- this.simpleValidate = rubric => {
27256
- return ImageWidget.validate(this.getUserInput(), rubric);
27257
- };
27258
27264
  this.focus = () => {};
27259
27265
  }
27260
- static validate(userInput, rubric) {
27266
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27267
+ static validate() {
27268
+ return noopValidator();
27269
+ }
27270
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27271
+ simpleValidate() {
27261
27272
  return noopValidator();
27262
27273
  }
27263
27274
  // no-op
@@ -27574,11 +27585,9 @@ class Interaction extends React.Component {
27574
27585
  // anything.
27575
27586
  return {};
27576
27587
  };
27577
- this.simpleValidate = rubric => {
27578
- return Interaction.validate(this.getUserInput(), rubric);
27579
- };
27580
27588
  }
27581
- static validate(state, rubric) {
27589
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27590
+ static validate() {
27582
27591
  return noopValidator();
27583
27592
  }
27584
27593
  UNSAFE_componentWillReceiveProps(nextProps) {
@@ -27599,6 +27608,10 @@ class Interaction extends React.Component {
27599
27608
  });
27600
27609
  }
27601
27610
  }
27611
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
27612
+ simpleValidate() {
27613
+ return noopValidator();
27614
+ }
27602
27615
  render() {
27603
27616
  const range = this.props.graph.range;
27604
27617
  let labels = this.props.graph.labels;
@@ -29649,18 +29662,26 @@ function GraphLockedLabelsLayer(props) {
29649
29662
  lockedFigures
29650
29663
  } = props;
29651
29664
  return lockedFigures.map((figure, i) => {
29652
- var _flags$mafs;
29665
+ var _flags$mafs, _flags$mafs2;
29653
29666
  if (figure.type === "label") {
29654
29667
  return /*#__PURE__*/React.createElement(LockedLabel, _extends({
29655
29668
  key: `label-${i}`
29656
29669
  }, figure));
29657
29670
  }
29658
- if (flags != null && (_flags$mafs = flags["mafs"]) != null && _flags$mafs["locked-point-labels"] && figure.type === "point") {
29671
+ if (
29672
+ // Point flag + point type
29673
+ flags != null && (_flags$mafs = flags["mafs"]) != null && _flags$mafs["locked-point-labels"] && figure.type === "point" ||
29674
+ // Line flag + line type
29675
+ flags != null && (_flags$mafs2 = flags["mafs"]) != null && _flags$mafs2["locked-line-labels"] && figure.type === "line") {
29659
29676
  return /*#__PURE__*/React.createElement(React.Fragment, {
29660
- key: `point-${i}`
29677
+ key: i
29661
29678
  }, figure.labels.map((label, j) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29662
- key: `locked-figure-${i}-label-${j}`
29663
- }, label))));
29679
+ key: `${i}-label-${j}`
29680
+ }, label))), figure.type === "line" && /*#__PURE__*/React.createElement(React.Fragment, null, figure.points[0].labels.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29681
+ key: `locked-figure-${i}-point-1-label-${k}`
29682
+ }, label))), figure.points[1].labels.map((label, k) => /*#__PURE__*/React.createElement(LockedLabel, _extends({
29683
+ key: `locked-figure-${i}-point-2-label-${k}`
29684
+ }, label)))));
29664
29685
  }
29665
29686
  return null;
29666
29687
  });
@@ -30000,7 +30021,9 @@ const GraphLockedLayer = props => {
30000
30021
 
30001
30022
  const actions = {
30002
30023
  global: {
30003
- deleteIntent
30024
+ deleteIntent,
30025
+ changeInteractionMode,
30026
+ changeKeyboardInvitationVisibility
30004
30027
  },
30005
30028
  angle: {
30006
30029
  movePoint
@@ -30092,6 +30115,20 @@ function clickPoint(index) {
30092
30115
  index
30093
30116
  };
30094
30117
  }
30118
+ const CHANGE_INTERACTION_MODE = "point-change-interaction-mode";
30119
+ function changeInteractionMode(mode) {
30120
+ return {
30121
+ type: CHANGE_INTERACTION_MODE,
30122
+ mode
30123
+ };
30124
+ }
30125
+ const CHANGE_KEYBOARD_INVITATION_VISIBILITY = "change-keyboard-interaction-invitation-visibility";
30126
+ function changeKeyboardInvitationVisibility(shouldShow) {
30127
+ return {
30128
+ type: CHANGE_KEYBOARD_INVITATION_VISIBILITY,
30129
+ shouldShow
30130
+ };
30131
+ }
30095
30132
  const MOVE_ALL = "move-all";
30096
30133
  function moveAll(delta) {
30097
30134
  return {
@@ -31930,16 +31967,18 @@ const MafsGraph = props => {
31930
31967
  height
31931
31968
  },
31932
31969
  onKeyUp: event => {
31933
- if (event.key === "Backspace") {
31934
- var _graphRef$current;
31935
- dispatch(actions.global.deleteIntent());
31936
- (_graphRef$current = graphRef.current) == null || _graphRef$current.focus();
31937
- }
31970
+ handleKeyboardEvent(event, state, dispatch);
31938
31971
  },
31939
31972
  "aria-label": fullGraphAriaLabel,
31940
31973
  "aria-describedby": fullGraphAriaDescription ? descriptionId : undefined,
31941
31974
  ref: graphRef,
31942
- tabIndex: 0
31975
+ tabIndex: 0,
31976
+ onFocus: event => {
31977
+ handleFocusEvent(event, state, dispatch);
31978
+ },
31979
+ onBlur: event => {
31980
+ handleBlurEvent(event, state, dispatch);
31981
+ }
31943
31982
  }, fullGraphAriaDescription && /*#__PURE__*/React.createElement(View, {
31944
31983
  id: descriptionId,
31945
31984
  tabIndex: -1,
@@ -32001,30 +32040,63 @@ const MafsGraph = props => {
32001
32040
  }, /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.showProtractor && /*#__PURE__*/React.createElement(Protractor, null), renderGraph({
32002
32041
  state,
32003
32042
  dispatch
32004
- })))))), renderGraphControls({
32043
+ }))))), state.type === "point" && state.showKeyboardInteractionInvitation && /*#__PURE__*/React.createElement(View, {
32044
+ style: {
32045
+ textAlign: "center",
32046
+ backgroundColor: "white",
32047
+ border: "1px solid #21242C52",
32048
+ padding: "16px 0",
32049
+ boxShadow: "0px 8px 8px 0px #21242C14",
32050
+ // This translates the box to the center of the
32051
+ // graph Then backs it off by half of its
32052
+ // overall height so it's perfectly centered
32053
+ top: "50%",
32054
+ transform: "translateY(-50%)"
32055
+ }
32056
+ }, /*#__PURE__*/React.createElement(LabelMedium, null, "Press ", /*#__PURE__*/React.createElement("strong", null, "Shift + Enter"), " to interact with the graph"))), renderGraphControls({
32005
32057
  state,
32006
32058
  dispatch,
32007
32059
  width
32008
32060
  })));
32009
32061
  };
32010
- const renderPointGraphControls = props => /*#__PURE__*/React.createElement(View, {
32011
- style: {
32012
- flexDirection: "row",
32013
- width: props.width
32014
- }
32015
- }, props.state.showRemovePointButton && props.state.focusedPointIndex !== null && /*#__PURE__*/React.createElement(Button, {
32016
- id: REMOVE_BUTTON_ID,
32017
- kind: "secondary",
32018
- color: "destructive",
32019
- tabIndex: -1,
32020
- style: {
32021
- width: "100%",
32022
- marginLeft: "20px"
32023
- },
32024
- onClick: event => {
32025
- props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32026
- }
32027
- }, "Remove Point"));
32062
+ const renderPointGraphControls = props => {
32063
+ const {
32064
+ interactionMode,
32065
+ showRemovePointButton,
32066
+ focusedPointIndex
32067
+ } = props.state;
32068
+ return /*#__PURE__*/React.createElement(View, {
32069
+ style: {
32070
+ flexDirection: "row",
32071
+ width: props.width
32072
+ }
32073
+ }, interactionMode === "keyboard" && /*#__PURE__*/React.createElement(Button, {
32074
+ kind: "secondary",
32075
+ style: {
32076
+ width: "100%",
32077
+ marginLeft: "20px"
32078
+ },
32079
+ tabIndex: 0,
32080
+ onClick: () => {
32081
+ props.dispatch(actions.pointGraph.addPoint([0, 0]));
32082
+ }
32083
+ }, "Add Point"), interactionMode === "mouse" && showRemovePointButton && focusedPointIndex !== null && /*#__PURE__*/React.createElement(Button, {
32084
+ id: REMOVE_BUTTON_ID,
32085
+ kind: "secondary",
32086
+ color: "destructive"
32087
+ // This button is meant to be interacted with by the mouse only
32088
+ // Never allow learners to tab to this button
32089
+ ,
32090
+ tabIndex: -1,
32091
+ style: {
32092
+ width: "100%",
32093
+ marginLeft: "20px"
32094
+ },
32095
+ onClick: event => {
32096
+ props.dispatch(actions.pointGraph.removePoint(props.state.focusedPointIndex));
32097
+ }
32098
+ }, "Remove Point"));
32099
+ };
32028
32100
  const renderGraphControls = props => {
32029
32101
  const {
32030
32102
  state,
@@ -32048,6 +32120,34 @@ const renderGraphControls = props => {
32048
32120
  return null;
32049
32121
  }
32050
32122
  };
32123
+ function handleFocusEvent(event, state, dispatch) {
32124
+ if (state.type === "point" && state.numPoints === "unlimited") {
32125
+ if (event.target.classList.contains("mafs-graph") && state.interactionMode === "mouse") {
32126
+ dispatch(actions.global.changeKeyboardInvitationVisibility(true));
32127
+ }
32128
+ }
32129
+ }
32130
+ function handleBlurEvent(event, state, dispatch) {
32131
+ if (state.type === "point" && state.numPoints === "unlimited") {
32132
+ dispatch(actions.global.changeKeyboardInvitationVisibility(false));
32133
+ }
32134
+ }
32135
+ function handleKeyboardEvent(event, state, dispatch) {
32136
+ if (state.type === "point" && state.numPoints === "unlimited") {
32137
+ if (event.key === "Backspace") {
32138
+ dispatch(actions.global.deleteIntent());
32139
+
32140
+ // After removing a point blur
32141
+ // It would be nice if this could focus on the graph but doing so
32142
+ // would trigger the message to prompt a learner to enter keyboard mode
32143
+ document.activeElement.blur();
32144
+ } else if (event.shiftKey && event.key === "Enter") {
32145
+ dispatch(actions.global.changeInteractionMode("keyboard"));
32146
+ } else if (state.interactionMode === "keyboard" && event.key === "a") {
32147
+ dispatch(actions.pointGraph.addPoint([0, 0]));
32148
+ }
32149
+ }
32150
+ }
32051
32151
 
32052
32152
  // Calculate the difference between the min and max values of a range
32053
32153
  const getRangeDiff = range => {
@@ -32212,7 +32312,9 @@ function initializeGraphState(params) {
32212
32312
  coords: getPointCoords(graph, range, step),
32213
32313
  numPoints: graph.numPoints || 0,
32214
32314
  focusedPointIndex: null,
32215
- showRemovePointButton: false
32315
+ showRemovePointButton: false,
32316
+ interactionMode: "mouse",
32317
+ showKeyboardInteractionInvitation: false
32216
32318
  });
32217
32319
  case "circle":
32218
32320
  return _extends({}, shared, {
@@ -32482,6 +32584,10 @@ function interactiveGraphReducer(state, action) {
32482
32584
  return doDeleteIntent(state);
32483
32585
  case CLICK_POINT:
32484
32586
  return doClickPoint(state, action);
32587
+ case CHANGE_INTERACTION_MODE:
32588
+ return doChangeInteractionMode(state, action);
32589
+ case CHANGE_KEYBOARD_INVITATION_VISIBILITY:
32590
+ return doChangeKeyboardInvitationVisibility(state, action);
32485
32591
  default:
32486
32592
  throw new UnreachableCaseError(action);
32487
32593
  }
@@ -32530,6 +32636,31 @@ function doClickPoint(state, action) {
32530
32636
  }
32531
32637
  return state;
32532
32638
  }
32639
+ function doChangeInteractionMode(state, action) {
32640
+ if (state.type !== "point") {
32641
+ return state;
32642
+ }
32643
+ if (state.numPoints === "unlimited") {
32644
+ const nextKeyboardInvitation = action.mode === "keyboard" ? false : state.showKeyboardInteractionInvitation;
32645
+ return _extends({}, state, {
32646
+ interactionMode: action.mode,
32647
+ showKeyboardInteractionInvitation: nextKeyboardInvitation
32648
+ });
32649
+ }
32650
+ return state;
32651
+ }
32652
+ function doChangeKeyboardInvitationVisibility(state, action) {
32653
+ if (state.type !== "point") {
32654
+ return state;
32655
+ }
32656
+ if (state.numPoints === "unlimited") {
32657
+ return _extends({}, state, {
32658
+ showKeyboardInteractionInvitation: action.shouldShow,
32659
+ hasBeenInteractedWith: true
32660
+ });
32661
+ }
32662
+ return state;
32663
+ }
32533
32664
  function doMovePointInFigure(state, action) {
32534
32665
  switch (state.type) {
32535
32666
  case "segment":
@@ -33266,7 +33397,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
33266
33397
  const snapTo = graph.type === "polygon" ? graph.snapTo : null;
33267
33398
  const showAngles = graph.type === "polygon" ? graph.showAngles : null;
33268
33399
  const showSides = graph.type === "polygon" ? graph.showSides : null;
33269
- const startCoords = "startCoords" in graph ? graph.startCoords : null;
33400
+ const startCoords = "startCoords" in graph ? graph.startCoords : undefined;
33270
33401
  const originalPropsRef = useRef(props);
33271
33402
  const latestPropsRef = useLatestRef(props);
33272
33403
  useEffect(() => {
@@ -38629,9 +38760,8 @@ const Measurer = createReactClass({
38629
38760
  getUserInput: function () {
38630
38761
  return {};
38631
38762
  },
38632
- simpleValidate: function (rubric) {
38633
- // TODO(joel) - I don't understand how this is useful!
38634
- return Measurer.validate(this.getUserInput(), rubric);
38763
+ simpleValidate: function () {
38764
+ return noopValidator(1);
38635
38765
  },
38636
38766
  focus: $.noop,
38637
38767
  render: function () {
@@ -38660,14 +38790,7 @@ const Measurer = createReactClass({
38660
38790
  }
38661
38791
  });
38662
38792
  _.extend(Measurer, {
38663
- validate: function (state, rubric) {
38664
- return {
38665
- type: "points",
38666
- earned: 1,
38667
- total: 1,
38668
- message: null
38669
- };
38670
- }
38793
+ validate: noopValidator(1)
38671
38794
  });
38672
38795
  const propUpgrades = {
38673
38796
  "1": v0props => {
@@ -39559,23 +39682,17 @@ Molecule.contextType = PerseusI18nContext;
39559
39682
  class MoleculeWidget extends React.Component {
39560
39683
  constructor(...args) {
39561
39684
  super(...args);
39562
- this.simpleValidate = () => {
39563
- return {
39564
- type: "points",
39565
- earned: 0,
39566
- total: 0,
39567
- message: null
39568
- };
39569
- };
39570
39685
  this.getUserInput = () => {
39571
39686
  return [];
39572
39687
  };
39573
- this.validate = (state, rubric) => {
39574
- // TODO(colin): this is here as part of the interface for a component.
39575
- // Figure out if there is something more appropriate that this should
39576
- // return.
39577
- return noopValidator();
39578
- };
39688
+ }
39689
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39690
+ simpleValidate() {
39691
+ return noopValidator();
39692
+ }
39693
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
39694
+ validate() {
39695
+ return noopValidator();
39579
39696
  }
39580
39697
  render() {
39581
39698
  return /*#__PURE__*/React.createElement(Molecule, {
@@ -42539,7 +42656,8 @@ class Passage extends React.Component {
42539
42656
  });
42540
42657
  };
42541
42658
  }
42542
- static validate(state, rubric) {
42659
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42660
+ static validate() {
42543
42661
  return noopValidator();
42544
42662
  }
42545
42663
  componentDidMount() {
@@ -42755,8 +42873,10 @@ class Passage extends React.Component {
42755
42873
  getUserInput() {
42756
42874
  return null;
42757
42875
  }
42758
- simpleValidate(rubric) {
42759
- return Passage.validate(this.getUserInput(), rubric);
42876
+
42877
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
42878
+ simpleValidate() {
42879
+ return noopValidator();
42760
42880
  }
42761
42881
 
42762
42882
  /**
@@ -42926,12 +43046,13 @@ class PassageRefTarget extends React.Component {
42926
43046
  this.change = (...args) => {
42927
43047
  return change.apply(this, args);
42928
43048
  };
42929
- // TODO passage-ref-target isn't interactive; remove
42930
- this.simpleValidate = rubric => {
42931
- return PassageRefTarget.validate(this.getUserInput(), rubric);
42932
- };
42933
43049
  }
42934
- static validate(state, rubric) {
43050
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43051
+ static validate() {
43052
+ return noopValidator();
43053
+ }
43054
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
43055
+ simpleValidate() {
42935
43056
  return noopValidator();
42936
43057
  }
42937
43058
  render() {
@@ -44529,9 +44650,6 @@ class Video extends React.Component {
44529
44650
  this.getUserInput = () => {
44530
44651
  return null;
44531
44652
  };
44532
- this.simpleValidate = rubric => {
44533
- return Video.validate(null, rubric);
44534
- };
44535
44653
  this.change = (...args) => {
44536
44654
  // @ts-expect-error - TS2345 - Argument of type 'readonly unknown[]' is not assignable to parameter of type 'any[]'.
44537
44655
  return change.apply(this, args);
@@ -44541,8 +44659,12 @@ class Video extends React.Component {
44541
44659
  * This is the widget's grading function.
44542
44660
  * Points for videos are tallied by the embedded video itself, in the case
44543
44661
  * of Khan Academy videos.
44544
- */
44545
- static validate(userInput, rubric) {
44662
+ */ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
44663
+ static validate() {
44664
+ return noopValidator();
44665
+ }
44666
+ // TODO (LEMS-2396): remove validation logic from widgets that don't validate
44667
+ simpleValidate() {
44546
44668
  return noopValidator();
44547
44669
  }
44548
44670
  render() {
@@ -44648,7 +44770,7 @@ const init = function init(options) {
44648
44770
 
44649
44771
  // This file is processed by a Rollup plugin (replace) to inject the production
44650
44772
  const libName = "@khanacademy/perseus";
44651
- const libVersion = "34.0.0";
44773
+ const libVersion = "34.1.0";
44652
44774
  addLibraryVersionToPerseusDebug(libName, libVersion);
44653
44775
 
44654
44776
  const apiVersion = {