@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
@@ -19,19 +19,20 @@ import type {KeypadAPI} from "@khanacademy/math-input";
19
19
  import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core";
20
20
  import type {LinterContextProps} from "@khanacademy/perseus-linter";
21
21
 
22
- type Props = {
23
- apiOptions: APIOptions;
24
- json: PerseusRenderer | ReadonlyArray<PerseusRenderer>;
25
- // Whether to use the new Bibliotron styles for articles
26
- /**
27
- * @deprecated Does nothing
28
- */
29
- useNewStyles: boolean;
30
- linterContext: LinterContextProps;
31
- legacyPerseusLint?: ReadonlyArray<string>;
32
- keypadElement?: KeypadAPI | null | undefined;
33
- dependencies: PerseusDependenciesV2;
34
- } & KeypadProps;
22
+ type Props = Partial<React.ContextType<typeof DependenciesContext>> &
23
+ KeypadProps & {
24
+ apiOptions: APIOptions;
25
+ json: PerseusRenderer | ReadonlyArray<PerseusRenderer>;
26
+ // Whether to use the new Bibliotron styles for articles
27
+ /**
28
+ * @deprecated Does nothing
29
+ */
30
+ useNewStyles: boolean;
31
+ linterContext: LinterContextProps;
32
+ legacyPerseusLint?: ReadonlyArray<string>;
33
+ keypadElement?: KeypadAPI | null | undefined;
34
+ dependencies: PerseusDependenciesV2;
35
+ };
35
36
 
36
37
  type DefaultProps = {
37
38
  apiOptions: Props["apiOptions"];
@@ -44,6 +45,7 @@ class ArticleRenderer
44
45
  implements KeypadContextRendererInterface
45
46
  {
46
47
  _currentFocus: any;
48
+ sectionRenderers: Array<Renderer> = [];
47
49
 
48
50
  static defaultProps: DefaultProps = {
49
51
  apiOptions: ApiOptions.defaults,
@@ -87,14 +89,21 @@ class ArticleRenderer
87
89
  // paths, so as to check whether the focused path represents an
88
90
  // input.
89
91
  let didFocusInput = false;
92
+ let focusedInput;
93
+
90
94
  if (this._currentFocus) {
91
- const [sectionRef, ...focusPath] = this._currentFocus;
92
- // eslint-disable-next-line react/no-string-refs
93
- // @ts-expect-error - TS2339 - Property 'getInputPaths' does not exist on type 'ReactInstance'.
94
- const inputPaths = this.refs[sectionRef].getInputPaths();
95
+ const [sectionIndex, ...focusPath] = this._currentFocus;
96
+
97
+ const inputPaths =
98
+ this.sectionRenderers[sectionIndex].getInputPaths();
99
+
95
100
  didFocusInput = inputPaths.some((inputPath) => {
96
101
  return Util.inputPathsEqual(inputPath, focusPath);
97
102
  });
103
+ focusedInput =
104
+ this.sectionRenderers[sectionIndex].getDOMNodeForPath(
105
+ focusPath,
106
+ );
98
107
  }
99
108
 
100
109
  if (this.props.apiOptions.onFocusChange != null) {
@@ -102,6 +111,7 @@ class ArticleRenderer
102
111
  this._currentFocus,
103
112
  prevFocusPath,
104
113
  didFocusInput ? keypadElement?.getDOMNode() : null,
114
+ didFocusInput ? focusedInput : null,
105
115
  );
106
116
  }
107
117
 
@@ -137,10 +147,8 @@ class ArticleRenderer
137
147
 
138
148
  blur: () => void = () => {
139
149
  if (this._currentFocus) {
140
- const [sectionRef, ...inputPath] = this._currentFocus;
141
- // eslint-disable-next-line react/no-string-refs
142
- // @ts-expect-error - TS2339 - Property 'blurPath' does not exist on type 'ReactInstance'.
143
- this.refs[sectionRef].blurPath(inputPath);
150
+ const [sectionIndex, ...inputPath] = this._currentFocus;
151
+ this.sectionRenderers[sectionIndex].blurPath(inputPath);
144
152
  }
145
153
  };
146
154
 
@@ -166,47 +174,54 @@ class ArticleRenderer
166
174
  });
167
175
 
168
176
  // TODO(alex): Add mobile api functions and pass them down here
