@khanacademy/perseus 11.6.1 → 12.0.1
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/components/math-input.d.ts +38 -11
- package/dist/components/tex-buttons.d.ts +2 -1
- package/dist/es/index.css +5 -6
- package/dist/es/index.js +999 -919
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +5 -6
- package/dist/index.js +976 -895
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +2 -1
- package/dist/strings.js +3 -0
- package/dist/widgets/expression.d.ts +15 -14
- package/package.json +8 -8
- package/src/__tests__/mock-asset-loading-widget.tsx +1 -2
- package/src/components/__stories__/math-input.stories.tsx +17 -4
- package/src/components/__tests__/math-input.test.tsx +95 -8
- package/src/components/math-input.tsx +274 -96
- package/src/components/tex-buttons.tsx +2 -2
- package/src/perseus-types.ts +11 -1
- package/src/styles/perseus-renderer.less +5 -6
- package/src/styles/widgets/expression.less +0 -6
- package/src/widgets/__stories__/expression.stories.tsx +3 -3
- package/src/widgets/__tests__/expression.test.tsx +4 -77
- package/src/widgets/expression.tsx +99 -94
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import * as KAS from "@khanacademy/kas";
|
|
2
|
-
import {KeypadInput, KeypadType} from "@khanacademy/math-input";
|
|
2
|
+
import {KeyArray, KeypadInput, KeypadType} from "@khanacademy/math-input";
|
|
3
3
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
4
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
4
5
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
6
|
+
import Tooltip from "@khanacademy/wonder-blocks-tooltip";
|
|
5
7
|
import classNames from "classnames";
|
|
6
8
|
import * as React from "react";
|
|
7
9
|
import _ from "underscore";
|
|
8
10
|
|
|
9
|
-
import InlineIcon from "../components/inline-icon";
|
|
10
11
|
import MathInput from "../components/math-input";
|
|
11
|
-
import Tooltip, {
|
|
12
|
-
HorizontalDirection,
|
|
13
|
-
VerticalDirection,
|
|
14
|
-
} from "../components/tooltip";
|
|
15
12
|
import {useDependencies} from "../dependencies";
|
|
16
|
-
import {iconExclamationSign} from "../icon-paths";
|
|
17
13
|
import {Errors as PerseusErrors, Log} from "../logging/log";
|
|
18
14
|
import * as Changeable from "../mixins/changeable";
|
|
19
15
|
import {ApiOptions, ClassNames as ApiClassNames} from "../perseus-api";
|
|
16
|
+
import a11y from "../util/a11y";
|
|
20
17
|
import KhanAnswerTypes from "../util/answer-types";
|
|
21
18
|
|
|
19
|
+
import type {DependenciesContext} from "../dependencies";
|
|
22
20
|
import type {
|
|
23
21
|
PerseusExpressionWidgetOptions,
|
|
24
22
|
PerseusExpressionAnswerForm,
|
|
@@ -34,6 +32,7 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core";
|
|
|
34
32
|
|
|
35
33
|
type InputPath = ReadonlyArray<string>;
|
|
36
34
|
|
|
35
|
+
const ERROR_TITLE = i18n._("Oops!");
|
|
37
36
|
const ERROR_MESSAGE = i18n._("Sorry, I don't understand that!");
|
|
38
37
|
|
|
39
38
|
const insertBraces = (value) => {
|
|
@@ -85,7 +84,7 @@ type RenderProps = {
|
|
|
85
84
|
type ExternalProps = WidgetProps<RenderProps, Rubric>;
|
|
86
85
|
|
|
87
86
|
export type Props = ExternalProps &
|
|
88
|
-
|
|
87
|
+
Partial<React.ContextType<typeof DependenciesContext>> & {
|
|
89
88
|
apiOptions: NonNullable<ExternalProps["apiOptions"]>;
|
|
90
89
|
buttonSets: NonNullable<ExternalProps["buttonSets"]>;
|
|
91
90
|
functions: NonNullable<ExternalProps["functions"]>;
|
|
@@ -97,8 +96,9 @@ export type Props = ExternalProps &
|
|
|
97
96
|
};
|
|
98
97
|
|
|
99
98
|
export type ExpressionState = {
|
|
99
|
+
invalid: boolean;
|
|
100
100
|
showErrorTooltip: boolean;
|
|
101
|
-
|
|
101
|
+
showErrorStyle: boolean;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
type DefaultProps = {
|
|
@@ -121,7 +121,6 @@ type OnInputErrorFunctionType = (
|
|
|
121
121
|
// The new, MathQuill input expression widget
|
|
122
122
|
export class Expression extends React.Component<Props, ExpressionState> {
|
|
123
123
|
_isMounted = false;
|
|
124
|
-
errorTimeout: null | number = null;
|
|
125
124
|
|
|
126
125
|
//#region Previously a class extension
|
|
127
126
|
/* Content creators input a list of answers which are matched from top to
|
|
@@ -315,11 +314,14 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
315
314
|
displayName = "Expression";
|
|
316
315
|
|
|
317
316
|
state: ExpressionState = {
|
|
317
|
+
invalid: false,
|
|
318
318
|
showErrorTooltip: false,
|
|
319
|
-
|
|
319
|
+
showErrorStyle: false,
|
|
320
320
|
};
|
|
321
321
|
|
|
322
322
|
componentDidMount: () => void = () => {
|
|
323
|
+
document.addEventListener("mousedown", this._handleMouseDown);
|
|
324
|
+
|
|
323
325
|
// TODO(scottgrant): This is a hack to remove the deprecated call to
|
|
324
326
|
// this.isMounted() but is still considered an anti-pattern.
|
|
325
327
|
this._isMounted = true;
|
|
@@ -334,42 +336,38 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
334
336
|
!_.isEqual(this.props.value, prevProps.value) ||
|
|
335
337
|
!_.isEqual(this.props.functions, prevProps.functions)
|
|
336
338
|
) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (this.parse(this.props.value, this.props).parsed) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this.props.value,
|
|
354
|
-
ERROR_MESSAGE,
|
|
355
|
-
);
|
|
356
|
-
if (apiResult !== false) {
|
|
357
|
-
this.setState({showErrorTooltip: true});
|
|
358
|
-
}
|
|
359
|
-
}, 2000);
|
|
339
|
+
this.setState({
|
|
340
|
+
invalid: false,
|
|
341
|
+
showErrorTooltip: false,
|
|
342
|
+
showErrorStyle: false,
|
|
343
|
+
});
|
|
344
|
+
if (!this.parse(this.props.value, this.props).parsed) {
|
|
345
|
+
const apiResult = this.props.apiOptions.onInputError(
|
|
346
|
+
null, // reserved for some widget identifier
|
|
347
|
+
this.props.value,
|
|
348
|
+
ERROR_MESSAGE,
|
|
349
|
+
);
|
|
350
|
+
if (apiResult !== false) {
|
|
351
|
+
this.setState({
|
|
352
|
+
invalid: true,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
360
355
|
}
|
|
361
356
|
}
|
|
362
357
|
};
|
|
363
358
|
|
|
364
359
|
componentWillUnmount: () => void = () => {
|
|
365
|
-
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
366
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
367
|
-
// @ts-expect-error - TS2769 - No overload matches this call.
|
|
368
|
-
clearTimeout(this.errorTimeout);
|
|
369
|
-
|
|
370
360
|
this._isMounted = false;
|
|
371
361
|
};
|
|
372
362
|
|
|
363
|
+
_handleMouseDown = () => {
|
|
364
|
+
if (this._isMounted && this.state.showErrorTooltip) {
|
|
365
|
+
this.setState({
|
|
366
|
+
showErrorTooltip: false,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
373
371
|
simpleValidate: (
|
|
374
372
|
rubric: Rubric,
|
|
375
373
|
onInputError: OnInputErrorFunctionType,
|
|
@@ -422,7 +420,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
422
420
|
};
|
|
423
421
|
|
|
424
422
|
_handleFocus: () => void = () => {
|
|
425
|
-
this.props.analytics
|
|
423
|
+
this.props.analytics?.onAnalyticsEvent({
|
|
426
424
|
type: "perseus:expression-focused",
|
|
427
425
|
payload: null,
|
|
428
426
|
});
|
|
@@ -497,7 +495,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
497
495
|
};
|
|
498
496
|
|
|
499
497
|
sendExpressionEvaluatedEvent(result: "correct" | "incorrect" | "invalid") {
|
|
500
|
-
this.props.analytics
|
|
498
|
+
this.props.analytics?.onAnalyticsEvent({
|
|
501
499
|
type: "perseus:expression-evaluated",
|
|
502
500
|
payload: {
|
|
503
501
|
result,
|
|
@@ -536,41 +534,6 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
536
534
|
/>
|
|
537
535
|
);
|
|
538
536
|
}
|
|
539
|
-
// TODO(alex): Style this tooltip to be more consistent with other
|
|
540
|
-
// tooltips on the site; align to left middle (once possible)
|
|
541
|
-
const errorTooltip = (
|
|
542
|
-
<span className="error-tooltip" role="tooltip">
|
|
543
|
-
<Tooltip
|
|
544
|
-
className="error-text-container"
|
|
545
|
-
horizontalPosition={HorizontalDirection.Right}
|
|
546
|
-
horizontalAlign={HorizontalDirection.Left}
|
|
547
|
-
verticalPosition={VerticalDirection.Top}
|
|
548
|
-
arrowSize={10}
|
|
549
|
-
borderColor="#fcc335"
|
|
550
|
-
show={this.state.showErrorText}
|
|
551
|
-
>
|
|
552
|
-
<span
|
|
553
|
-
className="error-icon"
|
|
554
|
-
data-test-id="test-error-icon"
|
|
555
|
-
onMouseEnter={() => {
|
|
556
|
-
this.setState({showErrorText: true});
|
|
557
|
-
}}
|
|
558
|
-
onMouseLeave={() => {
|
|
559
|
-
this.setState({showErrorText: false});
|
|
560
|
-
}}
|
|
561
|
-
onClick={() => {
|
|
562
|
-
// TODO(alex): Better error feedback for mobile
|
|
563
|
-
this.setState({
|
|
564
|
-
showErrorText: !this.state.showErrorText,
|
|
565
|
-
});
|
|
566
|
-
}}
|
|
567
|
-
>
|
|
568
|
-
<InlineIcon {...iconExclamationSign} />
|
|
569
|
-
</span>
|
|
570
|
-
<div className="error-text">{ERROR_MESSAGE}</div>
|
|
571
|
-
</Tooltip>
|
|
572
|
-
</span>
|
|
573
|
-
);
|
|
574
537
|
|
|
575
538
|
const className = classNames({
|
|
576
539
|
"perseus-widget-expression": true,
|
|
@@ -578,20 +541,54 @@ export class Expression extends React.Component<Props, ExpressionState> {
|
|
|
578
541
|
});
|
|
579
542
|
|
|
580
543
|
return (
|
|
581
|
-
<div
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
544
|
+
<div
|
|
545
|
+
className={className}
|
|
546
|
+
onBlur={() =>
|
|
547
|
+
this.state.invalid &&
|
|
548
|
+
this.setState({
|
|
549
|
+
showErrorTooltip: true,
|
|
550
|
+
showErrorStyle: true,
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
onFocus={() =>
|
|
554
|
+
this.setState({
|
|
555
|
+
showErrorTooltip: false,
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
>
|
|
559
|
+
{/**
|
|
560
|
+
* This is a visually hidden container for the error tooltip.
|
|
561
|
+
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role#example_3_visually_hidden_alert_container_for_screen_reader_notifications
|
|
562
|
+
*/}
|
|
563
|
+
<View style={a11y.srOnly} role="alert">
|
|
564
|
+
{this.state.showErrorTooltip &&
|
|
565
|
+
ERROR_TITLE + " " + ERROR_MESSAGE}
|
|
566
|
+
</View>
|
|
567
|
+
<Tooltip
|
|
568
|
+
forceAnchorFocusivity={false}
|
|
569
|
+
opened={this.state.showErrorTooltip}
|
|
570
|
+
title={ERROR_TITLE}
|
|
571
|
+
content={ERROR_MESSAGE}
|
|
572
|
+
>
|
|
573
|
+
<MathInput
|
|
574
|
+
// eslint-disable-next-line react/no-string-refs
|
|
575
|
+
ref="input"
|
|
576
|
+
className={ApiClassNames.INTERACTIVE}
|
|
577
|
+
value={this.props.value}
|
|
578
|
+
onChange={this.changeAndTrack}
|
|
579
|
+
convertDotToTimes={this.props.times}
|
|
580
|
+
buttonSets={this.props.buttonSets}
|
|
581
|
+
onFocus={this._handleFocus}
|
|
582
|
+
onBlur={this._handleBlur}
|
|
583
|
+
hasError={this.state.showErrorStyle}
|
|
584
|
+
extraKeys={this.props.keypadConfiguration?.extraKeys}
|
|
585
|
+
analytics={
|
|
586
|
+
this.props.analytics ?? {
|
|
587
|
+
onAnalyticsEvent: async () => {},
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/>
|
|
591
|
+
</Tooltip>
|
|
595
592
|
</div>
|
|
596
593
|
);
|
|
597
594
|
}
|
|
@@ -616,8 +613,8 @@ const keypadConfigurationForProps = (
|
|
|
616
613
|
const keypadType = KeypadType.EXPRESSION;
|
|
617
614
|
|
|
618
615
|
// Extract any and all variables and constants from the answer forms.
|
|
619
|
-
const uniqueExtraVariables: Record<
|
|
620
|
-
const uniqueExtraConstants: Record<
|
|
616
|
+
const uniqueExtraVariables: Partial<Record<Key, boolean>> = {};
|
|
617
|
+
const uniqueExtraConstants: Partial<Record<Key, boolean>> = {};
|
|
621
618
|
for (const answerForm of widgetOptions.answerForms) {
|
|
622
619
|
const maybeExpr = KAS.parse(answerForm.value, widgetOptions);
|
|
623
620
|
if (maybeExpr.parsed) {
|
|
@@ -630,12 +627,20 @@ const keypadConfigurationForProps = (
|
|
|
630
627
|
symbol === "pi" || symbol === "theta";
|
|
631
628
|
const toKey = (symbol: any) =>
|
|
632
629
|
isGreek(symbol) ? symbol.toUpperCase() : symbol;
|
|
630
|
+
const isKey = (key: string): key is Key =>
|
|
631
|
+
KeyArray.includes(key as Key);
|
|
633
632
|
|
|
634
633
|
for (const variable of expr.getVars()) {
|
|
635
|
-
|
|
634
|
+
const maybeKey = toKey(variable);
|
|
635
|
+
if (isKey(maybeKey)) {
|
|
636
|
+
uniqueExtraVariables[maybeKey] = true;
|
|
637
|
+
}
|
|
636
638
|
}
|
|
637
639
|
for (const constant of expr.getConsts()) {
|
|
638
|
-
|
|
640
|
+
const maybeKey = toKey(constant);
|
|
641
|
+
if (isKey(maybeKey)) {
|
|
642
|
+
uniqueExtraConstants[maybeKey] = true;
|
|
643
|
+
}
|
|
639
644
|
}
|
|
640
645
|
}
|
|
641
646
|
}
|