@khanacademy/math-input 16.1.2 → 16.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/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { default as DesktopKeypad } from "./components/keypad";
13
13
  export { KeypadContext, StatefulKeypadContextProvider, } from "./components/keypad-context";
14
14
  export { keypadElementPropType } from "./components/prop-types";
15
15
  export type { KeypadAPI, KeypadConfiguration } from "./types";
16
+ export { convertDotToTimesByLocale } from "./utils";
16
17
  export type { default as Keys } from "./data/keys";
17
18
  export { KeyArray } from "./data/keys";
18
19
  export { default as KeyConfigs } from "./data/key-configs";
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
48
48
 
49
49
  // This file is processed by a Rollup plugin (replace) to inject the production
50
50
  const libName = "@khanacademy/math-input";
51
- const libVersion = "16.1.2";
51
+ const libVersion = "16.3.0";
52
52
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
53
53
 
54
54
  function _extends() {
@@ -821,6 +821,30 @@ const DecimalSeparator = {
821
821
  // - Some languages/locales use different decimal separators than the ones
822
822
  // listed here. Much of the Arab world uses U+066C.
823
823
  const decimalSeparator = i18n.getDecimalSeparator() === "," ? DecimalSeparator.COMMA : DecimalSeparator.PERIOD;
824
+ const CDOT_ONLY = ["az", "cs", "da", "de", "hu", "hy", "kk", "ky", "lt", "lv", "nb", "sk", "sr", "sv", "uz"];
825
+ const TIMES_ONLY = ["fr", "tr", "pt-pt"];
826
+
827
+ /**
828
+ * convertDotToTimes (aka `times`) is an option the content creators have to
829
+ * use × (TIMES) rather than · (CDOT) for multiplication (for younger learners).
830
+ * Some locales _only_ use one or the other for all multiplication regardless
831
+ * of age.
832
+ *
833
+ * convertDotToTimesByLocale overrides convertDotToTimes for those locales.
834
+ *
835
+ * @param {boolean} convertDotToTimes - the setting set by content creators
836
+ * @returns {boolean} - true to convert to × (TIMES), false to use · (CDOT)
837
+ */
838
+ function convertDotToTimesByLocale(convertDotToTimes) {
839
+ const locale = i18n.getLocale();
840
+ if (CDOT_ONLY.includes(locale)) {
841
+ return false;
842
+ }
843
+ if (TIMES_ONLY.includes(locale)) {
844
+ return true;
845
+ }
846
+ return convertDotToTimes;
847
+ }
824
848
 
825
849
  function handleLeftArrow(mathField, cursor) {
826
850
  // If we're inside a function, and just after the left parentheses, we
@@ -1399,6 +1423,8 @@ class MathInput extends React__namespace.Component {
1399
1423
 
1400
1424
  // @ts-expect-error - TS2564 - Property 'blurOnTouchEndOutside' has no initializer and is not definitely assigned in the constructor.
1401
1425
 
1426
+ // @ts-expect-error - TS2564 - Property 'blurOnClickOutside' has no initializer and is not definitely assigned in the constructor.
1427
+
1402
1428
  // @ts-expect-error - TS2564 - Property '_container' has no initializer and is not definitely assigned in the constructor.
1403
1429
 
1404
1430
  // @ts-expect-error - TS2564 - Property '_containerBounds' has no initializer and is not definitely assigned in the constructor.
@@ -1446,6 +1472,16 @@ class MathInput extends React__namespace.Component {
1446
1472
  this._container = ReactDOM__default["default"].findDOMNode(this);
1447
1473
  this._root = this._container.querySelector(".mq-root-block");
1448
1474
  this._root.addEventListener("scroll", this._handleScroll);
1475
+ const isWithinKeypadBounds = (x, y) => {
1476
+ const bounds = this._getKeypadBounds();
1477
+
1478
+ // If there are no bounds, then the keypad is not mounted, so we
1479
+ // assume that the event is not within the keypad bounds.
1480
+ if (!bounds) {
1481
+ return false;
1482
+ }
1483
+ return bounds.left <= x && bounds.right >= x && bounds.top <= y && bounds.bottom >= y || bounds.bottom < y;
1484
+ };
1449
1485
 
1450
1486
  // Record the initial scroll displacement on touch start. This allows
1451
1487
  // us to detect whether a touch event was a scroll and only blur the
@@ -1463,10 +1499,9 @@ class MathInput extends React__namespace.Component {
1463
1499
  if (!this._container.contains(evt.target)) {
1464
1500
  let touchDidStartInOrBelowKeypad = false;
1465
1501
  if (this.props.keypadElement && this.props.keypadElement.getDOMNode()) {
1466
- const bounds = this._getKeypadBounds();
1467
1502
  for (let i = 0; i < evt.changedTouches.length; i++) {
1468
1503
  const [x, y] = [evt.changedTouches[i].clientX, evt.changedTouches[i].clientY];
1469
- if (bounds.left <= x && bounds.right >= x && bounds.top <= y && bounds.bottom >= y || bounds.bottom < y) {
1504
+ if (isWithinKeypadBounds(x, y)) {
1470
1505
  touchDidStartInOrBelowKeypad = true;
1471
1506
  break;
1472
1507
  }
@@ -1502,25 +1537,31 @@ class MathInput extends React__namespace.Component {
1502
1537
  this.dragListener.detach();
1503
1538
  }
1504
1539
  };
1540
+
1541
+ // We want to allow the user to blur the input by clicking outside of it
1542
+ // when using ChromeOS third-party browsers that use mobile user agents,
1543
+ // but don't actually simulate touch events.
1544
+ this.blurOnClickOutside = evt => {
1545
+ if (this.state.focused) {
1546
+ if (!this._container.contains(evt.target)) {
1547
+ if (this.props.keypadElement && this.props.keypadElement.getDOMNode()) {
1548
+ const [x, y] = [evt.clientX, evt.clientY];
1549
+ // We only want to blur if the click is above the keypad,
1550
+ // to the left of the keypad, or to the right of the keypad.
1551
+ // The reasoning for not blurring for any clicks below the keypad is
1552
+ // that the keypad may be anchored above the 'Check answer' bottom bar,
1553
+ // in which case we don't want to dismiss the keypad on check.
1554
+ if (!isWithinKeypadBounds(x, y)) {
1555
+ this.blur();
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ };
1505
1561
  window.addEventListener("touchstart", this.recordTouchStartOutside);
1506
1562
  window.addEventListener("touchend", this.blurOnTouchEndOutside);
1507
1563
  window.addEventListener("touchcancel", this.blurOnTouchEndOutside);
1508
-
1509
- // HACK(benkomalo): if the window resizes, the keypad bounds can
1510
- // change. That's a bit peeking into the internals of the keypad
1511
- // itself, since we know bounds can change only when the viewport
1512
- // changes, but seems like a rare enough thing to get wrong that it's
1513
- // not worth wiring up extra things for the technical "purity" of
1514
- // having the keypad notify of changes to us.
1515
- window.addEventListener("resize", this._clearKeypadBoundsCache);
1516
- window.addEventListener("orientationchange", this._clearKeypadBoundsCache);
1517
- }
1518
-
1519
- // eslint-disable-next-line react/no-unsafe
1520
- UNSAFE_componentWillReceiveProps(props) {
1521
- if (this.props.keypadElement !== props.keypadElement) {
1522
- this._clearKeypadBoundsCache();
1523
- }
1564
+ window.addEventListener("click", this.blurOnClickOutside);
1524
1565
  }
1525
1566
  componentDidUpdate(prevProps, prevState) {
1526
1567
  if (this.mathField.getContent() !== this.props.value) {
@@ -1535,18 +1576,8 @@ class MathInput extends React__namespace.Component {
1535
1576
  window.removeEventListener("touchstart", this.recordTouchStartOutside);
1536
1577
  window.removeEventListener("touchend", this.blurOnTouchEndOutside);
1537
1578
  window.removeEventListener("touchcancel", this.blurOnTouchEndOutside);
1538
- // @ts-expect-error - TS2769 - No overload matches this call.
1539
- window.removeEventListener("resize", this._clearKeypadBoundsCache());
1540
- window.removeEventListener("orientationchange",
1541
- // @ts-expect-error - TS2769 - No overload matches this call.
1542
- this._clearKeypadBoundsCache());
1579
+ window.removeEventListener("click", this.blurOnClickOutside);
1543
1580
  }
1544
- _clearKeypadBoundsCache = () => {
1545
- this._keypadBounds = null;
1546
- };
1547
- _cacheKeypadBounds = keypadNode => {
1548
- this._keypadBounds = keypadNode.getBoundingClientRect();
1549
- };
1550
1581
  _updateInputPadding = () => {
1551
1582
  this._container = ReactDOM__default["default"].findDOMNode(this);
1552
1583
  this._root = this._container.querySelector(".mq-root-block");
@@ -1556,14 +1587,16 @@ class MathInput extends React__namespace.Component {
1556
1587
  this._root.style.fontSize = `${fontSizePt}pt`;
1557
1588
  };
1558
1589
 
1559
- /** Gets and cache they bounds of the keypadElement */
1560
- _getKeypadBounds = () => {
1561
- if (!this._keypadBounds) {
1562
- const node = this.props.keypadElement?.getDOMNode();
1563
- this._cacheKeypadBounds(node);
1590
+ /** Returns the current bounds of the keypadElement */
1591
+ _getKeypadBounds() {
1592
+ const keypadNode = this.props.keypadElement?.getDOMNode();
1593
+
1594
+ // If the keypad is mounted, return its bounds. Otherwise, return null.
1595
+ if (keypadNode instanceof Element) {
1596
+ return keypadNode.getBoundingClientRect();
1564
1597
  }
1565
- return this._keypadBounds;
1566
- };
1598
+ return null;
1599
+ }
1567
1600
  _updateCursorHandle = animateIntoPosition => {
1568
1601
  const containerBounds = this._container.getBoundingClientRect();
1569
1602
  const cursor = this._container.querySelector(".mq-cursor");
@@ -1861,6 +1894,32 @@ class MathInput extends React__namespace.Component {
1861
1894
  this.focus();
1862
1895
  }
1863
1896
  };
1897
+
1898
+ // We want to allow the user to be able to focus the input via click
1899
+ // when using ChromeOS third-party browsers that use mobile user agents,
1900
+ // but don't actually simulate touch events.
1901
+ handleClick = e => {
1902
+ e.stopPropagation();
1903
+
1904
+ // Hide the cursor handle on click
1905
+ this._hideCursorHandle();
1906
+
1907
+ // Cache the container bounds, so as to avoid re-computing. If we don't
1908
+ // have any content, then it's not necessary, since the cursor can't be
1909
+ // moved anyway.
1910
+ if (this.mathField.getContent() !== "") {
1911
+ this._containerBounds = this._container.getBoundingClientRect();
1912
+
1913
+ // Make the cursor visible and set the handle-less cursor's
1914
+ // location.
1915
+ this._insertCursorAtClosestNode(e.clientX, e.clientY);
1916
+ }
1917
+
1918
+ // Trigger a focus event, if we're not already focused.
1919
+ if (!this.state.focused) {
1920
+ this.focus();
1921
+ }
1922
+ };
1864
1923
  handleTouchMove = e => {
1865
1924
  e.stopPropagation();
1866
1925
 
@@ -2084,7 +2143,7 @@ class MathInput extends React__namespace.Component {
2084
2143
  onTouchStart: this.handleTouchStart,
2085
2144
  onTouchMove: this.handleTouchMove,
2086
2145
  onTouchEnd: this.handleTouchEnd,
2087
- onClick: e => e.stopPropagation(),
2146
+ onClick: this.handleClick,
2088
2147
  role: "textbox",
2089
2148
  ariaLabel: ariaLabel
2090
2149
  }, /*#__PURE__*/React__namespace.createElement("div", {
@@ -5127,7 +5186,7 @@ function SharedKeys(props) {
5127
5186
  coord: fractionCoord,
5128
5187
  secondary: true
5129
5188
  }), /*#__PURE__*/React__namespace.createElement(KeypadButton, {
5130
- keyConfig: convertDotToTimes ? KeyConfigs.TIMES : KeyConfigs.CDOT,
5189
+ keyConfig: convertDotToTimesByLocale(!!convertDotToTimes) ? KeyConfigs.TIMES : KeyConfigs.CDOT,
5131
5190
  onClickKey: onClickKey,
5132
5191
  coord: [4, 1],
5133
5192
  secondary: true
@@ -5872,6 +5931,7 @@ exports.KeypadInput = MathInput;
5872
5931
  exports.KeypadType = KeypadType;
5873
5932
  exports.MobileKeypad = MobileKeypad;
5874
5933
  exports.StatefulKeypadContextProvider = StatefulKeypadContextProvider;
5934
+ exports.convertDotToTimesByLocale = convertDotToTimesByLocale;
5875
5935
  exports.createMathField = createMathField;
5876
5936
  exports.getCursorContext = getCursorContext;
5877
5937
  exports.keyTranslator = keyToMathquillMap;