@khanacademy/perseus 43.1.0 → 45.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 (57) hide show
  1. package/dist/all-widgets.d.ts +2 -23
  2. package/dist/basic-widgets.d.ts +2 -23
  3. package/dist/components/graph.d.ts +1 -0
  4. package/dist/components/icon.d.ts +0 -1
  5. package/dist/components/multi-button-group.d.ts +1 -0
  6. package/dist/components/tooltip.d.ts +1 -0
  7. package/dist/es/index.js +276 -212
  8. package/dist/es/index.js.map +1 -1
  9. package/dist/es/strings.js +4 -2
  10. package/dist/es/strings.js.map +1 -1
  11. package/dist/extra-widgets.d.ts +2 -23
  12. package/dist/index.d.ts +4 -2
  13. package/dist/index.js +273 -205
  14. package/dist/index.js.map +1 -1
  15. package/dist/interactive2/interactive-util.d.ts +1 -1
  16. package/dist/interactive2/movable-point-options.d.ts +2 -2
  17. package/dist/interactive2/movable-point.d.ts +7 -7
  18. package/dist/interactive2/objective_.d.ts +1 -1
  19. package/dist/jipt-paragraphs.d.ts +2 -2
  20. package/dist/perseus-api.d.ts +2 -77
  21. package/dist/perseus-types.d.ts +3 -10
  22. package/dist/renderer-util.d.ts +4 -2
  23. package/dist/renderer.d.ts +6 -10
  24. package/dist/server-item-renderer.d.ts +58 -50
  25. package/dist/strings.d.ts +1 -0
  26. package/dist/strings.js +4 -2
  27. package/dist/strings.js.map +1 -1
  28. package/dist/styles/global-styles.d.ts +0 -1
  29. package/dist/styles/shared.d.ts +0 -1
  30. package/dist/traversal.d.ts +1 -1
  31. package/dist/types.d.ts +5 -0
  32. package/dist/util/a11y.d.ts +0 -1
  33. package/dist/util/debounce.d.ts +1 -1
  34. package/dist/util/parse-perseus-json/perseus-parsers/graded-group-widget.d.ts +2 -2
  35. package/dist/widgets/definition/definition.d.ts +1 -1
  36. package/dist/widgets/deprecated-standin/deprecated-standin.d.ts +1 -1
  37. package/dist/widgets/dropdown/dropdown.d.ts +9 -1
  38. package/dist/widgets/explanation/explanation.d.ts +1 -1
  39. package/dist/widgets/expression/expression.d.ts +92 -217
  40. package/dist/widgets/group/group.d.ts +1 -1
  41. package/dist/widgets/image/image.d.ts +1 -1
  42. package/dist/widgets/interaction/interaction.d.ts +1 -1
  43. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +1 -1
  44. package/dist/widgets/label-image/answer-pill.d.ts +0 -1
  45. package/dist/widgets/label-image/label-image.d.ts +5 -65
  46. package/dist/widgets/label-image/marker.d.ts +0 -1
  47. package/dist/widgets/measurer/measurer.d.ts +1 -1
  48. package/dist/widgets/molecule/molecule.d.ts +1 -1
  49. package/dist/widgets/passage/passage-markdown.d.ts +2 -2
  50. package/dist/widgets/passage/passage.d.ts +1 -1
  51. package/dist/widgets/passage-ref/passage-ref.d.ts +1 -1
  52. package/dist/widgets/passage-ref-target/passage-ref-target.d.ts +1 -1
  53. package/dist/widgets/radio/choice-icon/shared-styles.d.ts +0 -1
  54. package/dist/widgets/radio/choice-none-above.d.ts +1 -1
  55. package/dist/widgets/radio/radio.d.ts +2 -2
  56. package/dist/widgets/video/video.d.ts +1 -1
  57. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -384,7 +384,8 @@ const mockStrings = {
384
384
  } = _ref28;
385
385
  return `Interactive elements: ${elements}`;
386
386
  },
387
- srNoInteractiveElements: "No interactive elements"
387
+ srNoInteractiveElements: "No interactive elements",
388
+ selectAnAnswer: "Select an answer"
388
389
  };
389
390
 
390
391
  /**
@@ -14756,9 +14757,9 @@ const pluck = function (table, subKey) {
14756
14757
  * });
14757
14758
  * {a: 2, b: 3}
14758
14759
  */
