@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/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
@@ -3,7 +3,7 @@
3
3
  "description": "Khan Academy's new expression editor for the mobile web.",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "16.2.0",
6
+ "version": "16.4.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -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: (arg1: React.TouchEvent<HTMLDivElement>) => void = (
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 = (e: React.MouseEvent<HTMLDivElement>): void => {
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
- <View
930
- style={styles.input}
931
- onTouchStart={this.handleTouchStart}
932
- onTouchMove={this.handleTouchMove}
933
- onTouchEnd={this.handleTouchEnd}
934
- onClick={this.handleClick}
935
- role={"textbox"}
936
- ariaLabel={ariaLabel}
937
- >
938
- {/* NOTE(charlie): This is used purely to namespace the styles in
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
- <div
941
- className="keypad-input"
942
- // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
943
- tabIndex={"0"}
944
- ref={(node) => {
945
- this.inputRef = node;
946
- }}
947
- onKeyUp={this.handleKeyUp}
948
- >
949
- {/* NOTE(charlie): This element must be styled with inline
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
- <div
953
- ref={(node) => {
954
- this._mathContainer = ReactDOM.findDOMNode(node);
955
- }}
956
- style={innerStyle}
957
- />
958
- </div>
959
- {focused && handle.visible && (
960
- <CursorHandle
961
- {...handle}
962
- onTouchStart={this.onCursorHandleTouchStart}
963
- onTouchMove={this.onCursorHandleTouchMove}
964
- onTouchEnd={this.onCursorHandleTouchEnd}
965
- onTouchCancel={this.onCursorHandleTouchCancel}
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
- </View>
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={convertDotToTimes ? Keys.TIMES : Keys.CDOT}
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
+ }