169
- const sections = this._sections().map((section, i) => {
170
- const refForSection = `section-${i}`;
177
+ // We're using the index as the key here because we don't have a unique
178
+ // identifier for each section. This should be fine as we never remove
179
+ // or reorder sections.
180
+ const sections = this._sections().map((section, sectionIndex) => {
171
181
  return (
172
- <DependenciesContext.Provider value={this.props.dependencies}>
173
- <div key={i} className="clearfix">
174
- <Renderer
175
- {...section}
176
- ref={refForSection}
177
- key={i}
178
- key_={i}
179
- keypadElement={this.props.keypadElement}
180
- apiOptions={{
181
- ...apiOptions,
182
- onFocusChange: (newFocusPath, oldFocusPath) => {
183
- // Prefix the paths with the relevant section,
184
- // so as to allow us to distinguish between
185
- // equivalently-named inputs across Renderers.
186
- this._handleFocusChange(
187
- newFocusPath &&
188
- [refForSection].concat(
189
- newFocusPath,
190
- ),
191
- oldFocusPath &&
192
- [refForSection].concat(
193
- oldFocusPath,
194
- ),
195
- );
196
- },
197
- }}
198
- linterContext={PerseusLinter.pushContextStack(
199
- this.props.linterContext,
200
- "article",
201
- )}
202
- legacyPerseusLint={this.props.legacyPerseusLint}
203
- />
204
- </div>
205
- </DependenciesContext.Provider>
182
+ <div key={sectionIndex} className="clearfix">
183
+ <Renderer
184
+ {...section}
185
+ ref={(elem) => {
186
+ if (elem) {
187
+ this.sectionRenderers[sectionIndex] = elem;
188
+ }
189
+ }}
190
+ key={sectionIndex}
191
+ key_={sectionIndex}
192
+ keypadElement={this.props.keypadElement}
193
+ apiOptions={{
194
+ ...apiOptions,
195
+ onFocusChange: (newFocusPath, oldFocusPath) => {
196
+ // Prefix the paths with the relevant section index,
197
+ // so as to allow us to distinguish between
198
+ // equivalently-named inputs across Renderers.
199
+ this._handleFocusChange(
200
+ newFocusPath &&
201
+ [sectionIndex].concat(newFocusPath),
202
+ oldFocusPath &&
203
+ [sectionIndex].concat(oldFocusPath),
204
+ );
205
+ },
206
+ }}
207
+ linterContext={PerseusLinter.pushContextStack(
208
+ this.props.linterContext,
209
+ "article",
210
+ )}
211
+ legacyPerseusLint={this.props.legacyPerseusLint}
212
+ {...this.props.dependencies}
213
+ />
214
+ </div>
206
215
  );
207
216
  });
208
217
 
209
- return <div className={classes}>{sections}</div>;
218
+ return (
219
+ <div className={classes}>
220
+ <DependenciesContext.Provider value={this.props.dependencies}>
221
+ {sections}
222
+ </DependenciesContext.Provider>
223
+ </div>
224
+ );
210
225
  }
211
226
  }
212
227
 
@@ -5,6 +5,9 @@ import {Errors, Log} from "./logging/log";
5
5
  type Props = {
6
6
  children: React.ReactNode;
7
7
  metadata?: Record<string, string>;
8
+
9
+ // A callback that is called when the error boundary traps an error.
10
+ onError?: (error: Error, info: any) => void;
8
11
  };
