@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.
- package/CHANGELOG.md +31 -0
- package/dist/article-renderer.d.ts +5 -2
- package/dist/error-boundary.d.ts +1 -0
- package/dist/es/index.css +4 -66
- package/dist/es/index.js +112 -87
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +4 -66
- package/dist/index.js +103 -75
- package/dist/index.js.map +1 -1
- package/dist/renderer.d.ts +4 -1
- package/dist/styles/constants.d.ts +2 -2
- package/dist/widget-container.d.ts +2 -0
- package/dist/widgets/expression.d.ts +5 -4
- package/package.json +8 -8
- package/src/__stories__/article-renderer.stories.tsx +28 -2
- package/src/__testdata__/article-renderer.testdata.ts +85 -0
- package/src/__tests__/article-renderer.test.tsx +131 -0
- package/src/__tests__/renderer-api.test.tsx +18 -23
- package/src/article-renderer.tsx +73 -58
- package/src/error-boundary.tsx +3 -0
- package/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +5 -5
- package/src/renderer.tsx +29 -27
- package/src/server-item-renderer.tsx +1 -0
- package/src/styles/constants.ts +4 -4
- package/src/styles/perseus-renderer.less +1 -10
- package/src/styles/variables.less +2 -2
- package/src/styles/variables.shared.less +2 -2
- package/src/styles/widgets/expression.less +0 -77
- package/src/styles/widgets/passage.less +1 -19
- package/src/widget-container.tsx +32 -10
- package/src/widgets/__stories__/label-image.stories.tsx +30 -0
- package/src/widgets/__testdata__/label-image.testdata.ts +63 -0
- package/src/widgets/__tests__/__snapshots__/group.test.tsx.snap +5 -5
- package/src/widgets/__tests__/__snapshots__/radio.test.ts.snap +28 -28
- package/src/widgets/__tests__/expression.test.tsx +12 -9
- package/src/widgets/__tests__/renderQuestion.tsx +30 -13
- package/src/widgets/expression.tsx +19 -25
- package/src/widgets/transformer.tsx +1 -2
- package/tsconfig-build.tsbuildinfo +1 -1
package/src/article-renderer.tsx
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 [
|
|
141
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
<
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
linterContext
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
|
package/src/error-boundary.tsx
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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
|
-
<
|
|
554
|
-
key={
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
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
|
}
|
package/src/styles/constants.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// @baseFontFamily: "
|
|
2
|
-
export const baseFontFamily = "'
|
|
3
|
-
// @boldFontFamily: "
|
|
4
|
-
export const boldFontFamily = "'
|
|
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: "
|
|
3
|
-
@boldFontFamily: "
|
|
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
|
|
90
|
-
//
|
|
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:
|
|
39
|
-
serif;
|
|
21
|
+
font-family: KaTeX_Main, Times, "Times New Roman", serif;
|
|
40
22
|
}
|
|
41
23
|
|
|
42
24
|
.passage-title div.paragraph {
|
package/src/widget-container.tsx
CHANGED
|
@@ -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
|
-
<
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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;
|