14759
- const mapObject$1 = function (obj, lambda) {
14760
+ const mapObject$2 = function (obj, lambda) {
14760
14761
  const result = {};
14761
- ___default["default"].each(___default["default"].keys(obj), function (key) {
14762
+ Object.keys(obj).forEach(key => {
14762
14763
  // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'K'.
14763
14764
  result[key] = lambda(obj[key], key);
14764
14765
  });
@@ -14775,14 +14776,14 @@ const mapObject$1 = function (obj, lambda) {
14775
14776
  */
14776
14777
  const mapObjectFromArray = function (arr, lambda) {
14777
14778
  const result = {};
14778
- ___default["default"].each(arr, function (elem) {
14779
+ arr.forEach(elem => {
14779
14780
  result[elem] = lambda(elem);
14780
14781
  });
14781
14782
  return result;
14782
14783
  };
14783
14784
  var objective_ = {
14784
14785
  pluck: pluck,
14785
- mapObject: mapObject$1,
14786
+ mapObject: mapObject$2,
14786
14787
  mapObjectFromArray: mapObjectFromArray
14787
14788
  };
14788
14789
 
@@ -16829,7 +16830,7 @@ class Graphie extends React__namespace.Component {
16829
16830
  };
16830
16831
  _setupGraphie = () => {
16831
16832
  this._removeMovables();
16832
- const graphieDiv = ReactDOM__default["default"].findDOMNode(this.graphieDivRef.current);
16833
+ const graphieDiv = this.graphieDivRef.current;
16833
16834
  if (graphieDiv == null || graphieDiv instanceof Text) {
16834
16835
  throw new Error("No graphie container div found");
16835
16836
  }
@@ -18918,6 +18919,138 @@ class QuestionParagraph extends React__namespace.Component {
18918
18919
  }
18919
18920
  }
18920
18921
 
18922
+ // These are all the current widgets that require user-input and are supported for automatic scoring
18923
+ const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
18924
+
18925
+ /**
18926
+ * Regex for widget placeholders in a string.
18927
+ *
18928
+ * First capture group is the widget ID (ex. 'radio 1')
18929
+ * Second capture group is the widget type (ex. "radio)
18930
+ * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
18931
+ */
18932
+ function getWidgetRegex() {
18933
+ return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
18934
+ }
18935
+
18936
+ /**
18937
+ * Add a widget placeholder using the widget ID.
18938
+ * ex. addWidget("radio 1") => "[[☃ radio 1]]"
18939
+ *
18940
+ * @param {string} id
18941
+ * @returns {string}
18942
+ */
18943
+ function addWidget(id) {
18944
+ return `[[☃ ${id}]]`;
18945
+ }
18946
+
18947
+ /**
18948
+ * Get a widget type by a widget's ID
18949
+ * (please don't derive type from ID)
18950
+ *
18951
+ * @param {string} widgetId the ID of the widget
18952
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18953
+ * @returns {string} the widget type (ie "radio")
18954
+ */
18955
+ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
18956
+ const widget = widgetMap[WidgetId];
18957
+ return widget?.type ?? null;
18958
+ }
18959
+
18960
+ /**
18961
+ * Does the content have a specific type of widget?
18962
+ *
18963
+ * @param {string} type the type of the widget in question (ie "radio")
18964
+ * @param {string} content the string to search through
18965
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
18966
+ * @returns {boolean} if the content includes the widget type
18967
+ */
18968
+ function contentHasWidgetType(type, content, widgetMap) {
18969
+ return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
18970
+ }
18971
+
18972
+ /**
18973
+ * Extract all widget IDs, which includes the widget type and instance number.
18974
+ * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
18975
+ *
18976
+ * Content should contain Perseus widget placeholders,
18977
+ * which look like: '[[☃ radio 1]]'.
18978
+ *
18979
+ * @param {string} content
18980
+ * @returns {ReadonlyArray<string>} widgetIds
18981
+ */
18982
+ function getWidgetIdsFromContent(content) {
18983
+ const widgets = [];
18984
+ const localWidgetRegex = getWidgetRegex();
18985
+ let match = localWidgetRegex.exec(content);
18986
+ while (match !== null) {
18987
+ widgets.push(match[1]);
18988
+ match = localWidgetRegex.exec(content);
18989
+ }
18990
+ return widgets;
18991
+ }
18992
+
18993
+ /**
18994
+ * Get a list of widget IDs from content,
18995
+ * but only for specific widget types
18996
+ *
18997
+ * @param {string} type the type of widget (ie "radio")
18998
+ * @param {string} content the string to parse
18999
+ * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
19000
+ * @returns {ReadonlyArray<string>} the widget type (ie "radio")
19001
+ */
19002
+ function getWidgetIdsFromContentByType(type, content, widgetMap) {
19003
+ const rv = [];
19004
+ const widgetIdsInContent = getWidgetIdsFromContent(content);
19005
+ widgetIdsInContent.forEach(widgetId => {
19006
+ const widget = widgetMap[widgetId];
19007
+ if (widget?.type === type) {
19008
+ rv.push(widgetId);
19009
+ }
19010
+ });
19011
+ return rv;
19012
+ }
19013
+
19014
+ /**
19015
+ * Pull the widget map out of ItemData
19016
+ *
19017
+ * @param {PerseusItem} PerseusItem containing a widgetMap
19018
+ * @returns {WidgetMap} the widget map in the PerseusItem
19019
+ */
19020
+ function getWidgetsMapFromItemData(itemData) {
19021
+ return itemData.question.widgets;
19022
+ }
19023
+
19024
+ /**
19025
+ * Get the widget information from a WidgetMap
19026
+ *
19027
+ * @param {string} widgetId the ID of the widget
19028
+ * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
19029
+ */
19030
+ function getWidgetFromWidgetMap(widgetId, widgetMap) {
19031
+ return widgetMap[widgetId] ?? null;
19032
+ }
19033
+
19034
+ /**
19035
+ * Get select widgets from a widget map.
19036
+ * Useful for multi-items that needs to split
19037
+ * one PerseusItem into several
19038
+ *
19039
+ * @param {ReadonlyArray<string>} widgetIds to extract
19040
+ * @param {PerseusWidgetsMap} widgetMap to extract from
19041
+ * @return {PerseusWidgetsMap} a new widget map with requested widgets
19042
+ */
19043
+ function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
19044
+ const widgets = {};
19045
+ widgetIds.forEach(widgetId => {
19046
+ const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
19047
+ if (widget) {
19048
+ widgets[widgetId] = widget;
19049
+ }
19050
+ });
19051
+ return widgets;
19052
+ }
19053
+
18921
19054
  const DEFAULT_ALIGNMENT$1 = "block";
18922
19055
  // NOTE(kevinb): "default" is not one in `validAlignments`.
18923
19056
  const DEFAULT_SUPPORTED_ALIGNMENTS = ["default"];
@@ -19348,12 +19481,38 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
19348
19481
  isLintable: isLintable
19349
19482
  });
19350
19483
 
19484
+ const {
19485
+ mapObject: mapObject$1
19486
+ } = objective_;
19487
+ function getUpgradedWidgetOptions(oldWidgetOptions) {
19488
+ // @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; }'.
19489
+ return mapObject$1(oldWidgetOptions, (widgetInfo, widgetId) => {
19490
+ if (!widgetInfo.type || !widgetInfo.alignment) {
19491
+ const newValues = {};
19492
+ if (!widgetInfo.type) {
19493
+ // TODO: why does widget have no type?
19494
+ // We don't want to derive type from widget ID
19495
+ // see: LEMS-1845
19496
+ newValues.type = widgetId.split(" ")[0];
19497
+ }
19498
+ if (!widgetInfo.alignment) {
19499
+ newValues.alignment = "default";
19500
+ }
19501
+ widgetInfo = {
19502
+ ...widgetInfo,
19503
+ ...newValues
19504
+ };
19505
+ }
19506
+ return upgradeWidgetInfoToLatestVersion(widgetInfo);
19507
+ });
19508
+ }
19351
19509
  function emptyWidgetsFunctional(widgets,
19352
19510
  // This is a port of old code, I'm not sure why
19353
19511
  // we need widgetIds vs the keys of the widgets object
19354
19512
  widgetIds, userInputMap, strings, locale) {
19513
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19355
19514
  return widgetIds.filter(id => {
19356
- const widget = widgets[id];
19515
+ const widget = upgradedWidgets[id];
19357
19516
  if (!widget || widget.static) {
19358
19517
  // Static widgets shouldn't count as empty
19359
19518
  return false;
@@ -19372,12 +19531,27 @@ widgetIds, userInputMap, strings, locale) {
19372
19531
  }
19373
19532
  });
19374
19533
  }
19534
+
19535
+ // TODO: combine scorePerseusItem with scoreWidgetsFunctional
19536
+ // once scorePerseusItem is the only one calling scoreWidgetsFunctional
19537
+ function scorePerseusItem(perseusRenderData, userInputMap,
19538
+ // TODO(LEMS-2461,LEMS-2391): these probably
19539
+ // need to be removed before we move this to the server
19540
+ strings, locale) {
19541
+ // There seems to be a chance that PerseusRenderer.widgets might include
19542
+ // widget data for widgets that are not in PerseusRenderer.content,
19543
+ // so this checks that the widgets are being used before scoring them
19544
+ const usedWidgetIds = getWidgetIdsFromContent(perseusRenderData.content);
19545
+ const scores = scoreWidgetsFunctional(perseusRenderData.widgets, usedWidgetIds, userInputMap, strings, locale);
19546
+ return Util.flattenScores(scores);
19547
+ }
19375
19548
  function scoreWidgetsFunctional(widgets,
19376
19549
  // This is a port of old code, I'm not sure why
19377
19550
  // we need widgetIds vs the keys of the widgets object
19378
19551
  widgetIds, userInputMap, strings, locale) {
19552
+ const upgradedWidgets = getUpgradedWidgetOptions(widgets);
19379
19553
  const gradedWidgetIds = widgetIds.filter(id => {
19380
- const props = widgets[id];
19554
+ const props = upgradedWidgets[id];
19381
19555
  const widgetIsGraded = props?.graded == null || props.graded;
19382
19556
  const widgetIsStatic = !!props?.static;
19383
19557
  // Ungraded widgets or widgets set to static shouldn't be graded.
@@ -19385,14 +19559,14 @@ widgetIds, userInputMap, strings, locale) {
19385
19559
  });
19386
19560
  const widgetScores = {};
19387
19561
  gradedWidgetIds.forEach(id => {
19388
- const widget = widgets[id];
19562
+ const widget = upgradedWidgets[id];
19389
19563
  if (!widget) {
19390
19564
  return;
19391
19565
  }
19392
19566
  const userInput = userInputMap[id];
19393
19567
  const scorer = getWidgetScorer(widget.type);
19394
19568
  if (widget.type === "group") {
19395
- const scores = scoreWidgetsFunctional(widget.options.widgets, Object.keys(widget.options.widgets), userInputMap[id], strings, locale);
19569
+ const scores = scoreWidgetsFunctional(widget.options.widgets, getWidgetIdsFromContent(widget.options.content), userInputMap[id], strings, locale);
19396
19570
  widgetScores[id] = Util.flattenScores(scores);
19397
19571
  } else if (scorer) {
19398
19572
  widgetScores[id] = scorer(userInput, widget.options, strings, locale);
@@ -19917,32 +20091,12 @@ class Renderer extends React__namespace.Component {
19917
20091
  };
19918
20092
  };
19919
20093
  _getInitialWidgetState = props => {
19920
- const allWidgetInfo = this._getAllWidgetsInfo(props);
20094
+ const allWidgetInfo = getUpgradedWidgetOptions(props.widgets);
19921
20095
  return {
19922
20096
  widgetInfo: allWidgetInfo,
19923
20097
  widgetProps: this._getAllWidgetsStartProps(allWidgetInfo, props)
19924
20098
  };
19925
20099
  };
19926
-
19927
- // @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; }'.
19928
- _getAllWidgetsInfo = props => {
19929
- return mapObject(props.widgets, (widgetInfo, widgetId) => {
19930
- if (!widgetInfo.type || !widgetInfo.alignment) {
19931
- const newValues = {};
19932
- if (!widgetInfo.type) {
19933
- // TODO: why does widget have no type?
19934
- // We don't want to derive type from widget ID
19935
- // see: LEMS-1845
19936
- newValues.type = widgetId.split(" ")[0];
19937
- }
19938
- if (!widgetInfo.alignment) {
19939
- newValues.alignment = "default";
19940
- }
19941
- widgetInfo = ___default["default"].extend({}, widgetInfo, newValues);
19942
- }
19943
- return upgradeWidgetInfoToLatestVersion(widgetInfo);
19944
- });
19945
- };
19946
20100
  _getAllWidgetsStartProps = (allWidgetInfo, props) => {
19947
20101
  const {
19948
20102
  apiOptions,
@@ -20441,7 +20595,7 @@ class Renderer extends React__namespace.Component {
20441
20595
  // Instead, we center them with css magic in styles.less
20442
20596
  // /cry(aria)
20443
20597
  this._foundTextNodes = true;
20444
- if (___default["default"].contains(this.widgetIds, node.id)) {
20598
+ if (this.widgetIds.includes(node.id)) {
20445
20599
  // We don't want to render a duplicate widget key/ref,
20446
20600
  // as this causes problems with react (for obvious
20447
20601
  // reasons). Instead we just notify the
@@ -20766,7 +20920,7 @@ class Renderer extends React__namespace.Component {
20766
20920
  };
20767
20921
  getInputPaths = () => {
20768
20922
  const inputPaths = [];
20769
- ___default["default"].each(this.widgetIds, widgetId => {
20923
+ this.widgetIds.forEach(widgetId => {
20770
20924
  const widget = this.getWidgetInstance(widgetId);
20771
20925
  if (widget && widget.getInputPaths) {
20772
20926
  // Grab all input paths and add widgetID to the front
@@ -20774,7 +20928,7 @@ class Renderer extends React__namespace.Component {
20774
20928
  // Prefix paths with their widgetID and add to collective
20775
20929
  // list of paths.
20776
20930
  // @ts-expect-error - TS2345 - Argument of type '(inputPath: string) => void' is not assignable to parameter of type 'CollectionIterator<FocusPath, void, readonly FocusPath[]>'.
20777
- ___default["default"].each(widgetInputPaths, inputPath => {
20931
+ widgetInputPaths.forEach(inputPath => {
20778
20932
  const relativeInputPath = [widgetId].concat(inputPath);
20779
20933
  inputPaths.push(relativeInputPath);
20780
20934
  });
@@ -20967,33 +21121,30 @@ class Renderer extends React__namespace.Component {
20967
21121
  }
20968
21122
 
20969
21123
  /**
20970
- * Returns an object mapping from widget ID to perseus-style score.
20971
- * The keys of this object are the values of the array returned
20972
- * from `getWidgetIds`.
20973
- */
20974
- scoreWidgets() {
20975
- return scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20976
- }
20977
-
20978
- /**
20979
- * Grades the content.
21124
+ * Scores the content.
21125
+ *
21126
+ * @deprecated use scorePerseusItem
20980
21127
  */
20981
21128
  score() {
20982
- const scores = this.scoreWidgets();
21129
+ const scores = scoreWidgetsFunctional(this.state.widgetInfo, this.widgetIds, this.getUserInputMap(), this.props.strings, this.context.locale);
20983
21130
  const combinedScore = Util.flattenScores(scores);
20984
21131
  return combinedScore;
20985
21132
  }
20986
- guessAndScore() {
21133
+
21134
+ /**
21135
+ * @deprecated use scorePerseusItem
21136
+ */
21137
+ guessAndScore = () => {
20987
21138
  const totalGuess = this.getUserInput();
20988
21139
  const totalScore = this.score();
20989
21140
  return [totalGuess, totalScore];
20990
- }
21141
+ };
20991
21142
  examples = () => {
20992
21143
  const widgetIds = this.widgetIds;
20993
- const examples = ___default["default"].compact(___default["default"].map(widgetIds, widgetId => {
21144
+ const examples = widgetIds.map(widgetId => {
20994
21145
  const widget = this.getWidgetInstance(widgetId);
20995
21146
  return widget != null && widget.examples ? widget.examples() : null;
20996
- }));
21147
+ }).filter(Boolean);
20997
21148
 
20998
21149
  // no widgets with examples
20999
21150
  if (!examples.length) {
@@ -21376,6 +21527,7 @@ const HORIZONTAL_ALIGNMNENTS = {
21376
21527
  * ```
21377
21528
  */
21378
21529
  class Tooltip extends React__namespace.Component {
21530
+ tooltipContainerRef = /*#__PURE__*/React__namespace.createRef();
21379
21531
  static defaultProps = {
21380
21532
  className: "",
21381
21533
  arrowSize: 10,
@@ -21458,8 +21610,7 @@ class Tooltip extends React__namespace.Component {
21458
21610
  display: this.props.show ? "block" : "none"
21459
21611
  }
21460
21612
  }, /*#__PURE__*/React__namespace.createElement("div", {
21461
- // eslint-disable-next-line react/no-string-refs
21462
- ref: "tooltipContainer",
21613
+ ref: this.tooltipContainerRef,
21463
21614
  className: "tooltipContainer",
21464
21615
  style: {
21465
21616
  position: "absolute",
@@ -21486,10 +21637,8 @@ class Tooltip extends React__namespace.Component {
21486
21637
  }, this.props.children.slice(1)), arrowBelow));
21487
21638
  }
21488
21639
  _updateHeight() {
21489
- const tooltipContainer = ReactDOM__default["default"].findDOMNode(
21490
- // eslint-disable-next-line react/no-string-refs
21491
- this.refs.tooltipContainer);
21492
- const height = tooltipContainer.offsetHeight;
21640
+ const tooltipContainer = this.tooltipContainerRef.current;
21641
+ const height = tooltipContainer?.offsetHeight || 0;
21493
21642
  if (height !== this.state.height) {
21494
21643
  this.setState({
21495
21644
  height
@@ -27443,6 +27592,7 @@ function scoreDropdown(userInput, rubric) {
27443
27592
  }
27444
27593
 
27445
27594
  class Dropdown extends React__namespace.Component {
27595
+ static contextType = PerseusI18nContext;
27446
27596
  static defaultProps = {
27447
27597
  choices: [],
27448
27598
  selected: 0,
@@ -27485,26 +27635,44 @@ class Dropdown extends React__namespace.Component {
27485
27635
  value: String(i + 1),
27486
27636
  label: choice
27487
27637
  }))];
27488
- return /*#__PURE__*/React__namespace.createElement("div", {
27489
- // NOTE(jared): These are required to prevent weird behavior
27490
- // When there's a dropdown in a zoomable table.
27638
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
27639
+ scope: "dropdown-widget",
27640
+ mockOnFirstRender: true
27641
+ }, ids => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
27642
+ // NOTE(jared): These are required to prevent weird behavior
27643
+ // When there's a dropdown in a zoomable table.
27644
+ , {
27491
27645
  onClick: e => {
27492
27646
  e.stopPropagation();
27493
27647
  },
27494
27648
  onTouchStart: e => {
27495
27649
  e.stopPropagation();
27496
27650
  }
27497
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27651
+ }, this.props.visibleLabel && /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelLarge, {
27652
+ tag: "label",
27653
+ id: ids.get("dropdown-label")
27654
+ }, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27655
+ id: ids.get("dropdown"),
27498
27656
  placeholder: "",
27499
27657
  onChange: value => this._handleChange(parseInt(value)),
27500
27658
  selectedValue: String(this.props.selected),
27501
- disabled: this.props.apiOptions.readOnly
27502
- }, children));
27659
+ disabled: this.props.apiOptions.readOnly,
27660
+ "aria-label": this.props.ariaLabel || this.context.strings.selectAnAnswer,
27661
+ "aria-labelledby": ids.get("dropdown-label")
27662
+ // This is currently necessary for SRs to read the labels properly.
27663
+ // However, WB is working on a change to add the "combobox" role to
27664
+ // all dropdowns.
27665
+ // See https://khanacademy.atlassian.net/browse/WB-1671
27666
+ ,
27667
+ role: "combobox"
27668
+ }, children)));
27503
27669
  }
27504
27670
  }
27505
27671
  const optionsTransform = widgetOptions => {
27506
27672
  return {
27507
27673
  placeholder: widgetOptions.placeholder,
27674
+ visibleLabel: widgetOptions.visibleLabel,
27675
+ ariaLabel: widgetOptions.ariaLabel,
27508
27676
  choices: widgetOptions.choices.map(choice => choice.content)
27509
27677
  };
27510
27678
  };
@@ -29584,6 +29752,9 @@ class Group extends React__namespace.Component {
29584
29752
  this.change("widgets", this.rendererRef.props.widgets);
29585
29753
  }
29586
29754
  };
29755
+
29756
+ // TODO(LEMS-2391): replace this when there's a separate check
29757
+ // for valid/invalid state
29587
29758
  const score = this.rendererRef?.score();
29588
29759
  const isValid = score && score.type !== "invalid";
29589
29760
  const isInvalid = score && score.type === "invalid";
@@ -30630,6 +30801,7 @@ function numSteps$1(range, step) {
30630
30801
  return Math.floor((range[1] - range[0]) / step);
30631
30802
  }
30632
30803
  class Graph extends React__namespace.Component {
30804
+ graphieDivRef = /*#__PURE__*/React__namespace.createRef();
30633
30805
  protractor;
30634
30806
  ruler;
30635
30807
  _graphie;
@@ -30709,9 +30881,7 @@ class Graph extends React__namespace.Component {
30709
30881
  if (this._hasSetupGraphieThisUpdate) {
30710
30882
  return;
30711
30883
  }
30712
-
30713
- // eslint-disable-next-line react/no-string-refs
30714
- const graphieDiv = ReactDOM__default["default"].findDOMNode(this.refs.graphieDiv);
30884
+ const graphieDiv = this.graphieDivRef.current;
30715
30885
  // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'.
30716
30886
  $__default["default"](graphieDiv).empty();
30717
30887
 
@@ -30900,7 +31070,7 @@ class Graph extends React__namespace.Component {
30900
31070
  onClick: this.onClick
30901
31071
  }, image, /*#__PURE__*/React__namespace.createElement("div", {
30902
31072
  className: "graphie",
30903
- ref: "graphieDiv"
31073
+ ref: this.graphieDivRef
30904
31074
  }));
30905
31075
  }
30906
31076
  }
@@ -31986,13 +32156,13 @@ class InfoTip extends React__namespace.Component {
31986
32156
  * this component allows multiple selection!
31987
32157
  */
31988
32158
  class MultiButtonGroup extends React__namespace.Component {
32159
+ buttonContainerRef = /*#__PURE__*/React__namespace.createRef();
31989
32160
  static defaultProps = {
31990
32161
  values: [],
31991
32162
  allowEmpty: true
31992
32163
  };
31993
32164
  focus() {
31994
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'.
31995
- ReactDOM__namespace.findDOMNode(this)?.focus();
32165
+ this.buttonContainerRef.current?.focus();
31996
32166
  return true;
31997
32167
  }
31998
32168
  toggleSelect = newValue => {
@@ -32027,7 +32197,8 @@ class MultiButtonGroup extends React__namespace.Component {
32027
32197
  display: "inline-block"
32028
32198
  };
32029
32199
  return /*#__PURE__*/React__namespace.createElement("div", {
32030
- style: outerStyle
32200
+ style: outerStyle,
32201
+ ref: this.buttonContainerRef
32031
32202
  }, buttons);
32032
32203
  }
32033
32204
  }
@@ -45209,7 +45380,7 @@ const init = function () {
45209
45380
 
45210
45381
  // This file is processed by a Rollup plugin (replace) to inject the production
45211
45382
  const libName = "@khanacademy/perseus";
45212
- const libVersion = "43.1.0";
45383
+ const libVersion = "45.0.0";
45213
45384
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45214
45385
 
45215
45386
  const apiVersion = {
@@ -45813,6 +45984,13 @@ class ServerItemRenderer extends React__namespace.Component {
45813
45984
  this.props.onRendered(true);
45814
45985
  }
45815
45986
  }
45987
+ if (this.props.score && this.props.score !== prevProps.score) {
45988
+ const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45989
+ this.setState({
45990
+ questionCompleted: this.props.score.correct,
45991
+ questionHighlightedWidgets: emptyQuestionAreaWidgets
45992
+ });
45993
+ }
45816
45994
  }
45817
45995
  componentWillUnmount() {
45818
45996
  if (this.blurTimeoutID != null) {
@@ -45948,18 +46126,37 @@ class ServerItemRenderer extends React__namespace.Component {
45948
46126
  return this.questionRenderer.getPromptJSON();
45949
46127
  }
45950
46128
 
46129
+ /**
46130
+ * Returns an array of the widget `.getUserInput()` results
46131
+ *
46132
+ * TODO: can we remove this? Seems to be just for backwards
46133
+ * compatibility with old Perseus Chrome logging
46134
+ * @deprecated use getUserInput
46135
+ */
46136
+ getUserInputLegacy() {
46137
+ return this.questionRenderer.getUserInput();
46138
+ }
46139
+
46140
+ /**
46141
+ * Returns an object of the widget `.getUserInput()` results
46142
+ */
46143
+ getUserInput() {
46144
+ return this.questionRenderer.getUserInputMap();
46145
+ }
46146
+
45951
46147
  /**
45952
46148
  * Grades the item.
46149
+ *
46150
+ * @deprecated use scorePerseusItem
45953
46151
  */
45954
46152
  scoreInput() {
45955
- const guessAndScore = this.questionRenderer.guessAndScore();
45956
- const guess = guessAndScore[0];
45957
- const score = guessAndScore[1];
46153
+ const guess = this.getUserInput();
46154
+ const score = scorePerseusItem(this.props.item.question, guess, this.context.strings, this.context.locale);
45958
46155
 
45959
46156
  // Continue to include an empty guess for the now defunct answer area.
45960
46157
  // TODO(alex): Check whether we rely on the format here for
45961
46158
  // analyzing ProblemLogs. If not, remove this layer.
45962
- const maxCompatGuess = [guess, []];
46159
+ const maxCompatGuess = [this.questionRenderer.getUserInput(), []];
45963
46160
  const keScore = Util.keScoreFromPerseusScore(score, maxCompatGuess, this.questionRenderer.getSerializedState());
45964
46161
  const emptyQuestionAreaWidgets = this.questionRenderer.emptyWidgets();
45965
46162
  this.setState({
@@ -47821,138 +48018,6 @@ const registerAllWidgetsForTesting = () => {
47821
48018
  replaceDeprecatedWidgets();
47822
48019
  };
47823
48020
 
47824
- // These are all the current widgets that require user-input and are supported for automatic scoring
47825
- const QUESTION_WIDGETS = ["numeric-input", "input-number", "radio", "interactive-graph", "interactive-number-line", "categorizer", "plotter", "orderer", "protractor", "matcher", "sorter"];
47826
-
47827
- /**
47828
- * Regex for widget placeholders in a string.
47829
- *
47830
- * First capture group is the widget ID (ex. 'radio 1')
47831
- * Second capture group is the widget type (ex. "radio)
47832
- * exec return will look like: ['[[☃ radio 1]]', 'radio 1', 'radio']
47833
- */
47834
- function getWidgetRegex() {
47835
- return /\[\[☃ ([A-Za-z0-9- ]+)\]\]/g;
47836
- }
47837
-
47838
- /**
47839
- * Add a widget placeholder using the widget ID.
47840
- * ex. addWidget("radio 1") => "[[☃ radio 1]]"
47841
- *
47842
- * @param {string} id
47843
- * @returns {string}
47844
- */
47845
- function addWidget(id) {
47846
- return `[[☃ ${id}]]`;
47847
- }
47848
-
47849
- /**
47850
- * Get a widget type by a widget's ID
47851
- * (please don't derive type from ID)
47852
- *
47853
- * @param {string} widgetId the ID of the widget
47854
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47855
- * @returns {string} the widget type (ie "radio")
47856
- */
47857
- function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
47858
- const widget = widgetMap[WidgetId];
47859
- return widget?.type ?? null;
47860
- }
47861
-
47862
- /**
47863
- * Does the content have a specific type of widget?
47864
- *
47865
- * @param {string} type the type of the widget in question (ie "radio")
47866
- * @param {string} content the string to search through
47867
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47868
- * @returns {boolean} if the content includes the widget type
47869
- */
47870
- function contentHasWidgetType(type, content, widgetMap) {
47871
- return getWidgetIdsFromContentByType(type, content, widgetMap).length > 0;
47872
- }
47873
-
47874
- /**
47875
- * Extract all widget IDs, which includes the widget type and instance number.
47876
- * example output: ['radio 1', 'categorizer 1', 'categorizor 2']
47877
- *
47878
- * Content should contain Perseus widget placeholders,
47879
- * which look like: '[[☃ radio 1]]'.
47880
- *
47881
- * @param {string} content
47882
- * @returns {ReadonlyArray<string>} widgetIds
47883
- */
47884
- function getWidgetIdsFromContent(content) {
47885
- const widgets = [];
47886
- const localWidgetRegex = getWidgetRegex();
47887
- let match = localWidgetRegex.exec(content);
47888
- while (match !== null) {
47889
- widgets.push(match[1]);
47890
- match = localWidgetRegex.exec(content);
47891
- }
47892
- return widgets;
47893
- }
47894
-
47895
- /**
47896
- * Get a list of widget IDs from content,
47897
- * but only for specific widget types
47898
- *
47899
- * @param {string} type the type of widget (ie "radio")
47900
- * @param {string} content the string to parse
47901
- * @param {PerseusWidgetsMap} widgetMap widget ID to widget map
47902
- * @returns {ReadonlyArray<string>} the widget type (ie "radio")
47903
- */
47904
- function getWidgetIdsFromContentByType(type, content, widgetMap) {
47905
- const rv = [];
47906
- const widgetIdsInContent = getWidgetIdsFromContent(content);
47907
- widgetIdsInContent.forEach(widgetId => {
47908
- const widget = widgetMap[widgetId];
47909
- if (widget?.type === type) {
47910
- rv.push(widgetId);
47911
- }
47912
- });
47913
- return rv;
47914
- }
47915
-
47916
- /**
47917
- * Pull the widget map out of ItemData
47918
- *
47919
- * @param {PerseusItem} PerseusItem containing a widgetMap
47920
- * @returns {WidgetMap} the widget map in the PerseusItem
47921
- */
47922
- function getWidgetsMapFromItemData(itemData) {
47923
- return itemData.question.widgets;
47924
- }
47925
-
47926
- /**
47927
- * Get the widget information from a WidgetMap
47928
- *
47929
- * @param {string} widgetId the ID of the widget
47930
- * @returns {PerseusWidget | null} the widget data if it exists, otherwise null
47931
- */
47932
- function getWidgetFromWidgetMap(widgetId, widgetMap) {
47933
- return widgetMap[widgetId] ?? null;
47934
- }
47935
-
47936
- /**
47937
- * Get select widgets from a widget map.
47938
- * Useful for multi-items that needs to split
47939
- * one PerseusItem into several
47940
- *
47941
- * @param {ReadonlyArray<string>} widgetIds to extract
47942
- * @param {PerseusWidgetsMap} widgetMap to extract from
47943
- * @return {PerseusWidgetsMap} a new widget map with requested widgets
47944
- */
47945
- function getWidgetsFromWidgetMap(widgetIds, widgetMap) {
47946
- const widgets = {};
47947
- widgetIds.forEach(widgetId => {
47948
- const widget = getWidgetFromWidgetMap(widgetId, widgetMap);
47949
- if (widget) {
47950
- widgets[widgetId] = widget;
47951
- }
47952
- });
47953
- return widgets;
47954
- }
47955
-
47956
48021
  /**
47957
48022
  * This function extracts the answers from the widgets.
47958
48023
  *
@@ -48896,6 +48961,8 @@ const parseDefinitionWidget = parseWidget(constant("definition"), object({
48896
48961
 
48897
48962
  const parseDropdownWidget = parseWidget(constant("dropdown"), object({
48898
48963
  placeholder: string,
48964
+ ariaLabel: optional(string),
48965
+ visibleLabel: optional(string),
48899
48966
  static: defaulted(boolean, () => false),
48900
48967
  choices: array(object({
48901
48968
  content: string,
@@ -50120,6 +50187,7 @@ exports.parsePerseusItem = parsePerseusItem;
50120
50187
  exports.plotterPlotTypes = plotterPlotTypes;
50121
50188
  exports.preprocessTex = preprocessTex;
50122
50189
  exports.registerAllWidgetsForTesting = registerAllWidgetsForTesting;
50190
+ exports.scorePerseusItem = scorePerseusItem;
50123
50191
  exports.shapes = shapes;
50124
50192
  exports.shouldHaveIndividualAnswer = shouldHaveIndividualAnswer;
50125
50193
  exports.traverse = traverse;