@khanacademy/perseus 12.1.0 → 12.3.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 (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/article-renderer.d.ts +5 -2
  3. package/dist/error-boundary.d.ts +1 -0
  4. package/dist/es/index.css +4 -66
  5. package/dist/es/index.js +112 -87
  6. package/dist/es/index.js.map +1 -1
  7. package/dist/index.css +4 -66
  8. package/dist/index.js +103 -75
  9. package/dist/index.js.map +1 -1
  10. package/dist/renderer.d.ts +4 -1
  11. package/dist/styles/constants.d.ts +2 -2
  12. package/dist/widget-container.d.ts +2 -0
  13. package/dist/widgets/expression.d.ts +5 -4
  14. package/package.json +8 -8
  15. package/src/__stories__/article-renderer.stories.tsx +28 -2
  16. package/src/__testdata__/article-renderer.testdata.ts +85 -0
  17. package/src/__tests__/article-renderer.test.tsx +131 -0
  18. package/src/__tests__/renderer-api.test.tsx +18 -23
  19. package/src/article-renderer.tsx +73 -58
  20. package/src/error-boundary.tsx +3 -0
  21. package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +5 -5
  22. package/src/renderer.tsx +29 -27
  23. package/src/server-item-renderer.tsx +1 -0
  24. package/src/styles/constants.ts +4 -4
  25. package/src/styles/perseus-renderer.less +1 -10
  26. package/src/styles/variables.less +2 -2
  27. package/src/styles/variables.shared.less +2 -2
  28. package/src/styles/widgets/expression.less +0 -77
  29. package/src/styles/widgets/passage.less +1 -19
  30. package/src/widget-container.tsx +32 -10
  31. package/src/widgets/__stories__/label-image.stories.tsx +30 -0
  32. package/src/widgets/__testdata__/label-image.testdata.ts +63 -0
  33. package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +5 -5
  34. package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +28 -28
  35. package/src/widgets/__tests__/expression.test.tsx +12 -9
  36. package/src/widgets/__tests__/renderQuestion.tsx +30 -13
  37. package/src/widgets/expression.tsx +19 -25
  38. package/src/widgets/transformer.tsx +1 -2
  39. package/tsconfig-build.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @khanacademy/perseus
2
2
 
3
+ ## 12.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#780](https://github.com/Khan/perseus/pull/780) [`376eb0e4`](https://github.com/Khan/perseus/commit/376eb0e4aaaa4c7a90fd6107a84bb74d382b077c) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Adds an `onError` callback prop to the ErrorBoundary component. Uses this new callback in the WidgetContainer component to report all widget rendering failures through the analytics API.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#759](https://github.com/Khan/perseus/pull/759) [`c431c4b8`](https://github.com/Khan/perseus/commit/c431c4b8147ae0630df2d2b19b0f5a5b5f04d4bf) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove references to unused font families
12
+
13
+ * [#775](https://github.com/Khan/perseus/pull/775) [`cb529549`](https://github.com/Khan/perseus/commit/cb5295497fe74e3f4cc00eef0f99da9f83f58e4d) Thanks [@nedredmond](https://github.com/nedredmond)! - Added flag to `simpleValidate` call from Renderer to ensure we call analytics on MathInputEvaluated only when the answer is submitted
14
+
15
+ - [#781](https://github.com/Khan/perseus/pull/781) [`9b9dfd4f`](https://github.com/Khan/perseus/commit/9b9dfd4f6779d129040d9afcf3205f1863a64c25) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Fix for broken dilation tool in the Transformer Widget
16
+
17
+ - Updated dependencies [[`c431c4b8`](https://github.com/Khan/perseus/commit/c431c4b8147ae0630df2d2b19b0f5a5b5f04d4bf), [`fb84640d`](https://github.com/Khan/perseus/commit/fb84640de911a8e8817829985fe9956e83a7f7d1), [`376eb0e4`](https://github.com/Khan/perseus/commit/376eb0e4aaaa4c7a90fd6107a84bb74d382b077c), [`33cc24c3`](https://github.com/Khan/perseus/commit/33cc24c33f62cd87de7594ba575d311fd465e2e0)]:
18
+ - @khanacademy/math-input@14.2.1
19
+ - @khanacademy/perseus-core@1.2.0
20
+ - @khanacademy/kas@0.3.4
21
+ - @khanacademy/kmath@0.1.5
22
+ - @khanacademy/perseus-linter@0.3.7
23
+ - @khanacademy/pure-markdown@0.2.9
24
+ - @khanacademy/simple-markdown@0.10.3
25
+
26
+ ## 12.2.0
27
+
28
+ ### Minor Changes
29
+
30
+ - [#773](https://github.com/Khan/perseus/pull/773) [`809823e4`](https://github.com/Khan/perseus/commit/809823e490f65cbcbaa05b548de985035810ba8d) Thanks [@nixterrimus](https://github.com/nixterrimus)! - Fix text overlap in interactive graph widget
31
+
32
+ * [#755](https://github.com/Khan/perseus/pull/755) [`6d9e31a4`](https://github.com/Khan/perseus/commit/6d9e31a4ea4a1ed29bef2d9812d21e5b2fd41af6) Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! - Adding logic to ArticleRenderer so that it can return our currently focused element.
33
+
3
34
  ## 12.1.0
4
35
 
5
36
  ### Minor Changes
@@ -3,13 +3,15 @@
3
3
  * composed of multiple (Renderer) sections concatenated together.
4
4
  */
5
5
  import * as React from "react";
6
+ import { DependenciesContext } from "./dependencies";
7
+ import Renderer from "./renderer";
6
8
  import type { KeypadProps } from "./mixins/provide-keypad";
7
9
  import type { PerseusRenderer } from "./perseus-types";
8
10
  import type { APIOptions, PerseusDependenciesV2 } from "./types";
9
11
  import type { KeypadAPI } from "@khanacademy/math-input";
10
12
  import type { KeypadContextRendererInterface } from "@khanacademy/perseus-core";
11
13
  import type { LinterContextProps } from "@khanacademy/perseus-linter";
12
- type Props = {
14
+ type Props = Partial<React.ContextType<typeof DependenciesContext>> & KeypadProps & {
13
15
  apiOptions: APIOptions;
14
16
  json: PerseusRenderer | ReadonlyArray<PerseusRenderer>;
15
17
  /**
@@ -20,7 +22,7 @@ type Props = {
20
22
  legacyPerseusLint?: ReadonlyArray<string>;
21
23
  keypadElement?: KeypadAPI | null | undefined;
22
24
  dependencies: PerseusDependenciesV2;
23
- } & KeypadProps;
25
+ };
24
26
  type DefaultProps = {
25
27
  apiOptions: Props["apiOptions"];
26
28
  useNewStyles: Props["useNewStyles"];
@@ -28,6 +30,7 @@ type DefaultProps = {
28
30
  };
29
31
  declare class ArticleRenderer extends React.Component<Props> implements KeypadContextRendererInterface {
30
32
  _currentFocus: any;
33
+ sectionRenderers: Array<Renderer>;
31
34
  static defaultProps: DefaultProps;
32
35
  constructor(props: Props);
33
36
  componentDidMount(): void;
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
  type Props = {
3
3
  children: React.ReactNode;
4
4
  metadata?: Record<string, string>;
5
+ onError?: (error: Error, info: any) => void;
5
6
  };
6
7
  type State = {
7
8
  error: string;
package/dist/es/index.css CHANGED
@@ -2613,6 +2613,7 @@ img.zoom-img {
2613
2613
  }
2614
2614
  /* Widget CSS */
2615
2615
  .perseus-graph-padding {
2616
+ box-sizing: content-box;
2616
2617
  padding: 25px 25px 0 0;
2617
2618
  }
2618
2619
  .categorizer-container {
@@ -2683,72 +2684,8 @@ body.mobile .categorizer-container td.category input[type="radio"] + span:active
2683
2684
  #answer_area .perseus-widget-expression .error-tooltip .tooltipContainer > div:first-child {
2684
2685
  visibility: hidden !important;
2685
2686
  }
2686
- .perseus-widget-expression-old,
2687
- .perseus-widget-expression-old > .output,
2688
- .perseus-widget-expression-old > .output > .tex,
2689
- .perseus-widget-expression-old > .output > .placeholder {
2690
- display: block;
2691
- }
2692
- .perseus-widget-expression-old input,
2693
- #answer_area .perseus-widget-expression-old input {
2694
- direction: ltr;
2695
- border: 1px solid #a4a4a4;
2696
- border-radius: 5px;
2697
- box-sizing: border-box;
2698
- font-size: 14px;
2699
- margin-bottom: 5px;
2700
- max-width: 240px;
2701
- padding: 6px;
2702
- width: 100%;
2703
- }
2704
- .perseus-widget-expression-old > .output {
2705
- background: #f2f2f2;
2706
- border-radius: 5px;
2707
- padding: 10px;
2708
- margin: 10px 0;
2709
- }
2710
- #answer_area .perseus-widget-expression-old > .output {
2711
- background: #f7f7f7;
2712
- }
2713
- .perseus-widget-expression-old > .output > .tex {
2714
- overflow-x: auto;
2715
- padding: 5px;
2716
- }
2717
- .perseus-widget-expression-old > .output > .placeholder {
2718
- position: relative;
2719
- height: 40px;
2720
- overflow-y: hidden;
2721
- }
2722
- .perseus-widget-expression-old > .output > .placeholder > .error {
2723
- border-radius: 10px;
2724
- background: #f7f7f7;
2725
- border: 1px solid #ddd;
2726
- box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
2727
- color: #000;
2728
- display: none;
2729
- font-weight: normal;
2730
- min-height: 22px;
2731
- width: 168px;
2732
- position: absolute;
2733
- top: 0px;
2734
- left: 40px;
2735
- margin: auto;
2736
- }
2737
- .perseus-widget-expression-old > .output > .placeholder > .error > .buddy {
2738
- background-image: url(/images/perseus/error-buddy.png);
2739
- background-size: 100%;
2740
- height: 36px;
2741
- width: 40px;
2742
- position: absolute;
2743
- top: -3px;
2744
- left: -42px;
2745
- }
2746
- .perseus-widget-expression-old > .output > .placeholder > .error > .message {
2747
- font: 12px inherit;
2748
- line-height: 1.4em;
2749
- margin: 0px 4px;
2750
- }
2751
2687
  .perseus-widget-grapher {
2688
+ box-sizing: content-box;
2752
2689
  padding: 25px 25px 0 0;
2753
2690
  }
2754
2691
  .perseus-widget-grapher > .graphie-container {
@@ -2836,6 +2773,7 @@ body.mobile .categorizer-container td.category input[type="radio"] + span:active
2836
2773
  margin-top: 5px;
2837
2774
  }
2838
2775
  .perseus-widget-interactive-graph {
2776
+ box-sizing: content-box;
2839
2777
  padding: 25px 25px 0 0;
2840
2778
  }
2841
2779
  .perseus-widget-interactive-graph > .graphie-container {
@@ -3118,7 +3056,7 @@ body.mobile .perseus-matrix .math-output {
3118
3056
  }
3119
3057
  .framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage div.paragraph,
3120
3058
  .perseus-widget-passage-container .perseus-widget-passage div.paragraph {
3121
- font-family: "minion-pro", KaTeX_Main, Times, "Times New Roman", serif;
3059
+ font-family: KaTeX_Main, Times, "Times New Roman", serif;
3122
3060
  }
3123
3061
  .framework-perseus.perseus-article .perseus-renderer .perseus-widget-passage-container .perseus-widget-passage .passage-title div.paragraph,
3124
3062
  .perseus-widget-passage-container .perseus-widget-passage .passage-title div.paragraph {
package/dist/es/index.js CHANGED
@@ -49,6 +49,21 @@ function _extends() {
49
49
  return _extends.apply(this, arguments);
50
50
  }
51
51
 
52
+ function _objectWithoutPropertiesLoose(source, excluded) {
53
+ if (source == null) return {};
54
+ var target = {};
55
+ var sourceKeys = Object.keys(source);
56
+ var key, i;
57
+
58
+ for (i = 0; i < sourceKeys.length; i++) {
59
+ key = sourceKeys[i];
60
+ if (excluded.indexOf(key) >= 0) continue;
61
+ target[key] = source[key];
62
+ }
63
+
64
+ return target;
65
+ }
66
+
52
67
  const customKeyTranslator = _extends({}, keyTranslator, {
53
68
  // If there's something in the input that can become part of a
54
69
  // fraction, typing "/" puts it in the numerator. If not, typing
@@ -1870,6 +1885,7 @@ const KhanAnswerTypes = {
1870
1885
  }
1871
1886
  };
1872
1887
 
1888
+ const _excluded$4 = ["scoring"];
1873
1889
  const ERROR_TITLE = i18n._("Oops!");
1874
1890
  const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
1875
1891
  const insertBraces = value => {
@@ -1947,11 +1963,26 @@ class Expression extends React.Component {
1947
1963
  });
1948
1964
  }
1949
1965
  };
1950
- this.simpleValidate = (rubric, onInputError) => {
1951
- onInputError = onInputError || function () {};
1952
- const result = Expression.validate(this.getUserInput(), rubric, onInputError);
1953
- this.sendExpressionEvaluatedEvent(result.type === "invalid" ? "invalid" : result.earned === result.total ? "correct" : "incorrect");
1954
- return result;
1966
+ this.simpleValidate = (_ref, onInputError) => {
1967
+ let {
1968
+ scoring
1969
+ } = _ref,
1970
+ rubric = _objectWithoutPropertiesLoose(_ref, _excluded$4);
1971
+ const score = Expression.validate(this.getUserInput(), rubric, onInputError || function () {});
1972
+
1973
+ // "scoring" is a flag that indicates when we are checking answers.
1974
+ // otherwise, we may just be checking validity after changes.
1975
+ if (scoring && score.type !== "invalid") {
1976
+ var _this$props$analytics;
1977
+ (_this$props$analytics = this.props.analytics) == null ? void 0 : _this$props$analytics.onAnalyticsEvent({
1978
+ type: "perseus:expression-evaluated",
1979
+ payload: {
1980
+ result: score.earned === score.total ? "correct" : "incorrect",
1981
+ virtualKeypadVersion: deriveKeypadVersion(this.props.apiOptions)
1982
+ }
1983
+ });
1984
+ }
1985
+ return score;
1955
1986
  };
1956
1987
  this.getUserInput = () => {
1957
1988
  return Expression.getUserInputFromProps(this.props);
@@ -1973,8 +2004,8 @@ class Expression extends React.Component {
1973
2004
  this.props.trackInteraction();
1974
2005
  };
1975
2006
  this._handleFocus = () => {
1976
- var _this$props$analytics;
1977
- (_this$props$analytics = this.props.analytics) == null ? void 0 : _this$props$analytics.onAnalyticsEvent({
2007
+ var _this$props$analytics2;
2008
+ (_this$props$analytics2 = this.props.analytics) == null ? void 0 : _this$props$analytics2.onAnalyticsEvent({
1978
2009
  type: "perseus:expression-focused",
1979
2010
  payload: null
1980
2011
  });
@@ -2182,16 +2213,6 @@ class Expression extends React.Component {
2182
2213
  // @ts-expect-error - TS2339 - Property 'insert' does not exist on type 'ReactInstance'.
2183
2214
  this.refs.input.insert(keyPressed);
2184
2215
  }
2185
- sendExpressionEvaluatedEvent(result) {
2186
- var _this$props$analytics2;
2187
- (_this$props$analytics2 = this.props.analytics) == null ? void 0 : _this$props$analytics2.onAnalyticsEvent({
2188
- type: "perseus:expression-evaluated",
2189
- payload: {
2190
- result,
2191
- virtualKeypadVersion: deriveKeypadVersion(this.props.apiOptions)
2192
- }
2193
- });
2194
- }
2195
2216
  render() {
2196
2217
  var _this$props$keypadCon, _this$props$analytics3;
2197
2218
  if (this.props.apiOptions.customKeypad) {
@@ -3680,10 +3701,10 @@ Zoom.prototype.dispose = function () {
3680
3701
  };
3681
3702
  const ZoomService = new ZoomServiceClass();
3682
3703
 
3683
- // @baseFontFamily: "Proxima Nova", sans-serif;
3684
- const baseFontFamily$1 = "'Proxima Nova',sans-serif";
3685
- // @boldFontFamily: "Proxima Nova Semibold", sans-serif;
3686
- const boldFontFamily = "'Proxima Nova Semibold',sans-serif";
3704
+ // @baseFontFamily: "Lato", sans-serif;
3705
+ const baseFontFamily$1 = "'Lato', sans-serif";
3706
+ // @boldFontFamily: "Lato-Bold", "Lato", sans-serif;
3707
+ const boldFontFamily = "'Lato-Bold', 'Lato', sans-serif";
3687
3708
  // @green: #76a005;
3688
3709
  const green$1 = "#76A005";
3689
3710
  // @kaGreen: #71b307;
@@ -18675,6 +18696,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18675
18696
  class WidgetContainer extends React.Component {
18676
18697
  constructor(...args) {
18677
18698
  super(...args);
18699
+ this.widgetRef = /*#__PURE__*/React.createRef();
18678
18700
  this.state = {
18679
18701
  // TODO(benkomalo): before we're mounted, we don't know how big
18680
18702
  // we're going to be, so just default to MEDIUM for now. :/ In the
@@ -18684,8 +18706,7 @@ class WidgetContainer extends React.Component {
18684
18706
  widgetProps: this.props.initialProps
18685
18707
  };
18686
18708
  this.getWidget = () => {
18687
- // eslint-disable-next-line react/no-string-refs
18688
- return this.refs.widget;
18709
+ return this.widgetRef.current;
18689
18710
  };
18690
18711
  this.replaceWidgetProps = newWidgetProps => {
18691
18712
  this.setState({
@@ -18779,15 +18800,28 @@ class WidgetContainer extends React.Component {
18779
18800
  return /*#__PURE__*/React.createElement("div", {
18780
18801
  className: className,
18781
18802
  style: isStatic ? staticContainerStyles : {}
18803
+ }, /*#__PURE__*/React.createElement(DependenciesContext.Consumer, null, ({
18804
+ analytics
18805
+ }) => /*#__PURE__*/React.createElement(ErrorBoundary, {
18806
+ metadata: {
18807
+ widget_type: type,
18808
+ widget_id: this.props.id
18809
+ },
18810
+ onError: error => {
18811
+ analytics.onAnalyticsEvent({
18812
+ type: "perseus:widget-rendering-error",
18813
+ payload: {
18814
+ widgetType: type
18815
+ }
18816
+ });
18817
+ }
18782
18818
  }, /*#__PURE__*/React.createElement(WidgetType, _extends({}, this.state.widgetProps, {
18783
18819
  linterContext: linterContext,
18784
- containerSizeClass: this.state.sizeClass
18785
- // eslint-disable-next-line react/no-string-refs
18786
- ,
18787
- ref: "widget"
18820
+ containerSizeClass: this.state.sizeClass,
18821
+ ref: this.widgetRef
18788
18822
  })), isStatic && /*#__PURE__*/React.createElement("div", {
18789
18823
  style: staticOverlayStyles
18790
- }));
18824
+ }))));
18791
18825
  }
18792
18826
  UNSAFE_componentWillReceiveProps(nextProps) {
18793
18827
  if (this.props.type !== nextProps.type) {
@@ -18810,6 +18844,7 @@ const rContainsNonWhitespace = /\S/;
18810
18844
  const rImageURL = /(web\+graphie|https):\/\/[^\s]*/;
18811
18845
  const noopOnRender = () => {};
18812
18846
  const SHOULD_CLEAR_WIDGETS_PROP_LIST = ["content", "problemNum", "widgets"];
18847
+ const makeContainerId = id => "container:" + id;
18813
18848
 
18814
18849
  // Check if one focus path / id path is a prefix of another
18815
18850
  // The focus path null will never be a prefix of any non-null
@@ -18854,6 +18889,7 @@ class Renderer extends React.Component {
18854
18889
  this._isMounted = void 0;
18855
18890
  this._isTwoColumn = void 0;
18856
18891
  this._translationLinter = void 0;
18892
+ this._widgetContainers = new Map();
18857
18893
  this.lastRenderedMarkdown = void 0;
18858
18894
  this.reuseMarkdown = void 0;
18859
18895
  this.translationIndex = void 0;
@@ -18924,19 +18960,22 @@ class Renderer extends React.Component {
18924
18960
  // By this point we should have no duplicates, which are
18925
18961
  // filtered out in this.render(), so we shouldn't have to
18926
18962
  // worry about using this widget key and ref:
18927
- return /*#__PURE__*/React.createElement(ErrorBoundary, {
18928
- key: "container:" + id,
18929
- metadata: {
18930
- widget_type: type,
18931
- widget_id: id
18932
- }
18933
- }, /*#__PURE__*/React.createElement(WidgetContainer, {
18934
- ref: "container:" + id,
18963
+ return /*#__PURE__*/React.createElement(WidgetContainer, {
18964
+ key: makeContainerId(id),
18965
+ id: id,
18966
+ ref: node => {
18967
+ const containerId = makeContainerId(id);
18968
+ if (node != null) {
18969
+ this._widgetContainers.set(containerId, node);
18970
+ } else {
18971
+ this._widgetContainers.delete(containerId);
18972
+ }
18973
+ },
18935
18974
  type: type,
18936
18975
  initialProps: this.getWidgetProps(id),
18937
18976
  shouldHighlight: shouldHighlight,
18938
18977
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "widget")
18939
- }));
18978
+ });
18940
18979
  }
18941
18980
  return null;
18942
18981
  };
@@ -18963,7 +19002,6 @@ class Renderer extends React.Component {
18963
19002
  getTracking(widgetInfo && widgetInfo.type));
18964
19003
  }
18965
19004
  return _extends({}, widgetProps, {
18966
- ref: id,
18967
19005
  widgetId: id,
18968
19006
  alignment: widgetInfo && widgetInfo.alignment,
18969
19007
  // When determining if a widget is static, we verify that the widget is not an
@@ -19095,12 +19133,10 @@ class Renderer extends React.Component {
19095
19133
  return [...this.findInternalWidgets(filterCriterion), ...this.props.findExternalWidgets(filterCriterion)];
19096
19134
  };
19097
19135
  this.getWidgetInstance = id => {
19098
- // eslint-disable-next-line react/no-string-refs
19099
- const ref = this.refs["container:" + id];
19136
+ const ref = this._widgetContainers.get(makeContainerId(id));
19100
19137
  if (!ref) {
19101
19138
  return null;
19102
19139
  }
19103
- // @ts-expect-error - TS2339 - Property 'getWidget' does not exist on type 'ReactInstance'.
19104
19140
  return ref.getWidget();
19105
19141
  };
19106
19142
  this._onWidgetFocus = (id, focusPath = []) => {
@@ -19788,7 +19824,9 @@ class Renderer extends React.Component {
19788
19824
  const widget = this.getWidgetInstance(id);
19789
19825
  // widget can be undefined if it hasn't yet been rendered
19790
19826
  if (widget && widget.simpleValidate) {
19791
- widgetScores[id] = widget.simpleValidate(props == null ? void 0 : props.options, onInputError);
19827
+ widgetScores[id] = widget.simpleValidate(_extends({}, props == null ? void 0 : props.options, {
19828
+ scoring: true
19829
+ }), onInputError);
19792
19830
  }
19793
19831
  });
19794
19832
  return widgetScores;
@@ -19938,9 +19976,7 @@ class Renderer extends React.Component {
19938
19976
  // they are re-rendered, so even if they've been
19939
19977
  // re-rendered we need to call these methods on them.
19940
19978
  _.each(this.widgetIds, id => {
19941
- // eslint-disable-next-line react/no-string-refs
19942
- const container = this.refs["container:" + id];
19943
- // @ts-expect-error - TS2339 - Property 'replaceWidgetProps' does not exist on type 'ReactInstance'.
19979
+ const container = this._widgetContainers.get(makeContainerId(id));
19944
19980
  container && container.replaceWidgetProps(this.getWidgetProps(id));
19945
19981
  });
19946
19982
  if (this.props.serializedState && !_.isEqual(this.props.serializedState, this.getSerializedState())) {
@@ -20858,21 +20894,6 @@ InputWithExamples.defaultProps = {
20858
20894
  className: ""
20859
20895
  };
20860
20896
 
20861
- function _objectWithoutPropertiesLoose(source, excluded) {
20862
- if (source == null) return {};
20863
- var target = {};
20864
- var sourceKeys = Object.keys(source);
20865
- var key, i;
20866
-
20867
- for (i = 0; i < sourceKeys.length; i++) {
20868
- key = sourceKeys[i];
20869
- if (excluded.indexOf(key) >= 0) continue;
20870
- target[key] = source[key];
20871
- }
20872
-
20873
- return target;
20874
- }
20875
-
20876
20897
  const _excluded$3 = ["keypadElement", "onFocus", "value"];
20877
20898
  class SimpleKeypadInput extends React.Component {
20878
20899
  constructor(...args) {
@@ -42117,8 +42138,7 @@ class Transformer extends React.Component {
42117
42138
  // @ts-expect-error - TS2345 - Argument of type '{ type: "dilation"; center: any; scale: number; }' is not assignable to parameter of type 'PerseusTransformerTransformation'.
42118
42139
  self.doTransform({
42119
42140
  type: "dilation",
42120
- // NOTE(kevinb): It's hard to convince that this.dilationCircle
42121
- // will still be defined here. center: self.dilationCircle.centerPoint.coord,
42141
+ center: self.dilationCircle.centerPoint.coord,
42122
42142
  scale: newRadius / oldRadius
42123
42143
  });
42124
42144
  },
@@ -43748,7 +43768,7 @@ const init = function init(options) {
43748
43768
 
43749
43769
  // This file is processed by a Rollup plugin (replace) to inject the production
43750
43770
  const libName = "@khanacademy/perseus";
43751
- const libVersion = "12.1.0";
43771
+ const libVersion = "12.3.0";
43752
43772
  addLibraryVersionToPerseusDebug(libName, libVersion);
43753
43773
 
43754
43774
  const apiVersion = {
@@ -43770,6 +43790,7 @@ class ArticleRenderer extends React.Component {
43770
43790
  constructor(props) {
43771
43791
  super(props);
43772
43792
  this._currentFocus = void 0;
43793
+ this.sectionRenderers = [];
43773
43794
  this._handleFocusChange = (newFocusPath, oldFocusPath) => {
43774
43795
  // TODO(charlie): DRY this up--some of this logic is repeated in
43775
43796
  // ItemRenderer.
@@ -43794,17 +43815,17 @@ class ArticleRenderer extends React.Component {
43794
43815
  // paths, so as to check whether the focused path represents an
43795
43816
  // input.
43796
43817
  let didFocusInput = false;
43818
+ let focusedInput;
43797
43819
  if (this._currentFocus) {
43798
- const [sectionRef, ...focusPath] = this._currentFocus;
43799
- // eslint-disable-next-line react/no-string-refs
43800
- // @ts-expect-error - TS2339 - Property 'getInputPaths' does not exist on type 'ReactInstance'.
43801
- const inputPaths = this.refs[sectionRef].getInputPaths();
43820
+ const [sectionIndex, ...focusPath] = this._currentFocus;
43821
+ const inputPaths = this.sectionRenderers[sectionIndex].getInputPaths();
43802
43822
  didFocusInput = inputPaths.some(inputPath => {
43803
43823
  return Util.inputPathsEqual(inputPath, focusPath);
43804
43824
  });
43825
+ focusedInput = this.sectionRenderers[sectionIndex].getDOMNodeForPath(focusPath);
43805
43826
  }
43806
43827
  if (this.props.apiOptions.onFocusChange != null) {
43807
- this.props.apiOptions.onFocusChange(this._currentFocus, prevFocusPath, didFocusInput ? keypadElement == null ? void 0 : keypadElement.getDOMNode() : null);
43828
+ this.props.apiOptions.onFocusChange(this._currentFocus, prevFocusPath, didFocusInput ? keypadElement == null ? void 0 : keypadElement.getDOMNode() : null, didFocusInput ? focusedInput : null);
43808
43829
  }
43809
43830
  if (keypadElement && isMobile) {
43810
43831
  if (didFocusInput) {
@@ -43836,10 +43857,8 @@ class ArticleRenderer extends React.Component {
43836
43857
  };
43837
43858
  this.blur = () => {
43838
43859
  if (this._currentFocus) {
43839
- const [sectionRef, ...inputPath] = this._currentFocus;
43840
- // eslint-disable-next-line react/no-string-refs
43841
- // @ts-expect-error - TS2339 - Property 'blurPath' does not exist on type 'ReactInstance'.
43842
- this.refs[sectionRef].blurPath(inputPath);
43860
+ const [sectionIndex, ...inputPath] = this._currentFocus;
43861
+ this.sectionRenderers[sectionIndex].blurPath(inputPath);
43843
43862
  }
43844
43863
  };
43845
43864
  this._sections = () => {
@@ -43865,33 +43884,39 @@ class ArticleRenderer extends React.Component {
43865
43884
  });
43866
43885
 
43867
43886
  // TODO(alex): Add mobile api functions and pass them down here
43868
- const sections = this._sections().map((section, i) => {
43869
- const refForSection = `section-${i}`;
43870
- return /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
43871
- value: this.props.dependencies
43872
- }, /*#__PURE__*/React.createElement("div", {
43873
- key: i,
43887
+ // We're using the index as the key here because we don't have a unique
43888
+ // identifier for each section. This should be fine as we never remove
43889
+ // or reorder sections.
43890
+ const sections = this._sections().map((section, sectionIndex) => {
43891
+ return /*#__PURE__*/React.createElement("div", {
43892
+ key: sectionIndex,
43874
43893
  className: "clearfix"
43875
43894
  }, /*#__PURE__*/React.createElement(Renderer, _extends({}, section, {
43876
- ref: refForSection,
43877
- key: i,
43878
- key_: i,
43895
+ ref: elem => {
43896
+ if (elem) {
43897
+ this.sectionRenderers[sectionIndex] = elem;
43898
+ }
43899
+ },
43900
+ key: sectionIndex,
43901
+ key_: sectionIndex,
43879
43902
  keypadElement: this.props.keypadElement,
43880
43903
  apiOptions: _extends({}, apiOptions, {
43881
43904
  onFocusChange: (newFocusPath, oldFocusPath) => {
43882
- // Prefix the paths with the relevant section,
43905
+ // Prefix the paths with the relevant section index,
43883
43906
  // so as to allow us to distinguish between
43884
43907
  // equivalently-named inputs across Renderers.
43885
- this._handleFocusChange(newFocusPath && [refForSection].concat(newFocusPath), oldFocusPath && [refForSection].concat(oldFocusPath));
43908
+ this._handleFocusChange(newFocusPath && [sectionIndex].concat(newFocusPath), oldFocusPath && [sectionIndex].concat(oldFocusPath));
43886
43909
  }
43887
43910
  }),
43888
43911
  linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "article"),
43889
43912
  legacyPerseusLint: this.props.legacyPerseusLint
43890
- }))));
43913
+ }, this.props.dependencies)));
43891
43914
  });
43892
43915
  return /*#__PURE__*/React.createElement("div", {
43893
43916
  className: classes
43894
- }, sections);
43917
+ }, /*#__PURE__*/React.createElement(DependenciesContext.Provider, {
43918
+ value: this.props.dependencies
43919
+ }, sections));
43895
43920
  }
43896
43921
  }
43897
43922
  ArticleRenderer.defaultProps = {
@@ -44970,7 +44995,7 @@ class ServerItemRenderer extends React.Component {
44970
44995
  };
44971
44996
  const questionRenderer = /*#__PURE__*/React.createElement(context$1.Provider, {
44972
44997
  value: contextValue
44973
- }, /*#__PURE__*/React.createElement(Renderer, {
44998
+ }, /*#__PURE__*/React.createElement(Renderer, _extends({
44974
44999
  keypadElement: this.props.keypadElement,
44975
45000
  problemNum: this.props.problemNum,
44976
45001
  onInteractWithWidget: this.handleInteractWithWidget,
@@ -44986,7 +45011,7 @@ class ServerItemRenderer extends React.Component {
44986
45011
  content: this.props.item.question.content,
44987
45012
  widgets: this.props.item.question.widgets,
44988
45013
  images: this.props.item.question.images
44989
- }));
45014
+ }, this.props.dependencies)));
44990
45015
  const hintsRenderer = /*#__PURE__*/React.createElement(HintsRenderer, {
44991
45016
  hints: this.props.item.hints,
44992
45017
  hintsVisible: this.props.hintsVisible,