@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,41 +1,79 @@
|
|
|
1
|
+
/* eslint-disable @khanacademy/ts-no-error-suppressions */
|
|
1
2
|
import {
|
|
3
|
+
DesktopKeypad,
|
|
2
4
|
keyTranslator,
|
|
3
5
|
createMathField,
|
|
4
6
|
mathQuillInstance,
|
|
7
|
+
CursorContext,
|
|
8
|
+
getCursorContext,
|
|
5
9
|
} from "@khanacademy/math-input";
|
|
10
|
+
import Clickable from "@khanacademy/wonder-blocks-clickable";
|
|
11
|
+
import Color, {fade} from "@khanacademy/wonder-blocks-color";
|
|
12
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
13
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
14
|
+
import {Popover, PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
|
|
15
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
16
|
+
import {StyleSheet} from "aphrodite";
|
|
6
17
|
import classNames from "classnames";
|
|
7
18
|
import $ from "jquery";
|
|
8
19
|
import * as React from "react";
|
|
9
|
-
import ReactDOM from "react-dom";
|
|
10
20
|
import _ from "underscore";
|
|
11
21
|
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
import type {
|
|
15
|
-
import type {MathFieldInterface} from "@khanacademy/math-input";
|
|
22
|
+
import type {LegacyButtonSets} from "../perseus-types";
|
|
23
|
+
import type {PerseusDependenciesV2} from "../types";
|
|
24
|
+
import type {Keys, MathFieldInterface} from "@khanacademy/math-input";
|
|
16
25
|
|
|
17
26
|
type ButtonsVisibleType = "always" | "never" | "focused";
|
|
18
27
|
|
|
28
|
+
type KeypadButtonSets = {
|
|
29
|
+
advancedRelations?: boolean;
|
|
30
|
+
basicRelations?: boolean;
|
|
31
|
+
divisionKey?: boolean;
|
|
32
|
+
logarithms?: boolean;
|
|
33
|
+
preAlgebra?: boolean;
|
|
34
|
+
trigonometry?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
19
37
|
type Props = {
|
|
20
38
|
className?: string;
|
|
21
39
|
value: string;
|
|
22
40
|
onChange: any;
|
|
23
41
|
convertDotToTimes: boolean;
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
/**
|
|
43
|
+
* @deprecated Use `keypadButtonSets` instead. Maps to `keypadButtonSets`.
|
|
44
|
+
* @see keypadButtonSets
|
|
45
|
+
*/
|
|
46
|
+
buttonSets?: LegacyButtonSets;
|
|
47
|
+
/**
|
|
48
|
+
* Overrides deprecated `buttonSets` prop.
|
|
49
|
+
*/
|
|
50
|
+
keypadButtonSets?: KeypadButtonSets;
|
|
26
51
|
labelText?: string;
|
|
27
52
|
onFocus?: () => void;
|
|
28
53
|
onBlur?: () => void;
|
|
54
|
+
hasError?: boolean;
|
|
55
|
+
extraKeys?: ReadonlyArray<Keys>;
|
|
56
|
+
/**
|
|
57
|
+
* Whether to show the keypad buttons.
|
|
58
|
+
* The strings now misleading, but we keep them for backwards compatibility.
|
|
59
|
+
* - `focused` means that the keypad **appears on toggle, *off* by default**.
|
|
60
|
+
* - `always` means that the keypad **appears on toggle, *on* by default.**
|
|
61
|
+
* - `never` means that the keypad is **never shown**.
|
|
62
|
+
*/
|
|
63
|
+
buttonsVisible?: ButtonsVisibleType;
|
|
64
|
+
analytics: PerseusDependenciesV2["analytics"];
|
|
29
65
|
};
|
|
30
66
|
|
|
31
67
|
type DefaultProps = {
|
|
32
68
|
value: Props["value"];
|
|
33
69
|
convertDotToTimes: Props["convertDotToTimes"];
|
|
34
|
-
buttonsVisible: Props["buttonsVisible"];
|
|
35
70
|
};
|
|
36
71
|
|
|
37
72
|
type State = {
|
|
38
73
|
focused: boolean;
|
|
74
|
+
keypadOpen: boolean;
|
|
75
|
+
cursorContext: typeof CursorContext[keyof typeof CursorContext];
|
|
76
|
+
openedWithEventType?: string;
|
|
39
77
|
};
|
|
40
78
|
|
|
41
79
|
const customKeyTranslator = {
|
|
@@ -62,81 +100,30 @@ class MathInput extends React.Component<Props, State> {
|
|
|
62
100
|
static defaultProps: DefaultProps = {
|
|
63
101
|
value: "",
|
|
64
102
|
convertDotToTimes: false,
|
|
65
|
-
buttonsVisible: "focused",
|
|
66
103
|
};
|
|
67
104
|
|
|
68
|
-
state: State = {
|
|
105
|
+
state: State = {
|
|
106
|
+
focused: false,
|
|
107
|
+
keypadOpen: this.props.buttonsVisible === "always" ? true : false,
|
|
108
|
+
cursorContext: CursorContext.NONE,
|
|
109
|
+
};
|
|
69
110
|
|
|
70
111
|
componentDidMount() {
|
|
71
|
-
window.addEventListener("mousedown", this.handleMouseDown);
|
|
72
|
-
window.addEventListener("mouseup", this.handleMouseUp);
|
|
73
|
-
|
|
74
112
|
// Ideally, we would be able to pass an initial value directly into
|
|
75
113
|
// the constructor
|
|
76
114
|
this.mathField()?.latex(this.props.value);
|
|
77
115
|
}
|
|
78
116
|
|
|
79
|
-
|
|
80
|
-
window.removeEventListener("mousedown", this.handleMouseDown);
|
|
81
|
-
window.removeEventListener("mouseup", this.handleMouseUp);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// handlers:
|
|
85
|
-
// keep track of two related bits of state:
|
|
86
|
-
// * this.state.focused - whether the buttons are currently shown
|
|
87
|
-
// * this.mouseDown - whether a mouse click is active that started in the
|
|
88
|
-
// buttons div
|
|
89
|
-
|
|
90
|
-
handleFocus: () => void = () => {
|
|
91
|
-
this.setState({focused: true});
|
|
92
|
-
// TODO(joel) fix properly - we should probably allow onFocus handlers
|
|
93
|
-
// to this property, but we need to work correctly with them.
|
|
94
|
-
// if (this.props.onFocus) {
|
|
95
|
-
// this.props.onFocus();
|
|
96
|
-
// }
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
handleMouseDown: (arg1: MouseEvent) => void = (event) => {
|
|
100
|
-
// @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2345 - Argument of type 'EventTarget | null' is not assignable to parameter of type 'Node | null'.
|
|
101
|
-
const focused = ReactDOM.findDOMNode(this).contains(event.target);
|
|
102
|
-
this.mouseDown = focused;
|
|
103
|
-
if (!focused) {
|
|
104
|
-
this.setState({focused: false});
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
handleMouseUp: (arg1: MouseEvent) => void = () => {
|
|
109
|
-
// this mouse click started in the buttons div so we should focus the
|
|
110
|
-
// input
|
|
111
|
-
if (this.mouseDown) {
|
|
112
|
-
this.focus();
|
|
113
|
-
}
|
|
114
|
-
this.mouseDown = false;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
handleBlur: (arg1: React.FocusEvent) => void = (e) => {
|
|
118
|
-
// TODO(michaelpolyak): Consider trapping focus within the button group.
|
|
119
|
-
// Focusing back on the input when TAB out of the last button in the
|
|
120
|
-
// group. This will probably require ESCAPE key handling to enable to
|
|
121
|
-
// close (blur) the button group in order to focus on next page element.
|
|
122
|
-
if (
|
|
123
|
-
!this.mouseDown &&
|
|
124
|
-
// @ts-expect-error - TS2531 - Object is possibly 'null'.
|
|
125
|
-
!ReactDOM.findDOMNode(this).contains(e.relatedTarget)
|
|
126
|
-
) {
|
|
127
|
-
this.setState({focused: false});
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
_shouldShowButtons: () => boolean = () => {
|
|
132
|
-
if (this.props.buttonsVisible === "always") {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
117
|
+
openKeypad() {
|
|
135
118
|
if (this.props.buttonsVisible === "never") {
|
|
136
|
-
return
|
|
119
|
+
return;
|
|
137
120
|
}
|
|
138
|
-
|
|
139
|
-
}
|
|
121
|
+
this.setState({keypadOpen: true});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
closeKeypad() {
|
|
125
|
+
this.setState({keypadOpen: false});
|
|
126
|
+
}
|
|
140
127
|
|
|
141
128
|
insert: (value: any) => void = (value) => {
|
|
142
129
|
const input = this.mathField();
|
|
@@ -206,6 +193,9 @@ class MathInput extends React.Component<Props, State> {
|
|
|
206
193
|
if (this.props.value !== value) {
|
|
207
194
|
this.props.onChange(value);
|
|
208
195
|
}
|
|
196
|
+
this.setState({
|
|
197
|
+
cursorContext: getCursorContext(mathField),
|
|
198
|
+
});
|
|
209
199
|
},
|
|
210
200
|
enter: () => {
|
|
211
201
|
// NOTE(kevinb): This isn't how answers to exercises are
|
|
@@ -239,9 +229,31 @@ class MathInput extends React.Component<Props, State> {
|
|
|
239
229
|
this.setState({focused: true});
|
|
240
230
|
};
|
|
241
231
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
232
|
+
// removing mathfield focus here makes the cursor vanished when the
|
|
233
|
+
// input is still focused
|
|
234
|
+
blur: () => void = () => this.setState({focused: false});
|
|
235
|
+
|
|
236
|
+
handleKeypadPress: (key: Keys, e: any) => void = (key, e) => {
|
|
237
|
+
const translator = keyTranslator[key];
|
|
238
|
+
const mathField = this.mathField();
|
|
239
|
+
|
|
240
|
+
if (mathField) {
|
|
241
|
+
if (translator) {
|
|
242
|
+
translator(mathField, key);
|
|
243
|
+
}
|
|
244
|
+
this.setState({
|
|
245
|
+
cursorContext: getCursorContext(mathField),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// We want to prevent taking focus from input when clicking on keypad
|
|
250
|
+
// Clickable handles "onClick" differently than react;
|
|
251
|
+
// a keyboard event is "keydown" type.
|
|
252
|
+
// In react without WonderBlocks, "enter" or "space" keydown events
|
|
253
|
+
// are also "click" events, differentiated by "detail".
|
|
254
|
+
if (e.type === "click") {
|
|
255
|
+
this.focus();
|
|
256
|
+
}
|
|
245
257
|
};
|
|
246
258
|
|
|
247
259
|
render(): React.ReactNode {
|
|
@@ -258,36 +270,202 @@ class MathInput extends React.Component<Props, State> {
|
|
|
258
270
|
className = className + " " + this.props.className;
|
|
259
271
|
}
|
|
260
272
|
|
|
261
|
-
let buttons = null;
|
|
262
|
-
if (this._shouldShowButtons()) {
|
|
263
|
-
// @ts-expect-error - TS2322 - Type 'Element' is not assignable to type 'null'.
|
|
264
|
-
buttons = (
|
|
265
|
-
<TexButtons
|
|
266
|
-
sets={this.props.buttonSets}
|
|
267
|
-
className="math-input-buttons absolute"
|
|
268
|
-
convertDotToTimes={this.props.convertDotToTimes}
|
|
269
|
-
onInsert={this.insert}
|
|
270
|
-
/>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
273
|
return (
|
|
275
|
-
<
|
|
276
|
-
|
|
274
|
+
<View
|
|
275
|
+
style={[
|
|
276
|
+
styles.outerWrapper,
|
|
277
|
+
this.state.focused && styles.wrapperFocused,
|
|
278
|
+
this.props.hasError && styles.wrapperError,
|
|
279
|
+
]}
|
|
280
|
+
>
|
|
281
|
+
<div
|
|
282
|
+
style={{
|
|
283
|
+
display: "flex",
|
|
284
|
+
padding: 1,
|
|
285
|
+
}}
|
|
286
|
+
onClick={() => {
|
|
287
|
+
const mathField = this.mathField();
|
|
288
|
+
if (!mathField) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.setState({
|
|
292
|
+
cursorContext: getCursorContext(mathField),
|
|
293
|
+
});
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
277
296
|
<span
|
|
278
297
|
className={className}
|
|
279
298
|
ref={(ref) => (this.__mathFieldWrapperRef = ref)}
|
|
280
299
|
aria-label={this.props.labelText}
|
|
281
|
-
onFocus={this.
|
|
282
|
-
onBlur={this.
|
|
300
|
+
onFocus={() => this.focus()}
|
|
301
|
+
onBlur={() => this.blur()}
|
|
283
302
|
/>
|
|
303
|
+
<Popover
|
|
304
|
+
opened={this.state.keypadOpen}
|
|
305
|
+
onClose={() => this.closeKeypad()}
|
|
306
|
+
dismissEnabled
|
|
307
|
+
content={() => (
|
|
308
|
+
<PopoverContentCore
|
|
309
|
+
closeButtonVisible
|
|
310
|
+
style={styles.popoverContent}
|
|
311
|
+
>
|
|
312
|
+
<DesktopKeypad
|
|
313
|
+
onAnalyticsEvent={
|
|
314
|
+
this.props.analytics.onAnalyticsEvent
|
|
315
|
+
}
|
|
316
|
+
extraKeys={this.props.extraKeys}
|
|
317
|
+
onClickKey={this.handleKeypadPress}
|
|
318
|
+
cursorContext={this.state.cursorContext}
|
|
319
|
+
convertDotToTimes={
|
|
320
|
+
this.props.convertDotToTimes
|
|
321
|
+
}
|
|
322
|
+
{...(this.props.keypadButtonSets ??
|
|
323
|
+
mapButtonSets(this.props?.buttonSets))}
|
|
324
|
+
/>
|
|
325
|
+
</PopoverContentCore>
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
{this.props.buttonsVisible === "never" ? (
|
|
329
|
+
<MathInputIcon
|
|
330
|
+
hovered={false}
|
|
331
|
+
focused={false}
|
|
332
|
+
active={false}
|
|
333
|
+
/>
|
|
334
|
+
) : (
|
|
335
|
+
<Clickable
|
|
336
|
+
aria-label={
|
|
337
|
+
this.state.keypadOpen
|
|
338
|
+
? i18n._("close math keypad")
|
|
339
|
+
: i18n._("open math keypad")
|
|
340
|
+
}
|
|
341
|
+
aria-checked={this.state.keypadOpen}
|
|
342
|
+
role="switch"
|
|
343
|
+
onClick={() =>
|
|
344
|
+
this.state.keypadOpen
|
|
345
|
+
? this.closeKeypad()
|
|
346
|
+
: this.openKeypad()
|
|
347
|
+
}
|
|
348
|
+
>
|
|
349
|
+
{(props) => (
|
|
350
|
+
<MathInputIcon
|
|
351
|
+
active={this.state.keypadOpen}
|
|
352
|
+
{...props}
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</Clickable>
|
|
356
|
+
)}
|
|
357
|
+
</Popover>
|
|
284
358
|
</div>
|
|
285
|
-
|
|
286
|
-
{buttons}
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
359
|
+
</View>
|
|
289
360
|
);
|
|
290
361
|
}
|
|
291
362
|
}
|
|
292
363
|
|
|
364
|
+
const MathInputIcon = ({hovered, focused, active}) => {
|
|
365
|
+
let color: string | undefined;
|
|
366
|
+
switch (true) {
|
|
367
|
+
case focused || active:
|
|
368
|
+
color = Color.white;
|
|
369
|
+
break;
|
|
370
|
+
case hovered:
|
|
371
|
+
color = Color.blue;
|
|
372
|
+
break;
|
|
373
|
+
default:
|
|
374
|
+
color = Color.offBlack;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
const dynamicClass =
|
|
378
|
+
active || focused ? styles.iconActive : styles.iconInactive;
|
|
379
|
+
return (
|
|
380
|
+
<View style={[styles.iconContainer, dynamicClass]}>
|
|
381
|
+
<svg
|
|
382
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
383
|
+
width="16"
|
|
384
|
+
height="16"
|
|
385
|
+
fill={color}
|
|
386
|
+
viewBox="0 0 256 256"
|
|
387
|
+
>
|
|
388
|
+
<path d="M112,72a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16h64A8,8,0,0,1,112,72Zm-8,104H80V152a8,8,0,0,0-16,0v24H40a8,8,0,0,0,0,16H64v24a8,8,0,0,0,16,0V192h24a8,8,0,0,0,0-16Zm48,0h64a8,8,0,0,0,0-16H152a8,8,0,0,0,0,16Zm64,16H152a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16Zm-61.66-90.34a8,8,0,0,0,11.32,0L184,83.31l18.34,18.35a8,8,0,0,0,11.32-11.32L195.31,72l18.35-18.34a8,8,0,0,0-11.32-11.32L184,60.69,165.66,42.34a8,8,0,0,0-11.32,11.32L172.69,72,154.34,90.34A8,8,0,0,0,154.34,101.66Z" />
|
|
389
|
+
</svg>
|
|
390
|
+
</View>
|
|
391
|
+
);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const mapButtonSets = (buttonSets?: LegacyButtonSets) => {
|
|
395
|
+
const keypadButtonSets: KeypadButtonSets = {};
|
|
396
|
+
if (!buttonSets) {
|
|
397
|
+
return keypadButtonSets;
|
|
398
|
+
}
|
|
399
|
+
buttonSets.forEach((buttonSet) => {
|
|
400
|
+
switch (buttonSet) {
|
|
401
|
+
case "advanced relations":
|
|
402
|
+
keypadButtonSets.advancedRelations = true;
|
|
403
|
+
break;
|
|
404
|
+
case "basic relations":
|
|
405
|
+
keypadButtonSets.basicRelations = true;
|
|
406
|
+
break;
|
|
407
|
+
case "basic+div":
|
|
408
|
+
keypadButtonSets.divisionKey = true;
|
|
409
|
+
break;
|
|
410
|
+
case "logarithms":
|
|
411
|
+
keypadButtonSets.logarithms = true;
|
|
412
|
+
break;
|
|
413
|
+
case "prealgebra":
|
|
414
|
+
keypadButtonSets.preAlgebra = true;
|
|
415
|
+
break;
|
|
416
|
+
case "trig":
|
|
417
|
+
keypadButtonSets.trigonometry = true;
|
|
418
|
+
break;
|
|
419
|
+
case "basic":
|
|
420
|
+
default:
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
return keypadButtonSets;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const inputFocused = {
|
|
428
|
+
borderWidth: 2,
|
|
429
|
+
borderColor: Color.blue,
|
|
430
|
+
margin: -1,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const styles = StyleSheet.create({
|
|
434
|
+
iconContainer: {
|
|
435
|
+
display: "flex",
|
|
436
|
+
justifyContent: "center",
|
|
437
|
+
height: "100%",
|
|
438
|
+
padding: Spacing.xxxSmall_4,
|
|
439
|
+
borderRadius: 1,
|
|
440
|
+
},
|
|
441
|
+
iconInactive: {
|
|
442
|
+
backgroundColor: Color.offBlack8,
|
|
443
|
+
},
|
|
444
|
+
iconActive: {
|
|
445
|
+
backgroundColor: Color.offBlack64,
|
|
446
|
+
},
|
|
447
|
+
outerWrapper: {
|
|
448
|
+
display: "inline-block",
|
|
449
|
+
borderStyle: "solid",
|
|
450
|
+
borderWidth: 1,
|
|
451
|
+
borderColor: Color.offBlack50,
|
|
452
|
+
borderRadius: 3,
|
|
453
|
+
background: Color.white,
|
|
454
|
+
":hover": inputFocused,
|
|
455
|
+
},
|
|
456
|
+
wrapperFocused: inputFocused,
|
|
457
|
+
wrapperError: {
|
|
458
|
+
borderColor: Color.red,
|
|
459
|
+
background: fade(Color.red, 0.08),
|
|
460
|
+
":hover": {
|
|
461
|
+
borderColor: Color.red,
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
popoverContent: {
|
|
465
|
+
padding: 0,
|
|
466
|
+
paddingBottom: Spacing.xxSmall_6,
|
|
467
|
+
maxWidth: "initial",
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
293
471
|
export default MathInput;
|
|
@@ -6,6 +6,7 @@ import _ from "underscore";
|
|
|
6
6
|
|
|
7
7
|
import {getDependencies} from "../dependencies";
|
|
8
8
|
|
|
9
|
+
import type {LegacyButtonSets} from "../perseus-types";
|
|
9
10
|
import type {Keys} from "@khanacademy/math-input";
|
|
10
11
|
|
|
11
12
|
const prettyBig = {fontSize: "150%"} as const;
|
|
@@ -22,7 +23,7 @@ type ButtonSet = (props: any) => [
|
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
type ButtonSets = {
|
|
25
|
-
readonly [key
|
|
26
|
+
readonly [key in LegacyButtonSets[number]]: ReadonlyArray<ButtonSet>;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
// These are functions because we want to generate a new component for each use
|
|
@@ -285,7 +286,6 @@ class TexButtons extends React.Component<Props> {
|
|
|
285
286
|
// for _.keys() to return the keys in an arbitrary order, but in
|
|
286
287
|
// practice, they will be ordered as listed above.
|
|
287
288
|
const sortedButtonSets = _.sortBy(this.props.sets, (setName) =>
|
|
288
|
-
// @ts-expect-error - TS2345 - Argument of type 'string | number' is not assignable to parameter of type 'string'.
|
|
289
289
|
_.keys(buttonSets).indexOf(setName),
|
|
290
290
|
);
|
|
291
291
|
|
package/src/perseus-types.ts
CHANGED
|
@@ -311,11 +311,21 @@ export type PerseusExplanationWidgetOptions = {
|
|
|
311
311
|
static: boolean;
|
|
312
312
|
};
|
|
313
313
|
|
|
314
|
+
export type LegacyButtonSets = ReadonlyArray<
|
|
315
|
+
| "basic"
|
|
316
|
+
| "basic+div"
|
|
317
|
+
| "trig"
|
|
318
|
+
| "prealgebra"
|
|
319
|
+
| "logarithms"
|
|
320
|
+
| "basic relations"
|
|
321
|
+
| "advanced relations"
|
|
322
|
+
>;
|
|
323
|
+
|
|
314
324
|
export type PerseusExpressionWidgetOptions = {
|
|
315
325
|
// The expression forms the answer may come in
|
|
316
326
|
answerForms: ReadonlyArray<PerseusExpressionAnswerForm>;
|
|
317
327
|
// Different buttons sets that can show in the expression. Options are "basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations"
|
|
318
|
-
buttonSets:
|
|
328
|
+
buttonSets: LegacyButtonSets;
|
|
319
329
|
// Variables that can be used as functions. Default: ["f", "g", "h"]
|
|
320
330
|
functions: ReadonlyArray<string>;
|
|
321
331
|
// Use x for rendering multiplication instead of a center dot.
|
|
@@ -598,8 +598,7 @@
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
.perseus-math-input.mq-editable-field.mq-math-mode {
|
|
601
|
-
|
|
602
|
-
background: #fff;
|
|
601
|
+
background-color: transparent;
|
|
603
602
|
|
|
604
603
|
// If the font-size is too small, super/subscripts become hard to input
|
|
605
604
|
font-size: 18px;
|
|
@@ -607,11 +606,11 @@
|
|
|
607
606
|
// Make the click target easier to hit
|
|
608
607
|
min-width: 100px;
|
|
609
608
|
|
|
610
|
-
|
|
611
|
-
// border-color: rgb(78, 187, 212);
|
|
612
|
-
border-color: #a4a4a4;
|
|
609
|
+
border: unset;
|
|
613
610
|
|
|
614
|
-
|
|
611
|
+
&.mq-focused {
|
|
612
|
+
box-shadow: unset;
|
|
613
|
+
}
|
|
615
614
|
|
|
616
615
|
& > .mq-root-block {
|
|
617
616
|
// A bit of extra whitespace here greatly improves legibility
|
|
@@ -10,7 +10,7 @@ import expressionExport from "../expression";
|
|
|
10
10
|
|
|
11
11
|
import TestKeypadContextWrapper from "./test-keypad-context-wrapper";
|
|
12
12
|
|
|
13
|
-
import type {PerseusItem} from "../../perseus-types";
|
|
13
|
+
import type {LegacyButtonSets, PerseusItem} from "../../perseus-types";
|
|
14
14
|
import type {Keys as Key} from "@khanacademy/math-input";
|
|
15
15
|
|
|
16
16
|
type StoryArgs = {
|
|
@@ -69,12 +69,12 @@ export const DesktopKitchenSink = (args: StoryArgs): React.ReactElement => {
|
|
|
69
69
|
"logarithms",
|
|
70
70
|
"basic relations",
|
|
71
71
|
"advanced relations",
|
|
72
|
-
],
|
|
72
|
+
] as LegacyButtonSets,
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const keypadConfiguration = {
|
|
76
76
|
keypadType: KeypadType.EXPRESSION,
|
|
77
|
-
extraKeys: ["x", "y", "z"] as Key
|
|
77
|
+
extraKeys: ["x", "y", "z"] as Array<Key>,
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
return (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {it, describe, beforeEach} from "@jest/globals";
|
|
2
|
-
import {screen
|
|
2
|
+
import {screen} from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
4
|
import "@testing-library/jest-dom";
|
|
5
5
|
|
|
@@ -428,93 +428,20 @@ describe("error tooltip", () => {
|
|
|
428
428
|
);
|
|
429
429
|
});
|
|
430
430
|
|
|
431
|
-
it("shows
|
|
431
|
+
it("shows error text in tooltip", async () => {
|
|
432
432
|
// Arrange
|
|
433
433
|
const {renderer} = renderQuestion(expressionItem2.question);
|
|
434
434
|
const expression = renderer.findWidgets("expression 1")[0];
|
|
435
435
|
|
|
436
436
|
// Act
|
|
437
437
|
expression.insert("x&&&&&^1");
|
|
438
|
+
screen.getByRole("textbox").blur();
|
|
438
439
|
renderer.guessAndScore();
|
|
439
|
-
jest.runOnlyPendingTimers();
|
|
440
440
|
|
|
441
441
|
// Assert
|
|
442
|
-
|
|
443
|
-
"Sorry, I don't understand that!",
|
|
444
|
-
);
|
|
445
|
-
expect(errorMessage).not.toBeUndefined();
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it("shows error text on mouse over", async () => {
|
|
449
|
-
// Arrange
|
|
450
|
-
const {renderer} = renderQuestion(expressionItem2.question);
|
|
451
|
-
const expression = renderer.findWidgets("expression 1")[0];
|
|
452
|
-
|
|
453
|
-
// Act
|
|
454
|
-
expression.insert("x&&&&&^1");
|
|
455
|
-
renderer.guessAndScore();
|
|
456
|
-
jest.runOnlyPendingTimers();
|
|
457
|
-
|
|
458
|
-
const tooltip = await screen.findByTestId("test-error-icon");
|
|
459
|
-
|
|
460
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
461
|
-
fireEvent.mouseEnter(tooltip);
|
|
462
|
-
|
|
463
|
-
// Assert
|
|
464
|
-
expect(
|
|
465
|
-
screen.getByText("Sorry, I don't understand that!"),
|
|
466
|
-
).toBeVisible();
|
|
467
|
-
});
|
|
468
|
-
it("hides error text on mouse leave", async () => {
|
|
469
|
-
// Arrange
|
|
470
|
-
const {renderer} = renderQuestion(expressionItem2.question);
|
|
471
|
-
const expression = renderer.findWidgets("expression 1")[0];
|
|
472
|
-
|
|
473
|
-
expression.insert("x&&&&&^1");
|
|
474
|
-
renderer.guessAndScore();
|
|
475
|
-
jest.runOnlyPendingTimers();
|
|
476
|
-
|
|
477
|
-
const tooltip = await screen.findByTestId("test-error-icon");
|
|
478
|
-
|
|
479
|
-
// Act
|
|
480
|
-
// NOTE(Nicole): The existing perseus code requires explicit events
|
|
481
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
482
|
-
fireEvent.mouseEnter(tooltip);
|
|
483
|
-
// NOTE(Nicole): The existing perseus code requires explicit events
|
|
484
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
485
|
-
fireEvent.mouseLeave(tooltip);
|
|
486
|
-
|
|
487
|
-
// Assert
|
|
488
|
-
expect(
|
|
489
|
-
screen.getByText("Sorry, I don't understand that!"),
|
|
490
|
-
).not.toBeVisible();
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it("toggles error text on click", async () => {
|
|
494
|
-
// Arrange
|
|
495
|
-
const {renderer} = renderQuestion(expressionItem2.question);
|
|
496
|
-
const expression = renderer.findWidgets("expression 1")[0];
|
|
497
|
-
|
|
498
|
-
expression.insert("x&&&&&^1");
|
|
499
|
-
renderer.guessAndScore();
|
|
500
|
-
jest.runOnlyPendingTimers();
|
|
501
|
-
|
|
502
|
-
const tooltip = await screen.findByTestId("test-error-icon");
|
|
503
|
-
|
|
504
|
-
// Act & Assert
|
|
505
|
-
// NOTE(Nicole): The existing perseus code requires explicit events
|
|
506
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
507
|
-
fireEvent.click(tooltip);
|
|
442
|
+
expect(screen.getByText("Oops!")).toBeVisible();
|
|
508
443
|
expect(
|
|
509
444
|
screen.getByText("Sorry, I don't understand that!"),
|
|
510
445
|
).toBeVisible();
|
|
511
|
-
|
|
512
|
-
// Act & Assert
|
|
513
|
-
// NOTE(Nicole): The existing perseus code requires explicit events
|
|
514
|
-
// eslint-disable-next-line testing-library/prefer-user-event
|
|
515
|
-
fireEvent.click(tooltip);
|
|
516
|
-
expect(
|
|
517
|
-
screen.getByText("Sorry, I don't understand that!"),
|
|
518
|
-
).not.toBeVisible();
|
|
519
446
|
});
|
|
520
447
|
});
|