9
12
  type State = {
10
13
  error: string;
@@ -218,7 +218,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
218
218
  style="border-color: transparent; border-radius: 50%;"
219
219
  >
220
220
  <div
221
- class="circle_1tys2th"
221
+ class="circle_1w6i3h1"
222
222
  data-is-radio-icon="true"
223
223
  data-test-id="choice-icon__library-choice-icon"
224
224
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -385,7 +385,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
385
385
  style="border-color: transparent; border-radius: 50%;"
386
386
  >
387
387
  <div
388
- class="circle_1tys2th"
388
+ class="circle_1w6i3h1"
389
389
  data-is-radio-icon="true"
390
390
  data-test-id="choice-icon__library-choice-icon"
391
391
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -552,7 +552,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
552
552
  style="border-color: transparent; border-radius: 50%;"
553
553
  >
554
554
  <div
555
- class="circle_1tys2th"
555
+ class="circle_1w6i3h1"
556
556
  data-is-radio-icon="true"
557
557
  data-test-id="choice-icon__library-choice-icon"
558
558
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -719,7 +719,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
719
719
  style="border-color: transparent; border-radius: 50%;"
720
720
  >
721
721
  <div
722
- class="circle_1tys2th"
722
+ class="circle_1w6i3h1"
723
723
  data-is-radio-icon="true"
724
724
  data-test-id="choice-icon__library-choice-icon"
725
725
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
@@ -886,7 +886,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
886
886
  style="border-color: transparent; border-radius: 50%;"
887
887
  >
888
888
  <div
889
- class="circle_1tys2th"
889
+ class="circle_1w6i3h1"
890
890
  data-is-radio-icon="true"
891
891
  data-test-id="choice-icon__library-choice-icon"
892
892
  style="border-color: rgba(33,36,44,0.64); color: rgba(33, 36, 44, 0.64); border-radius: 24px;"
package/src/renderer.tsx CHANGED
@@ -29,6 +29,7 @@ import preprocessTex from "./util/katex-preprocess";
29
29
  import WidgetContainer from "./widget-container";
30
30
  import * as Widgets from "./widgets";
31
31
 
32
+ import type {DependenciesContext} from "./dependencies";
32
33
  import type {PerseusRenderer, PerseusWidgetOptions} from "./perseus-types";
33
34
  import type {
34
35
  APIOptions,
@@ -52,6 +53,8 @@ const noopOnRender = () => {};
52
53
 
53
54
  const SHOULD_CLEAR_WIDGETS_PROP_LIST = ["content", "problemNum", "widgets"];
54
55
 
56
+ const makeContainerId = (id: string) => "container:" + id;
57
+
55
58
  // Check if one focus path / id path is a prefix of another
56
59
  // The focus path null will never be a prefix of any non-null
57
60
  // path, since it represents no focus.
@@ -144,7 +147,7 @@ export type Widget = {
144
147
  examples?: () => ReadonlyArray<string>;
145
148
  };
146
149
 
147
- type Props = {
150
+ type Props = Partial<React.ContextType<typeof DependenciesContext>> & {
148
151
  apiOptions?: APIOptions;
149
152
  alwaysUpdate?: boolean;
150
153
  findExternalWidgets: any;
@@ -223,6 +226,8 @@ class Renderer extends React.Component<Props, State> {
223
226
  // The i18n linter.
224
227
  _translationLinter: TranslationLinter;
225
228
 
229
+ _widgetContainers: Map<string, WidgetContainer> = new Map();
230
+
226
231
  lastRenderedMarkdown: React.ReactNode;
227
232
  // @ts-expect-error - TS2564 - Property 'reuseMarkdown' has no initializer and is not definitely assigned in the constructor.
228
233
  reuseMarkdown: boolean;
@@ -390,9 +395,7 @@ class Renderer extends React.Component<Props, State> {
390
395
  // they are re-rendered, so even if they've been
391
396
  // re-rendered we need to call these methods on them.
392
397
  _.each(this.widgetIds, (id) => {
393
- // eslint-disable-next-line react/no-string-refs
394
- const container = this.refs["container:" + id];
395
- // @ts-expect-error - TS2339 - Property 'replaceWidgetProps' does not exist on type 'ReactInstance'.
398
+ const container = this._widgetContainers.get(makeContainerId(id));
396
399
  container && container.replaceWidgetProps(this.getWidgetProps(id));
397
400
  });
398
401
 
@@ -550,21 +553,25 @@ class Renderer extends React.Component<Props, State> {
550
553
  // filtered out in this.render(), so we shouldn't have to
551
554
  // worry about using this widget key and ref:
552
555
  return (
553
- <ErrorBoundary
554
- key={"container:" + id}
555
- metadata={{widget_type: type, widget_id: id}}
556
- >
557
- <WidgetContainer
558
- ref={"container:" + id}
559
- type={type}
560
- initialProps={this.getWidgetProps(id)}
561
- shouldHighlight={shouldHighlight}
562
- linterContext={PerseusLinter.pushContextStack(
563
- this.props.linterContext,
564
- "widget",
565
- )}
566
- />
567
- </ErrorBoundary>
556
+ <WidgetContainer
557
+ key={makeContainerId(id)}
558
+ id={id}
559
+ ref={(node) => {
560
+ const containerId = makeContainerId(id);
561
+ if (node != null) {
562
+ this._widgetContainers.set(containerId, node);
563
+ } else {
564
+ this._widgetContainers.delete(containerId);
565
+ }
566
+ }}
567
+ type={type}
568
+ initialProps={this.getWidgetProps(id)}
569
+ shouldHighlight={shouldHighlight}
570
+ linterContext={PerseusLinter.pushContextStack(
571
+ this.props.linterContext,
572
+ "widget",
573
+ )}
574
+ />
568
575
  );
569
576
  }
570
577
  return null;
@@ -603,7 +610,6 @@ class Renderer extends React.Component<Props, State> {
603
610
 
604
611
  return {
605
612
  ...widgetProps,
606
- ref: id,
607
613
  widgetId: id,
608
614
  alignment: widgetInfo && widgetInfo.alignment,
609
615
  // When determining if a widget is static, we verify that the widget is not an
@@ -853,15 +859,11 @@ class Renderer extends React.Component<Props, State> {
853
859
  ];
854
860
  };
855
861
 
856
- getWidgetInstance: (id: string) => Widget | null | undefined = (
857
- id: string,
858
- ): Widget | null | undefined => {
859
- // eslint-disable-next-line react/no-string-refs
860
- const ref = this.refs["container:" + id];
862
+ getWidgetInstance: (id: string) => Widget | null | undefined = (id) => {
863
+ const ref = this._widgetContainers.get(makeContainerId(id));
861
864
  if (!ref) {
862
865
  return null;
863
866
  }
864
- // @ts-expect-error - TS2339 - Property 'getWidget' does not exist on type 'ReactInstance'.
865
867
  return ref.getWidget();
866
868
  };
867
869
 
@@ -1781,7 +1783,7 @@ class Renderer extends React.Component<Props, State> {
1781
1783
  // widget can be undefined if it hasn't yet been rendered
1782
1784
  if (widget && widget.simpleValidate) {
1783
1785
  widgetScores[id] = widget.simpleValidate(
1784
- props?.options,
1786
+ {...props?.options, scoring: true},
1785
1787
  onInputError,
1786
1788
  );
1787
1789
  }
@@ -416,6 +416,7 @@ export class ServerItemRenderer
416
416
  content={this.props.item.question.content}
417
417
  widgets={this.props.item.question.widgets}
418
418
  images={this.props.item.question.images}
419
+ {...this.props.dependencies}
419
420
  />
420
421
  </AssetContext.Provider>
421
422
  );
@@ -1,7 +1,7 @@
1
- // @baseFontFamily: "Proxima Nova", sans-serif;
2
- export const baseFontFamily = "'Proxima Nova',sans-serif";
3
- // @boldFontFamily: "Proxima Nova Semibold", sans-serif;
4
- export const boldFontFamily = "'Proxima Nova Semibold',sans-serif";
1
+ // @baseFontFamily: "Lato", sans-serif;
2
+ export const baseFontFamily = "'Lato', sans-serif";
3
+ // @boldFontFamily: "Lato-Bold", "Lato", sans-serif;
4
+ export const boldFontFamily = "'Lato-Bold', 'Lato', sans-serif";
5
5
  // @green: #76a005;
6
6
  export const green = "#76A005";
7
7
  // @kaGreen: #71b307;
@@ -113,16 +113,6 @@
113
113
 
114
114
  // HACK(yejia): Override the font size and line height for blurbs
115
115
  //
116
- // NOTE(yejia): Minion Pro is only loaded on GTP pages, because those
117
- // are the main call sites for blurbs and the passage widget. We
118
- // considered inserting the relevant stylesheet whenever the passage
119
- // widget loads, but this approach produced a "flash of unstyled
120
- // text", so we decided to embed the stylesheet into the relevant
121
- // page templates instead.
122
- //
123
- // NOTE(yejia): Minion Pro will not be available on ZNDs, due to
124
- // restrictions in our Adobe TypeKit configuration.
125
- //
126
116
  // TODO(TP-3315): Remove once Perseus uses Wonder Blocks BodySerif
127
117
  .test-prep-blurb div.paragraph {
128
118
  font-size: 16px;
@@ -441,6 +431,7 @@
441
431
  /* Widget CSS */
442
432
 
443
433
  .perseus-graph-padding {
434
+ box-sizing: content-box;
444
435
  padding: 25px 25px 0 0;
445
436
  }
446
437
 
@@ -1,6 +1,6 @@
1
1
  // Steal from webapp/stylesheets/shared-package/variables.less
2
- @baseFontFamily: "Proxima Nova", sans-serif;
3
- @boldFontFamily: "Proxima Nova Semibold", sans-serif;
2
+ @baseFontFamily: "Lato", sans-serif;
3
+ @boldFontFamily: "Lato-Bold", "Lato", sans-serif;
4
4
  @green: #76a005;
5
5
  @kaGreen: #71b307;
6
6
  @blue: #1c758a;
@@ -86,8 +86,8 @@
86
86
 
87
87
  // Typography
88
88
  @baseFontSize: 14px;
89
- // The body of the page is setup to use 'Helvetica' by default and 'Proxima
90
- // Nova' (or 'Lato' if you're testing our new typography) if/once it downloads.
89
+ // The body of the page is setup to use 'Helvetica' by default and
90
+ // 'Lato' if/once it downloads.
91
91
  // For your elements to follow this pattern, all you need is to make the text
92
92
  // in your element inherit from the body using the "inherit" value.
93
93
  @baseFontFamily: inherit;
@@ -63,80 +63,3 @@
63
63
  }
64
64
  }
65
65
  }
66
-
67
- // Old expression widget
68
- .perseus-widget-expression-old {
69
- &,
70
- > .output,
71
- > .output > .tex,
72
- > .output > .placeholder {
73
- display: block;
74
- }
75
- }
76
-
77
- .perseus-widget-expression-old input,
78
- #answer_area .perseus-widget-expression-old input {
79
- direction: ltr;
80
- border: 1px solid #a4a4a4;
81
- border-radius: 5px;
82
- box-sizing: border-box;
83
- font-size: 14px;
84
- margin-bottom: 5px;
85
- max-width: 240px;
86
- padding: 6px;
87
- width: 100%;
88
- }
89
-
90
- .perseus-widget-expression-old > .output {
91
- background: #f2f2f2;
92
- border-radius: 5px;
93
- padding: 10px;
94
- margin: 10px 0;
95
- }
96
-
97
- #answer_area .perseus-widget-expression-old > .output {
98
- background: #f7f7f7;
99
- }
100
-
101
- .perseus-widget-expression-old > .output > .tex {
102
- overflow-x: auto;
103
- padding: 5px;
104
- }
105
-
106
- .perseus-widget-expression-old > .output > .placeholder {
107
- position: relative;
108
- height: 40px;
109
- overflow-y: hidden;
110
- }
111
-
112
- .perseus-widget-expression-old > .output > .placeholder > .error {
113
- border-radius: 10px;
114
- background: #f7f7f7;
115
- border: 1px solid #ddd;
116
- box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
117
- color: #000;
118
- display: none;
119
- font-weight: normal;
120
- min-height: 22px;
121
- width: 168px;
122
- position: absolute;
123
- top: 0px;
124
- left: 40px;
125
- margin: auto;
126
- }
127
-
128
- .perseus-widget-expression-old > .output > .placeholder > .error > .buddy {
129
- background-image: url(/images/perseus/error-buddy.png);
130
- background-size: 100%;
131
- height: 36px;
132
- width: 40px;
133
- position: absolute;
134
- top: -3px;
135
- left: -42px;
136
- }
137
-
138
- .perseus-widget-expression-old > .output > .placeholder > .error > .message {
139
- font: 12px @baseFontFamily;
140
- line-height: 1.4em;
141
- margin: 0px 4px;
142
- }
@@ -17,26 +17,8 @@
17
17
  margin: 22px;
18
18
  position: relative;
19
19
 
20
- // Use the "Minion Pro" font, if available. If not, fall back to
21
- // KaTeX_Main or another serif font.
22
- //
23
- // NOTE(mdr): Minion Pro is only loaded on SAT and GTP pages, because
24
- // those are the main call sites for the passage widget. We
25
- // considered inserting the relevant stylesheet whenever the
26
- // passage widget loads, but this approach produced a "flash of
27
- // unstyled text", so we decided to embed the stylesheet into the
28
- // relevant page templates instead.
29
- //
30
- // NOTE(mdr): Minion Pro will not be available on ZNDs, due to
31
- // restrictions in our Adobe TypeKit configuration.
32
- //
33
- // TODO(mdr): The BodySerif component will hopefully soon use Minion
34
- // Pro by default (WB-399). Once that happens, we should be able to
35
- // switch to the BodySerif component (TP-3112), and remove this
36
- // special font-family rule.
37
20
  div.paragraph {
38
- font-family: "minion-pro", KaTeX_Main, Times, "Times New Roman",
39
- serif;
21
+ font-family: KaTeX_Main, Times, "Times New Roman", serif;
40
22
  }
41
23
 
42
24
  .passage-title div.paragraph {
@@ -4,6 +4,8 @@ import classNames from "classnames";
4
4
  import * as React from "react";
5
5
  import ReactDOM from "react-dom";
6
6
 
7
+ import {DependenciesContext} from "./dependencies";
8
+ import ErrorBoundary from "./error-boundary";
7
9
  import {zIndexInteractiveComponent} from "./styles/constants";
8
10
  import {containerSizeClass, getClassFromWidth} from "./util/sizing-utils";
9
11
  import * as Widgets from "./widgets";
@@ -15,6 +17,7 @@ import type {LinterContextProps} from "@khanacademy/perseus-linter";
15
17
  type Props = {
16
18
  shouldHighlight: boolean;
17
19
  type: string; // widget type/name,
20
+ id: string; // widget id
18
21
  initialProps: WidgetProps<any, PerseusWidgetOptions>;
19
22
  linterContext: LinterContextProps;
20
23
  };
@@ -29,6 +32,8 @@ type State = {
29
32
  };
30
33
 
31
34
  class WidgetContainer extends React.Component<Props, State> {
35
+ widgetRef = React.createRef<React.ComponentType<any>>();
36
+
32
37
  static defaultProps: DefaultProps = {
33
38
  linterContext: linterContextDefault,
34
39
  };
@@ -132,14 +137,32 @@ class WidgetContainer extends React.Component<Props, State> {
132
137
  className={className}
133
138
  style={isStatic ? staticContainerStyles : {}}
134
139
  >
135
- <WidgetType
136
- {...this.state.widgetProps}
137
- linterContext={linterContext}
138
- containerSizeClass={this.state.sizeClass}
139
- // eslint-disable-next-line react/no-string-refs
140
- ref="widget"
141
- />
142
- {isStatic && <div style={staticOverlayStyles} />}
140
+ <DependenciesContext.Consumer>
141
+ {({analytics}) => (
142
+ <ErrorBoundary
143
+ metadata={{
144
+ widget_type: type,
145
+ widget_id: this.props.id,
146
+ }}
147
+ onError={(error) => {
148
+ analytics.onAnalyticsEvent({
149
+ type: "perseus:widget-rendering-error",
150
+ payload: {
151
+ widgetType: type,
152
+ },
153
+ });
154
+ }}
155
+ >
156
+ <WidgetType
157
+ {...this.state.widgetProps}
158
+ linterContext={linterContext}
159
+ containerSizeClass={this.state.sizeClass}
160
+ ref={this.widgetRef}
161
+ />
162
+ {isStatic && <div style={staticOverlayStyles} />}
163
+ </ErrorBoundary>
164
+ )}
165
+ </DependenciesContext.Consumer>
143
166
  </div>
144
167
  );
145
168
  }
@@ -163,8 +186,7 @@ class WidgetContainer extends React.Component<Props, State> {
163
186
  }
164
187
 
165
188
  getWidget: () => any = () => {
166
- // eslint-disable-next-line react/no-string-refs
167
- return this.refs.widget;
189
+ return this.widgetRef.current;
168
190
  };
169
191
 
170
192
  replaceWidgetProps: (arg1: WidgetProps<any, PerseusWidgetOptions>) => void =
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+
3
+ import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
4
+ import {question} from "../__testdata__/label-image.testdata";
5
+
6
+ import type {APIOptions} from "../../types";
7
+
8
+ type StoryArgs = {
9
+ isMobile: boolean;
10
+ };
11
+
12
+ type ImageStory = {
13
+ title: string;
14
+ args: StoryArgs;
15
+ };
16
+
17
+ export const Question1 = (args: StoryArgs): React.ReactElement => {
18
+ const apiOptions: APIOptions = {
19
+ isMobile: args.isMobile,
20
+ };
21
+
22
+ return <RendererWithDebugUI question={question} apiOptions={apiOptions} />;
23
+ };
24
+
25
+ export default {
26
+ title: "Perseus/Widgets/Label Image",
27
+ args: {
28
+ isMobile: false,
29
+ },
30
+ } as ImageStory;