@khanacademy/math-input 16.2.0 → 16.4.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 +12 -0
- package/dist/components/input/math-input.d.ts +2 -2
- package/dist/es/index.js +96 -54
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +121 -75
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +12 -0
- package/package.json +1 -1
- package/src/components/input/math-input.tsx +71 -40
- package/src/components/keypad/__tests__/keypad.test.tsx +37 -0
- package/src/components/keypad/shared-keys.tsx +6 -1
- package/src/index.ts +1 -0
- package/src/utils.test.ts +33 -0
- package/src/utils.ts +45 -1
- package/tsconfig-build.tsbuildinfo +1 -1
package/dist/utils.d.ts
CHANGED
|
@@ -3,3 +3,15 @@ export declare const DecimalSeparator: {
|
|
|
3
3
|
readonly PERIOD: ".";
|
|
4
4
|
};
|
|
5
5
|
export declare const decimalSeparator: string;
|
|
6
|
+
/**
|
|
7
|
+
* convertDotToTimes (aka `times`) is an option the content creators have to
|
|
8
|
+
* use × (TIMES) rather than · (CDOT) for multiplication (for younger learners).
|
|
9
|
+
* Some locales _only_ use one or the other for all multiplication regardless
|
|
10
|
+
* of age.
|
|
11
|
+
*
|
|
12
|
+
* convertDotToTimesByLocale overrides convertDotToTimes for those locales.
|
|
13
|
+
*
|
|
14
|
+
* @param {boolean} convertDotToTimes - the setting set by content creators
|
|
15
|
+
* @returns {boolean} - true to convert to × (TIMES), false to use · (CDOT)
|
|
16
|
+
*/
|
|
17
|
+
export declare function convertDotToTimesByLocale(convertDotToTimes: boolean): boolean;
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import * as React from "react";
|
|
|
7
7
|
import ReactDOM from "react-dom";
|
|
8
8
|
|
|
9
9
|
import {View} from "../../fake-react-native-web/index";
|
|
10
|
+
import {KeypadContext} from "../keypad-context";
|
|
10
11
|
|
|
11
12
|
import CursorHandle from "./cursor-handle";
|
|
12
13
|
import {
|
|
@@ -217,6 +218,7 @@ class MathInput extends React.Component<Props, State> {
|
|
|
217
218
|
this.props.keypadElement.getDOMNode()
|
|
218
219
|
) {
|
|
219
220
|
const [x, y] = [evt.clientX, evt.clientY];
|
|
221
|
+
|
|
220
222
|
// We only want to blur if the click is above the keypad,
|
|
221
223
|
// to the left of the keypad, or to the right of the keypad.
|
|
222
224
|
// The reasoning for not blurring for any clicks below the keypad is
|
|
@@ -604,9 +606,11 @@ class MathInput extends React.Component<Props, State> {
|
|
|
604
606
|
});
|
|
605
607
|
};
|
|
606
608
|
|
|
607
|
-
handleTouchStart
|
|
608
|
-
e
|
|
609
|
-
|
|
609
|
+
handleTouchStart = (
|
|
610
|
+
e: React.TouchEvent<HTMLDivElement>,
|
|
611
|
+
keypadActive: boolean,
|
|
612
|
+
setKeypadActive: (keypadActive: boolean) => void,
|
|
613
|
+
): void => {
|
|
610
614
|
e.stopPropagation();
|
|
611
615
|
|
|
612
616
|
// Hide the cursor handle on touch start, if the handle itself isn't
|
|
@@ -625,6 +629,11 @@ class MathInput extends React.Component<Props, State> {
|
|
|
625
629
|
this._insertCursorAtClosestNode(touch.clientX, touch.clientY);
|
|
626
630
|
}
|
|
627
631
|
|
|
632
|
+
// If we're already focused, but the keypad isn't active, activate it.
|
|
633
|
+
if (this.state.focused && !keypadActive) {
|
|
634
|
+
setKeypadActive(true);
|
|
635
|
+
}
|
|
636
|
+
|
|
628
637
|
// Trigger a focus event, if we're not already focused.
|
|
629
638
|
if (!this.state.focused) {
|
|
630
639
|
this.focus();
|
|
@@ -634,7 +643,11 @@ class MathInput extends React.Component<Props, State> {
|
|
|
634
643
|
// We want to allow the user to be able to focus the input via click
|
|
635
644
|
// when using ChromeOS third-party browsers that use mobile user agents,
|
|
636
645
|
// but don't actually simulate touch events.
|
|
637
|
-
handleClick = (
|
|
646
|
+
handleClick = (
|
|
647
|
+
e: React.MouseEvent<HTMLDivElement>,
|
|
648
|
+
keypadActive: boolean,
|
|
649
|
+
setKeypadActive: (keypadActive: boolean) => void,
|
|
650
|
+
): void => {
|
|
638
651
|
e.stopPropagation();
|
|
639
652
|
|
|
640
653
|
// Hide the cursor handle on click
|
|
@@ -651,6 +664,11 @@ class MathInput extends React.Component<Props, State> {
|
|
|
651
664
|
this._insertCursorAtClosestNode(e.clientX, e.clientY);
|
|
652
665
|
}
|
|
653
666
|
|
|
667
|
+
// If we're already focused, but the keypad isn't active, activate it.
|
|
668
|
+
if (this.state.focused && !keypadActive) {
|
|
669
|
+
setKeypadActive(true);
|
|
670
|
+
}
|
|
671
|
+
|
|
654
672
|
// Trigger a focus event, if we're not already focused.
|
|
655
673
|
if (!this.state.focused) {
|
|
656
674
|
this.focus();
|
|
@@ -926,46 +944,59 @@ class MathInput extends React.Component<Props, State> {
|
|
|
926
944
|
i18n._("Tap with one or two fingers to open keyboard");
|
|
927
945
|
|
|
928
946
|
return (
|
|
929
|
-
<
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
947
|
+
<KeypadContext.Consumer>
|
|
948
|
+
{({keypadActive, setKeypadActive}) => (
|
|
949
|
+
<View
|
|
950
|
+
style={styles.input}
|
|
951
|
+
onTouchStart={(e: React.TouchEvent<HTMLDivElement>) => {
|
|
952
|
+
this.handleTouchStart(
|
|
953
|
+
e,
|
|
954
|
+
keypadActive,
|
|
955
|
+
setKeypadActive,
|
|
956
|
+
);
|
|
957
|
+
}}
|
|
958
|
+
onTouchMove={this.handleTouchMove}
|
|
959
|
+
onTouchEnd={this.handleTouchEnd}
|
|
960
|
+
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
|
961
|
+
this.handleClick(e, keypadActive, setKeypadActive);
|
|
962
|
+
}}
|
|
963
|
+
role={"textbox"}
|
|
964
|
+
ariaLabel={ariaLabel}
|
|
965
|
+
>
|
|
966
|
+
{/* NOTE(charlie): This is used purely to namespace the styles in
|
|
939
967
|
overrides.css. */}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
968
|
+
<div
|
|
969
|
+
className="keypad-input"
|
|
970
|
+
// @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
|
|
971
|
+
tabIndex={"0"}
|
|
972
|
+
ref={(node) => {
|
|
973
|
+
this.inputRef = node;
|
|
974
|
+
}}
|
|
975
|
+
onKeyUp={this.handleKeyUp}
|
|
976
|
+
>
|
|
977
|
+
{/* NOTE(charlie): This element must be styled with inline
|
|
950
978
|
styles rather than with Aphrodite classes, as MathQuill
|
|
951
979
|
modifies the class names on the DOM node. */}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
980
|
+
<div
|
|
981
|
+
ref={(node) => {
|
|
982
|
+
this._mathContainer =
|
|
983
|
+
ReactDOM.findDOMNode(node);
|
|
984
|
+
}}
|
|
985
|
+
style={innerStyle}
|
|
986
|
+
/>
|
|
987
|
+
</div>
|
|
988
|
+
{focused && handle.visible && (
|
|
989
|
+
<CursorHandle
|
|
990
|
+
{...handle}
|
|
991
|
+
onTouchStart={this.onCursorHandleTouchStart}
|
|
992
|
+
onTouchMove={this.onCursorHandleTouchMove}
|
|
993
|
+
onTouchEnd={this.onCursorHandleTouchEnd}
|
|
994
|
+
onTouchCancel={this.onCursorHandleTouchCancel}
|
|
995
|
+
/>
|
|
996
|
+
)}
|
|
997
|
+
</View>
|
|
967
998
|
)}
|
|
968
|
-
</
|
|
999
|
+
</KeypadContext.Consumer>
|
|
969
1000
|
);
|
|
970
1001
|
}
|
|
971
1002
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as wbi18n from "@khanacademy/wonder-blocks-i18n";
|
|
1
2
|
import {render, screen} from "@testing-library/react";
|
|
2
3
|
import userEvent from "@testing-library/user-event";
|
|
3
4
|
import * as React from "react";
|
|
@@ -155,6 +156,42 @@ describe("keypad", () => {
|
|
|
155
156
|
expect(screen.getByTestId("TIMES")).toBeInTheDocument();
|
|
156
157
|
});
|
|
157
158
|
|
|
159
|
+
it(`forces CDOT in locales that require it`, () => {
|
|
160
|
+
// Arrange
|
|
161
|
+
jest.spyOn(wbi18n, "getLocale").mockReturnValue("az");
|
|
162
|
+
|
|
163
|
+
// Act
|
|
164
|
+
render(
|
|
165
|
+
<Keypad
|
|
166
|
+
onClickKey={() => {}}
|
|
167
|
+
convertDotToTimes={true}
|
|
168
|
+
onAnalyticsEvent={async () => {}}
|
|
169
|
+
/>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Assert
|
|
173
|
+
expect(screen.getByTestId("CDOT")).toBeInTheDocument();
|
|
174
|
+
expect(screen.queryByTestId("TIMES")).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it(`forces TIMES in locales that require it`, () => {
|
|
178
|
+
// Arrange
|
|
179
|
+
jest.spyOn(wbi18n, "getLocale").mockReturnValue("fr");
|
|
180
|
+
|
|
181
|
+
// Act
|
|
182
|
+
render(
|
|
183
|
+
<Keypad
|
|
184
|
+
onClickKey={() => {}}
|
|
185
|
+
convertDotToTimes={false}
|
|
186
|
+
onAnalyticsEvent={async () => {}}
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(screen.getByTestId("TIMES")).toBeInTheDocument();
|
|
192
|
+
expect(screen.queryByTestId("CDOT")).not.toBeInTheDocument();
|
|
193
|
+
});
|
|
194
|
+
|
|
158
195
|
it(`hides the tabs if providing the Fraction Keypad`, () => {
|
|
159
196
|
// Arrange
|
|
160
197
|
// Act
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
import Keys from "../../data/key-configs";
|
|
4
|
+
import {convertDotToTimesByLocale} from "../../utils";
|
|
4
5
|
|
|
5
6
|
import {KeypadButton} from "./keypad-button";
|
|
6
7
|
import {getCursorContextConfig} from "./utils";
|
|
@@ -56,7 +57,11 @@ export default function SharedKeys(props: Props) {
|
|
|
56
57
|
|
|
57
58
|
{/* Row 2 */}
|
|
58
59
|
<KeypadButton
|
|
59
|
-
keyConfig={
|
|
60
|
+
keyConfig={
|
|
61
|
+
convertDotToTimesByLocale(!!convertDotToTimes)
|
|
62
|
+
? Keys.TIMES
|
|
63
|
+
: Keys.CDOT
|
|
64
|
+
}
|
|
60
65
|
onClickKey={onClickKey}
|
|
61
66
|
coord={[4, 1]}
|
|
62
67
|
secondary
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
// External API of the "Provided" keypad component
|
|
41
41
|
export {keypadElementPropType} from "./components/prop-types";
|
|
42
42
|
export type {KeypadAPI, KeypadConfiguration} from "./types";
|
|
43
|
+
export {convertDotToTimesByLocale} from "./utils";
|
|
43
44
|
|
|
44
45
|
// Key list, configuration map, and types
|
|
45
46
|
export type {default as Keys} from "./data/keys";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as wbi18n from "@khanacademy/wonder-blocks-i18n";
|
|
2
|
+
|
|
3
|
+
import {convertDotToTimesByLocale} from "./utils";
|
|
4
|
+
|
|
5
|
+
describe("utils", () => {
|
|
6
|
+
describe("multiplicationSymbol", () => {
|
|
7
|
+
it("passes through convertDotToTimes in locales that don't override it", () => {
|
|
8
|
+
jest.spyOn(wbi18n, "getLocale").mockReturnValue("en");
|
|
9
|
+
|
|
10
|
+
const result1 = convertDotToTimesByLocale(true);
|
|
11
|
+
const result2 = convertDotToTimesByLocale(false);
|
|
12
|
+
|
|
13
|
+
expect(result1).toBe(true);
|
|
14
|
+
expect(result2).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("overrides with false for locales that only use dot", () => {
|
|
18
|
+
jest.spyOn(wbi18n, "getLocale").mockReturnValue("az");
|
|
19
|
+
|
|
20
|
+
const result = convertDotToTimesByLocale(true);
|
|
21
|
+
|
|
22
|
+
expect(result).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("overrides with true for locales that only use x", () => {
|
|
26
|
+
jest.spyOn(wbi18n, "getLocale").mockReturnValue("fr");
|
|
27
|
+
|
|
28
|
+
const result = convertDotToTimesByLocale(false);
|
|
29
|
+
|
|
30
|
+
expect(result).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {getDecimalSeparator} from "@khanacademy/wonder-blocks-i18n";
|
|
1
|
+
import {getDecimalSeparator, getLocale} from "@khanacademy/wonder-blocks-i18n";
|
|
2
2
|
|
|
3
3
|
export const DecimalSeparator = {
|
|
4
4
|
COMMA: ",",
|
|
@@ -15,3 +15,47 @@ export const decimalSeparator: string =
|
|
|
15
15
|
getDecimalSeparator() === ","
|
|
16
16
|
? DecimalSeparator.COMMA
|
|
17
17
|
: DecimalSeparator.PERIOD;
|
|
18
|
+
|
|
19
|
+
const CDOT_ONLY = [
|
|
20
|
+
"az",
|
|
21
|
+
"cs",
|
|
22
|
+
"da",
|
|
23
|
+
"de",
|
|
24
|
+
"hu",
|
|
25
|
+
"hy",
|
|
26
|
+
"kk",
|
|
27
|
+
"ky",
|
|
28
|
+
"lt",
|
|
29
|
+
"lv",
|
|
30
|
+
"nb",
|
|
31
|
+
"sk",
|
|
32
|
+
"sr",
|
|
33
|
+
"sv",
|
|
34
|
+
"uz",
|
|
35
|
+
];
|
|
36
|
+
const TIMES_ONLY = ["fr", "tr", "pt-pt"];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* convertDotToTimes (aka `times`) is an option the content creators have to
|
|
40
|
+
* use × (TIMES) rather than · (CDOT) for multiplication (for younger learners).
|
|
41
|
+
* Some locales _only_ use one or the other for all multiplication regardless
|
|
42
|
+
* of age.
|
|
43
|
+
*
|
|
44
|
+
* convertDotToTimesByLocale overrides convertDotToTimes for those locales.
|
|
45
|
+
*
|
|
46
|
+
* @param {boolean} convertDotToTimes - the setting set by content creators
|
|
47
|
+
* @returns {boolean} - true to convert to × (TIMES), false to use · (CDOT)
|
|
48
|
+
*/
|
|
49
|
+
export function convertDotToTimesByLocale(convertDotToTimes: boolean): boolean {
|
|
50
|
+
const locale = getLocale();
|
|
51
|
+
|
|
52
|
+
if (CDOT_ONLY.includes(locale)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (TIMES_ONLY.includes(locale)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return convertDotToTimes;
|
|
61
|
+
}
|