@khanacademy/perseus 54.0.0 → 55.0.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.
Files changed (43) hide show
  1. package/LICENSE +18 -0
  2. package/dist/components/number-input.d.ts +9 -10
  3. package/dist/components/range-input.d.ts +5 -6
  4. package/dist/components/simple-keypad-input.d.ts +5 -0
  5. package/dist/components/stub-tag-editor.d.ts +3 -4
  6. package/dist/components/text-list-editor.d.ts +3 -4
  7. package/dist/es/index.css +16 -6
  8. package/dist/es/index.js +919 -1066
  9. package/dist/es/index.js.map +1 -1
  10. package/dist/es/strings.js +40 -10
  11. package/dist/es/strings.js.map +1 -1
  12. package/dist/index.css +16 -6
  13. package/dist/index.js +1055 -1170
  14. package/dist/index.js.map +1 -1
  15. package/dist/mixins/changeable.d.ts +1 -2
  16. package/dist/perseus-api.d.ts +1 -56
  17. package/dist/shared-utils/add-library-version-to-perseus-debug.d.ts +9 -0
  18. package/dist/strings.d.ts +34 -4
  19. package/dist/strings.js +142 -97
  20. package/dist/strings.js.map +1 -1
  21. package/dist/types.d.ts +3 -1
  22. package/dist/util.d.ts +0 -5
  23. package/dist/widget-ai-utils/numeric-input/prompt-utils.d.ts +1 -1
  24. package/dist/widgets/categorizer/categorizer.d.ts +1 -1
  25. package/dist/widgets/dropdown/dropdown.d.ts +5 -4
  26. package/dist/widgets/expression/expression.d.ts +0 -2
  27. package/dist/widgets/interactive-graph.d.ts +4 -2
  28. package/dist/widgets/interactive-graphs/graphs/circle.d.ts +7 -0
  29. package/dist/widgets/interactive-graphs/graphs/components/movable-line.d.ts +6 -0
  30. package/dist/widgets/interactive-graphs/graphs/quadratic.d.ts +8 -0
  31. package/dist/widgets/interactive-graphs/graphs/sinusoid.d.ts +7 -0
  32. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +1 -1
  33. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +6 -1
  34. package/dist/widgets/numeric-input/index.d.ts +1 -1
  35. package/dist/widgets/numeric-input/input-with-examples.d.ts +8 -0
  36. package/dist/widgets/numeric-input/numeric-input.class.d.ts +66 -0
  37. package/dist/widgets/numeric-input/numeric-input.d.ts +123 -77
  38. package/dist/widgets/numeric-input/utils.d.ts +25 -0
  39. package/dist/widgets/radio/radio.d.ts +1 -1
  40. package/dist/widgets/sorter/sorter.d.ts +1 -1
  41. package/package.json +120 -124
  42. package/dist/components/input-with-examples.d.ts +0 -51
  43. package/dist/components/tooltip.d.ts +0 -85
package/dist/es/index.js CHANGED
@@ -1,16 +1,17 @@
1
+ import _extends from '@babel/runtime/helpers/extends';
1
2
  import * as KAS from '@khanacademy/kas';
2
3
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
- import { PerseusError, Errors, expressionLogic, getDecimalSeparator, approximateDeepEqual, pluck, CoreWidgetRegistry, getUpgradedWidgetOptions, mapObject, passageRefLogic, radioLogic, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getMatrixSize, measurerLogic, addLibraryVersionToPerseusDebug, upgradeWidgetInfoToLatestVersion, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
4
+ import { PerseusError, Errors, expressionLogic, getDecimalSeparator, approximateDeepEqual, pluck, CoreWidgetRegistry, getUpgradedWidgetOptions, mapObject, passageRefLogic, radioLogic, shuffle, random, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, shuffleMatcher, getMatrixSize, measurerLogic, upgradeWidgetInfoToLatestVersion, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
4
5
  export { isFailure, isSuccess, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parsePerseusItem } from '@khanacademy/perseus-core';
5
6
  import * as PerseusLinter from '@khanacademy/perseus-linter';
6
7
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
7
8
  import { View, RenderStateRoot, Id, useOnMountEffect, useLatestRef, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
8
- import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
9
+ import Tooltip, { TooltipContent } from '@khanacademy/wonder-blocks-tooltip';
9
10
  import { HeadingMedium, LabelSmall, LabelLarge, Body, LabelMedium } from '@khanacademy/wonder-blocks-typography';
10
11
  import { StyleSheet, css } from 'aphrodite';
11
12
  import classNames$1 from 'classnames';
12
13
  import * as React from 'react';
13
- import React__default, { useContext, useState, useEffect, useRef, createContext, forwardRef, useLayoutEffect, useImperativeHandle, useId } from 'react';
14
+ import React__default, { useContext, forwardRef, useId, useImperativeHandle, useRef, useState, useEffect, createContext, useLayoutEffect } from 'react';
14
15
  import * as ReactDOM from 'react-dom';
15
16
  import ReactDOM__default from 'react-dom';
16
17
  import _, { debounce as debounce$1 } from 'underscore';
@@ -21,12 +22,16 @@ import $ from 'jquery';
21
22
  import { v4 } from 'uuid';
22
23
  import PropTypes from 'prop-types';
23
24
  import { KhanAnswerTypes, emptyWidgetsFunctional, scoreWidgetsFunctional, flattenScores, inputNumberAnswerTypes, scoreRadio, scoreLabelImageMarker } from '@khanacademy/perseus-score';
25
+ import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
26
+ import { KeypadContext } from '@khanacademy/keypad-context';
27
+ import { TextField } from '@khanacademy/wonder-blocks-form';
24
28
  import { entries, UnreachableCaseError, keys } from '@khanacademy/wonder-stuff-core';
25
29
  import { CircularSpinner } from '@khanacademy/wonder-blocks-progress-spinner';
26
30
  import { point, KhanMath, number, vector as vector$3, line, geometry, angles, coefficients } from '@khanacademy/kmath';
31
+ import _classPrivateFieldLooseBase from '@babel/runtime/helpers/classPrivateFieldLooseBase';
32
+ import _classPrivateFieldLooseKey from '@babel/runtime/helpers/classPrivateFieldLooseKey';
27
33
  import SimpleMarkdown from '@khanacademy/simple-markdown';
28
34
  import { pureMarkdownRules } from '@khanacademy/pure-markdown';
29
- import { TextField } from '@khanacademy/wonder-blocks-form';
30
35
  import Button from '@khanacademy/wonder-blocks-button';
31
36
  import { Strut } from '@khanacademy/wonder-blocks-layout';
32
37
  import Banner from '@khanacademy/wonder-blocks-banner';
@@ -44,16 +49,6 @@ import IconButton from '@khanacademy/wonder-blocks-icon-button';
44
49
  import cornersOutIcon from '@phosphor-icons/core/regular/corners-out.svg';
45
50
  import Link from '@khanacademy/wonder-blocks-link';
46
51
 
47
- function _extends() {
48
- return _extends = Object.assign ? Object.assign.bind() : function (n) {
49
- for (var e = 1; e < arguments.length; e++) {
50
- var t = arguments[e];
51
- for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
52
- }
53
- return n;
54
- }, _extends.apply(null, arguments);
55
- }
56
-
57
52
  /**
58
53
  * The translated strings that are used to render Perseus.
59
54
  */
@@ -262,10 +257,14 @@ const mockStrings = {
262
257
  centerX,
263
258
  centerY
264
259
  }) => `Circle. The center point is at ${centerX} comma ${centerY}.`,
265
- srCircleRadiusPoint: ({
260
+ srCircleRadiusPointRight: ({
266
261
  radiusPointX,
267
262
  radiusPointY
268
- }) => `Radius point at ${radiusPointX} comma ${radiusPointY}.`,
263
+ }) => `Right radius endpoint at ${radiusPointX} comma ${radiusPointY}.`,
264
+ srCircleRadiusPointLeft: ({
265
+ radiusPointX,
266
+ radiusPointY
267
+ }) => `Left radius endpoint at ${radiusPointX} comma ${radiusPointY}.`,
269
268
  srCircleRadius: ({
270
269
  radius
271
270
  }) => `Circle radius is ${radius}.`,
@@ -300,13 +299,13 @@ const mockStrings = {
300
299
  xIntercept,
301
300
  yIntercept
302
301
  }) => `The line crosses the X-axis at ${xIntercept} comma 0 and the Y-axis at 0 comma ${yIntercept}.`,
303
- srLinearGraphOriginIntercept: "The line crosses the x and y axes at the graph's origin.",
302
+ srLinearGraphOriginIntercept: "The line crosses the X and Y axes at the graph's origin.",
304
303
  srLinearGrabHandle: ({
305
304
  point1X,
306
305
  point1Y,
307
306
  point2X,
308
307
  point2Y
309
- }) => `Line from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`,
308
+ }) => `Line going through point ${point1X} comma ${point1Y} and point ${point2X} comma ${point2Y}.`,
310
309
  srAngleStartingSide: ({
311
310
  x,
312
311
  y
@@ -393,6 +392,18 @@ const mockStrings = {
393
392
  x,
394
393
  y
395
394
  }) => `Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`,
395
+ srLinearSystemGrabHandle: ({
396
+ lineNumber,
397
+ point1X,
398
+ point1Y,
399
+ point2X,
400
+ point2Y
401
+ }) => `Line ${lineNumber} going through point ${point1X} comma ${point1Y} and point ${point2X} comma ${point2Y}.`,
402
+ srLinearSystemIntersection: ({
403
+ x,
404
+ y
405
+ }) => `Line 1 and line 2 intersect at point ${x} comma ${y}.`,
406
+ srLinearSystemParallel: "Line 1 and line 2 are parallel.",
396
407
  srRayGraph: "A ray on a coordinate plane.",
397
408
  srRayPoints: ({
398
409
  point1X,
@@ -485,10 +496,18 @@ const mockStrings = {
485
496
  x,
486
497
  y
487
498
  }) => `Midline intersection at ${x} comma ${y}.`,
488
- srSinusoidPeakPoint: ({
499
+ srSinusoidMaxPoint: ({
500
+ x,
501
+ y
502
+ }) => `Maximum point at ${x} comma ${y}.`,
503
+ srSinusoidMinPoint: ({
504
+ x,
505
+ y
506
+ }) => `Minimum point at ${x} comma ${y}.`,
507
+ srSinusoidFlatPoint: ({
489
508
  x,
490
509
  y
491
- }) => `Extremum point at ${x} comma ${y}.`,
510
+ }) => `Line through point at ${x} comma ${y}.`,
492
511
  srSinusoidDescription: ({
493
512
  minValue,
494
513
  maxValue,
@@ -1021,11 +1040,11 @@ const useDependencies = () => {
1021
1040
  };
1022
1041
 
1023
1042
  var dependencies = /*#__PURE__*/Object.freeze({
1024
- __proto__: null,
1025
- setDependencies: setDependencies,
1026
- getDependencies: getDependencies,
1027
- DependenciesContext: DependenciesContext,
1028
- useDependencies: useDependencies
1043
+ __proto__: null,
1044
+ setDependencies: setDependencies,
1045
+ getDependencies: getDependencies,
1046
+ DependenciesContext: DependenciesContext,
1047
+ useDependencies: useDependencies
1029
1048
  });
1030
1049
 
1031
1050
  /**
@@ -1129,9 +1148,9 @@ const propTypes = {
1129
1148
  };
1130
1149
 
1131
1150
  var changeable = /*#__PURE__*/Object.freeze({
1132
- __proto__: null,
1133
- change: change,
1134
- propTypes: propTypes
1151
+ __proto__: null,
1152
+ change: change,
1153
+ propTypes: propTypes
1135
1154
  });
1136
1155
 
1137
1156
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -1691,7 +1710,7 @@ class Expression extends React.Component {
1691
1710
  }, /*#__PURE__*/React.createElement(View, {
1692
1711
  style: a11y.srOnly,
1693
1712
  role: "alert"
1694
- }, this.state.showErrorTooltip && ERROR_TITLE + " " + ERROR_MESSAGE), /*#__PURE__*/React.createElement(Tooltip$1, {
1713
+ }, this.state.showErrorTooltip && ERROR_TITLE + " " + ERROR_MESSAGE), /*#__PURE__*/React.createElement(Tooltip, {
1695
1714
  forceAnchorFocusivity: false,
1696
1715
  opened: this.state.showErrorTooltip,
1697
1716
  title: ERROR_TITLE,
@@ -1847,6 +1866,182 @@ var Expression$1 = {
1847
1866
  }
1848
1867
  };
1849
1868
 
1869
+ const _excluded$4 = ["keypadElement", "onFocus", "value"];
1870
+ class SimpleKeypadInput extends React.Component {
1871
+ constructor(...args) {
1872
+ super(...args);
1873
+ this._isMounted = false;
1874
+ this.inputRef = /*#__PURE__*/React.createRef();
1875
+ }
1876
+ componentDidMount() {
1877
+ // TODO(scottgrant): This is a hack to remove the deprecated call to
1878
+ // this.isMounted() but is still considered an anti-pattern.
1879
+ this._isMounted = true;
1880
+ }
1881
+ componentWillUnmount() {
1882
+ this._isMounted = false;
1883
+ }
1884
+ focus() {
1885
+ var _this$inputRef$curren;
1886
+ // The inputRef is a ref to a MathInput, which
1887
+ // also controls the keypad state during focus events.
1888
+ (_this$inputRef$curren = this.inputRef.current) == null || _this$inputRef$curren.focus(this.context.setKeypadActive);
1889
+ }
1890
+ blur() {
1891
+ var _this$inputRef$curren2;
1892
+ (_this$inputRef$curren2 = this.inputRef.current) == null || _this$inputRef$curren2.blur();
1893
+ }
1894
+ getValue() {
1895
+ return this.props.value;
1896
+ }
1897
+ render() {
1898
+ const _this = this;
1899
+ // Intercept the `onFocus` prop, as we need to configure the keypad
1900
+ // before continuing with the default focus logic for Perseus inputs.
1901
+ // Intercept the `value` prop so as to map `null` to the empty string,
1902
+ // as the `KeypadInput` does not support `null` values.
1903
+ const _this$props = _this.props,
1904
+ {
1905
+ keypadElement,
1906
+ onFocus,
1907
+ value
1908
+ } = _this$props,
1909
+ rest = _objectWithoutPropertiesLoose(_this$props, _excluded$4);
1910
+ return (
1911
+ /*#__PURE__*/
1912
+ // @ts-expect-error - TS2769 - No overload matches this call.
1913
+ React.createElement(KeypadInput, _extends({
1914
+ ref: this.inputRef,
1915
+ keypadElement: keypadElement,
1916
+ onFocus: () => {
1917
+ if (keypadElement) {
1918
+ keypadElement.configure({
1919
+ keypadType: KeypadType.FRACTION
1920
+ }, () => {
1921
+ if (_this._isMounted) {
1922
+ onFocus == null || onFocus();
1923
+ }
1924
+ });
1925
+ } else {
1926
+ onFocus == null || onFocus();
1927
+ }
1928
+ },
1929
+ value: value == null ? "" : "" + value
1930
+ }, rest))
1931
+ );
1932
+ }
1933
+ }
1934
+
1935
+ // @ts-expect-error - TS2339 - Property 'propTypes' does not exist on type 'typeof SimpleKeypadInput'.
1936
+ SimpleKeypadInput.contextType = KeypadContext;
1937
+ SimpleKeypadInput.propTypes = {
1938
+ keypadElement: keypadElementPropType,
1939
+ onFocus: PropTypes.func,
1940
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
1941
+ };
1942
+
1943
+ const getPromptJSON$s = (renderProps, userInput) => {
1944
+ return {
1945
+ type: "input-number",
1946
+ options: {
1947
+ simplify: renderProps.simplify,
1948
+ answerType: renderProps.answerType
1949
+ },
1950
+ userInput: {
1951
+ value: userInput.currentValue
1952
+ }
1953
+ };
1954
+ };
1955
+
1956
+ /* eslint-disable @khanacademy/ts-no-error-suppressions */
1957
+ let lastId = 0;
1958
+ function uniqueIdForInput(prefix = "input-") {
1959
+ lastId++;
1960
+ return `${prefix}${lastId}`;
1961
+ }
1962
+ class TextInput extends React.Component {
1963
+ constructor(props) {
1964
+ super(props);
1965
+ this.inputRef = /*#__PURE__*/React.createRef();
1966
+ this.id = void 0;
1967
+ this._getInput = () => {
1968
+ if (!this.inputRef.current) {
1969
+ throw new PerseusError("Input ref accessed before set", Errors.Internal);
1970
+ }
1971
+ return this.inputRef.current;
1972
+ };
1973
+ this.focus = () => {
1974
+ this._getInput().focus();
1975
+ };
1976
+ this.blur = () => {
1977
+ this._getInput().blur();
1978
+ };
1979
+ this.getValue = () => {
1980
+ var _this$inputRef$curren;
1981
+ return (_this$inputRef$curren = this.inputRef.current) == null ? void 0 : _this$inputRef$curren.value;
1982
+ };
1983
+ this.getStringValue = () => {
1984
+ var _this$inputRef$curren2;
1985
+ return (_this$inputRef$curren2 = this.inputRef.current) == null ? void 0 : _this$inputRef$curren2.value.toString();
1986
+ };
1987
+ this.setSelectionRange = (selectionStart, selectionEnd) => {
1988
+ this._getInput().setSelectionRange(selectionStart, selectionEnd);
1989
+ };
1990
+ this.getSelectionStart = () => {
1991
+ return this._getInput().selectionStart;
1992
+ };
1993
+ this.getSelectionEnd = () => {
1994
+ return this._getInput().selectionEnd;
1995
+ };
1996
+ if (props.id) {
1997
+ this.id = props.id;
1998
+ } else {
1999
+ this.id = uniqueIdForInput();
2000
+ }
2001
+ }
2002
+ render() {
2003
+ const {
2004
+ labelText,
2005
+ value,
2006
+ onFocus,
2007
+ onBlur,
2008
+ disabled,
2009
+ placeholder,
2010
+ onKeyDown,
2011
+ style
2012
+ } = this.props;
2013
+ const formattedValue = value === null ? "" : value.toString();
2014
+
2015
+ // Some of our content was saved with empty strings as the label text,
2016
+ // and we don't want to render an empty aria-label attribute.
2017
+ const ariaLabel = labelText || undefined;
2018
+ return /*#__PURE__*/React.createElement(TextField, {
2019
+ ref: this.inputRef,
2020
+ style: style,
2021
+ disabled: disabled,
2022
+ id: this.id,
2023
+ value: formattedValue,
2024
+ type: "text",
2025
+ "aria-label": ariaLabel,
2026
+ "aria-describedby": this.props["aria-describedby"],
2027
+ onChange: value => this.props.onChange(value),
2028
+ placeholder: placeholder,
2029
+ onFocus: onFocus,
2030
+ onBlur: onBlur,
2031
+ onKeyDown: onKeyDown
2032
+ // @ts-expect-error - TS2322 - Type '{ style: StyleType; disabled: boolean | undefined; id: string; value: string; type: "text"; "aria-label": string | undefined; onChange: (value: string) => void; placeholder: string | undefined; ... 5 more ...; autoComplete: string; }' is not assignable to type 'IntrinsicAttributes & ExportProps & RefAttributes<HTMLInputElement>'.
2033
+ ,
2034
+ autoCorrect: "off",
2035
+ autoCapitalize: "off",
2036
+ autoComplete: "off"
2037
+ });
2038
+ }
2039
+ }
2040
+ TextInput.defaultProps = {
2041
+ value: "",
2042
+ disabled: false
2043
+ };
2044
+
1850
2045
  /**
1851
2046
  * A React context for keeping track of whether an component is fully
1852
2047
  * loaded. This is used to know when SvgImage and (Zoomable)TeX are finished
@@ -1939,54 +2134,6 @@ const rWidgetRule = /^\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;
1939
2134
  const rTypeFromWidgetId = /^([a-z-]+) ([0-9]+)$/;
1940
2135
  const rWidgetParts = new RegExp(rWidgetRule.source + "$");
1941
2136
  const snowman = "\u2603";
1942
- const seededRNG$1 = function seededRNG(seed) {
1943
- let randomSeed = seed;
1944
- return function () {
1945
- // Robert Jenkins' 32 bit integer hash function.
1946
- let seed = randomSeed;
1947
- seed = seed + 0x7ed55d16 + (seed << 12) & 0xffffffff;
1948
- seed = (seed ^ 0xc761c23c ^ seed >>> 19) & 0xffffffff;
1949
- seed = seed + 0x165667b1 + (seed << 5) & 0xffffffff;
1950
- seed = (seed + 0xd3a2646c ^ seed << 9) & 0xffffffff;
1951
- seed = seed + 0xfd7046c5 + (seed << 3) & 0xffffffff;
1952
- seed = (seed ^ 0xb55a4f09 ^ seed >>> 16) & 0xffffffff;
1953
- return (randomSeed = seed & 0xfffffff) / 0x10000000;
1954
- };
1955
- };
1956
-
1957
- // Shuffle an array using a given random seed or function.
1958
- // If `ensurePermuted` is true, the input and ouput are guaranteed to be
1959
- // distinct permutations.
1960
- function shuffle$3(array, randomSeed, ensurePermuted = false) {
1961
- // Always return a copy of the input array
1962
- const shuffled = _.clone(array);
1963
-
1964
- // Handle edge cases (input array is empty or uniform)
1965
- if (!shuffled.length || _.all(shuffled, function (value) {
1966
- return _.isEqual(value, shuffled[0]);
1967
- })) {
1968
- return shuffled;
1969
- }
1970
- let random;
1971
- if (typeof randomSeed === "function") {
1972
- random = randomSeed;
1973
- } else {
1974
- random = seededRNG$1(randomSeed);
1975
- }
1976
- do {
1977
- // Fischer-Yates shuffle
1978
- for (let top = shuffled.length; top > 0; top--) {
1979
- const newEnd = Math.floor(random() * top);
1980
- const temp = shuffled[newEnd];
1981
-
1982
- // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
1983
- shuffled[newEnd] = shuffled[top - 1];
1984
- // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading.
1985
- shuffled[top - 1] = temp;
1986
- }
1987
- } while (ensurePermuted && _.isEqual(array, shuffled));
1988
- return shuffled;
1989
- }
1990
2137
 
1991
2138
  /**
1992
2139
  * TODO(somewhatabstract, FEI-3463):
@@ -2419,7 +2566,6 @@ const textarea = {
2419
2566
  * to translate it but don't need the extra dollar signs.
2420
2567
  */
2421
2568
  const unescapeMathMode$1 = label => label.startsWith("$") && label.endsWith("$") ? label.slice(1, -1) : label;
2422
- const random$1 = seededRNG$1(new Date().getTime() & 0xffffffff);
2423
2569
  const Util = {
2424
2570
  inputPathsEqual,
2425
2571
  nestedMap: nestedMap$2,
@@ -2427,8 +2573,6 @@ const Util = {
2427
2573
  rTypeFromWidgetId,
2428
2574
  rWidgetParts,
2429
2575
  snowman,
2430
- seededRNG: seededRNG$1,
2431
- shuffle: shuffle$3,
2432
2576
  split,
2433
2577
  firstNumericalParse: firstNumericalParse$1,
2434
2578
  stringArrayOfSize: stringArrayOfSize$1,
@@ -2456,16 +2600,17 @@ const Util = {
2456
2600
  getSvgUrl: getSvgUrl,
2457
2601
  getDataUrl: getDataUrl,
2458
2602
  textarea,
2459
- unescapeMathMode: unescapeMathMode$1,
2460
- random: random$1
2603
+ unescapeMathMode: unescapeMathMode$1
2461
2604
  };
2462
2605
 
2463
2606
  // TODO(LP-11481): Change the primary API we use for logging in Perseus to use
2464
2607
  // getDependencies() internally... simplify usage to just using a "singleton"
2465
2608
  // which get's the injected logger.
2609
+
2466
2610
  /**
2467
2611
  * Provides logging infrastructure for Perseus
2468
2612
  */
2613
+
2469
2614
  const Log = {
2470
2615
  // Logs a message
2471
2616
  log: (message, extra) => {
@@ -3139,40 +3284,40 @@ const articleMaxWidthInPx = 688;
3139
3284
  const articleMaxWidthTableInPx = 512;
3140
3285
 
3141
3286
  var constants = /*#__PURE__*/Object.freeze({
3142
- __proto__: null,
3143
- baseFontFamily: baseFontFamily,
3144
- boldFontFamily: boldFontFamily,
3145
- kaGreen: kaGreen,
3146
- white: white,
3147
- gray97: gray97,
3148
- gray85: gray85,
3149
- gray76: gray76,
3150
- gray68: gray68,
3151
- gray17: gray17,
3152
- pureSmMin: pureSmMin$1,
3153
- pureMdMin: pureMdMin$1,
3154
- pureLgMin: pureLgMin$1,
3155
- pureXlMin: pureXlMin$1,
3156
- pureXsMax: pureXsMax$1,
3157
- pureSmMax: pureSmMax$1,
3158
- pureMdMax: pureMdMax$1,
3159
- pureLgMax: pureLgMax$1,
3160
- tableBackgroundAccent: tableBackgroundAccent,
3161
- phoneMargin: phoneMargin,
3162
- negativePhoneMargin: negativePhoneMargin,
3163
- hintBorderWidth: hintBorderWidth,
3164
- baseUnitPx: baseUnitPx,
3165
- interactiveSizes: interactiveSizes$1,
3166
- circleSize: circleSize$1,
3167
- radioMarginWidth: radioMarginWidth$1,
3168
- warningColor: warningColor,
3169
- warningColorHover: warningColorHover,
3170
- warningColorActive: warningColorActive,
3171
- publishBlockingErrorColor: publishBlockingErrorColor,
3172
- radioBorderColor: radioBorderColor$1,
3173
- checkedColor: checkedColor$1,
3174
- articleMaxWidthInPx: articleMaxWidthInPx,
3175
- articleMaxWidthTableInPx: articleMaxWidthTableInPx
3287
+ __proto__: null,
3288
+ baseFontFamily: baseFontFamily,
3289
+ boldFontFamily: boldFontFamily,
3290
+ kaGreen: kaGreen,
3291
+ white: white,
3292
+ gray97: gray97,
3293
+ gray85: gray85,
3294
+ gray76: gray76,
3295
+ gray68: gray68,
3296
+ gray17: gray17,
3297
+ pureSmMin: pureSmMin$1,
3298
+ pureMdMin: pureMdMin$1,
3299
+ pureLgMin: pureLgMin$1,
3300
+ pureXlMin: pureXlMin$1,
3301
+ pureXsMax: pureXsMax$1,
3302
+ pureSmMax: pureSmMax$1,
3303
+ pureMdMax: pureMdMax$1,
3304
+ pureLgMax: pureLgMax$1,
3305
+ tableBackgroundAccent: tableBackgroundAccent,
3306
+ phoneMargin: phoneMargin,
3307
+ negativePhoneMargin: negativePhoneMargin,
3308
+ hintBorderWidth: hintBorderWidth,
3309
+ baseUnitPx: baseUnitPx,
3310
+ interactiveSizes: interactiveSizes$1,
3311
+ circleSize: circleSize$1,
3312
+ radioMarginWidth: radioMarginWidth$1,
3313
+ warningColor: warningColor,
3314
+ warningColorHover: warningColorHover,
3315
+ warningColorActive: warningColorActive,
3316
+ publishBlockingErrorColor: publishBlockingErrorColor,
3317
+ radioBorderColor: radioBorderColor$1,
3318
+ checkedColor: checkedColor$1,
3319
+ articleMaxWidthInPx: articleMaxWidthInPx,
3320
+ articleMaxWidthTableInPx: articleMaxWidthTableInPx
3176
3321
  });
3177
3322
 
3178
3323
  /**
@@ -3522,16 +3667,6 @@ const InteractiveUtil = {
3522
3667
  }
3523
3668
  };
3524
3669
 
3525
- function _classPrivateFieldBase(e, t) {
3526
- if (!{}.hasOwnProperty.call(e, t)) throw new TypeError("attempted to use private field on non-instance");
3527
- return e;
3528
- }
3529
-
3530
- var id = 0;
3531
- function _classPrivateFieldKey(e) {
3532
- return "__private_" + id++ + "_" + e;
3533
- }
3534
-
3535
3670
  /*!
3536
3671
  * Raphael 1.5.2 - JavaScript Vector Library
3537
3672
  *
@@ -3588,7 +3723,7 @@ var Raphael$1 = (function () {
3588
3723
  appendChild = "appendChild",
3589
3724
  apply = "apply",
3590
3725
  concat = "concat",
3591
- supportsTouch = ("createTouch" in doc),
3726
+ supportsTouch = "createTouch" in doc,
3592
3727
  E = "",
3593
3728
  S = " ",
3594
3729
  Str = String,
@@ -5193,10 +5328,10 @@ var Raphael$1 = (function () {
5193
5328
  });
5194
5329
  break;
5195
5330
  case "gradient":
5196
- (({
5331
+ ({
5197
5332
  circle: 1,
5198
5333
  ellipse: 1
5199
- })[has](o.type) || Str(value).charAt() != "r") && _addGradientFill(node, value, o.paper);
5334
+ }[has](o.type) || Str(value).charAt() != "r") && _addGradientFill(node, value, o.paper);
5200
5335
  break;
5201
5336
  case "opacity":
5202
5337
  if (attrs.gradient && !attrs[has]("stroke-opacity")) {
@@ -8023,8 +8158,8 @@ function polar$2(r, th) {
8023
8158
  th = th * Math.PI / 180;
8024
8159
  return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
8025
8160
  }
8026
- var _bounds = /*#__PURE__*/_classPrivateFieldKey("bounds");
8027
- var _drawingTransform = /*#__PURE__*/_classPrivateFieldKey("drawingTransform");
8161
+ var _bounds = /*#__PURE__*/_classPrivateFieldLooseKey("bounds");
8162
+ var _drawingTransform = /*#__PURE__*/_classPrivateFieldLooseKey("drawingTransform");
8028
8163
  class Graphie$1 {
8029
8164
  constructor(el) {
8030
8165
  this.el = void 0;
@@ -8229,8 +8364,8 @@ class Graphie$1 {
8229
8364
  if (options.range == null) {
8230
8365
  throw new PerseusError("range should be specified in graph init", Errors.Internal);
8231
8366
  }
8232
- _classPrivateFieldBase(this, _bounds)[_bounds] = new GraphBounds(...options.range);
8233
- _classPrivateFieldBase(this, _drawingTransform)[_drawingTransform] = new DrawingTransform(this.raphael, scale, this.bounds());
8367
+ _classPrivateFieldLooseBase(this, _bounds)[_bounds] = new GraphBounds(...options.range);
8368
+ _classPrivateFieldLooseBase(this, _drawingTransform)[_drawingTransform] = new DrawingTransform(this.raphael, scale, this.bounds());
8234
8369
  const [w, h] = this.drawingTransform().canvasDimensions();
8235
8370
  $(this.el).css({
8236
8371
  width: w,
@@ -8465,16 +8600,16 @@ class Graphie$1 {
8465
8600
  }
8466
8601
  }
8467
8602
  drawingTransform() {
8468
- if (_classPrivateFieldBase(this, _drawingTransform)[_drawingTransform] == null) {
8603
+ if (_classPrivateFieldLooseBase(this, _drawingTransform)[_drawingTransform] == null) {
8469
8604
  throw new Error("Can't get drawingTransform of an uninitialized Graphie");
8470
8605
  }
8471
- return _classPrivateFieldBase(this, _drawingTransform)[_drawingTransform];
8606
+ return _classPrivateFieldLooseBase(this, _drawingTransform)[_drawingTransform];
8472
8607
  }
8473
8608
  bounds() {
8474
- if (_classPrivateFieldBase(this, _bounds)[_bounds] == null) {
8609
+ if (_classPrivateFieldLooseBase(this, _bounds)[_bounds] == null) {
8475
8610
  throw new Error("Can't get bounds of an uninitialized Graphie");
8476
8611
  }
8477
- return _classPrivateFieldBase(this, _bounds)[_bounds];
8612
+ return _classPrivateFieldLooseBase(this, _bounds)[_bounds];
8478
8613
  }
8479
8614
  style(attrs, fn) {
8480
8615
  const processed = this.processAttributes(attrs);
@@ -9025,8 +9160,10 @@ const labelDirections = {
9025
9160
  // is equivalent to [[-5, 5], [-10, 10]].
9026
9161
  // - A single magnitude to be used for both x and y. E.g. 10 is equivalent to
9027
9162
  // [[-10, 10], [-10, 10]]
9163
+
9028
9164
  // Overload normalizeRange to specify that it only returns undefined if its
9029
9165
  // argument is undefined.
9166
+
9030
9167
  // normalizeRange converts a RangeSpecifier into a pair of Intervals that give
9031
9168
  // the x and y ranges, respectively
9032
9169
  function normalizeRange(range) {
@@ -9219,7 +9356,7 @@ const GraphUtils = {
9219
9356
  clickBlockList = [],
9220
9357
  blockMouseTriggers = false,
9221
9358
  blockTouchTriggers = false,
9222
- eventCaptureSupported = ("addEventListener" in document),
9359
+ eventCaptureSupported = "addEventListener" in document,
9223
9360
  $document = $(document),
9224
9361
  nextTouchID = 1,
9225
9362
  lastTouchID = 0;
@@ -9914,7 +10051,6 @@ function scaledPolarRad(radius, radians) {
9914
10051
  return [radius * Math.cos(radians), radius * Math.sin(radians) * -1 // SVG flips y axis
9915
10052
  ];
9916
10053
  }
9917
-
9918
10054
  function scaledPolarDeg(radius, degrees) {
9919
10055
  const radians = degrees * Math.PI / 180;
9920
10056
  return scaledPolarRad(radius, radians);
@@ -11732,7 +11868,6 @@ _.extend(GraphUtils.Graphie.prototype, {
11732
11868
  if (hideArrow) {
11733
11869
  return; // Don't draw anything!
11734
11870
  }
11735
-
11736
11871
  const handleVec = vector$3.subtract(handleCoord, rotatePoint.coord);
11737
11872
  const handlePolar = vector$3.polarDegFromCart(handleVec);
11738
11873
  const angle = handlePolar[1];
@@ -12696,7 +12831,6 @@ _.extend(MovableAngle.prototype, {
12696
12831
  // clockwise to the first point
12697
12832
  coords[1] // the vertex parameter is last
12698
12833
  );
12699
-
12700
12834
  const clockwiseAngle = rawAngle + 360 % 360;
12701
12835
  return clockwiseAngle;
12702
12836
  },
@@ -14070,7 +14204,7 @@ class MovablePoint$5 {
14070
14204
  const showTrashTooltip = () => {
14071
14205
  this._showTooltip(container => {
14072
14206
  // eslint-disable-next-line import/no-deprecated
14073
- render( /*#__PURE__*/React.createElement("span", {
14207
+ render(/*#__PURE__*/React.createElement("span", {
14074
14208
  style: {
14075
14209
  fontSize: "2em"
14076
14210
  }
@@ -14869,7 +15003,6 @@ const DEFAULT_PROPS = {
14869
15003
  // likewise
14870
15004
  labelStyle: null // likewise
14871
15005
  };
14872
-
14873
15006
  const DEFAULT_STATE = {
14874
15007
  added: false,
14875
15008
  hasMoved: false,
@@ -17520,7 +17653,7 @@ class TranslationLinter {
17520
17653
  * Preprocess TeX code to convert things that MathJax doesn't know how to handle
17521
17654
  * to things is does.
17522
17655
  */
17523
- var preprocessTex = (texCode => texCode
17656
+ var preprocessTex = texCode => texCode
17524
17657
  // Replace uses of \begin{align}...\end{align} which KaTeX doesn't
17525
17658
  // support (yet) with \begin{aligned}...\end{aligned} which renders
17526
17659
  // the same is supported by KaTeX. It does the same for align*.
@@ -17530,7 +17663,7 @@ var preprocessTex = (texCode => texCode
17530
17663
  // "aligned").
17531
17664
  .replace(/\{align[*]?\}/g, "{aligned}")
17532
17665
  // Replace non-breaking spaces with regular spaces.
17533
- .replace(/[\u00a0]/g, " "));
17666
+ .replace(/[\u00a0]/g, " ");
17534
17667
 
17535
17668
  const {
17536
17669
  interactiveSizes
@@ -17792,29 +17925,29 @@ const isLintable = type => {
17792
17925
  };
17793
17926
 
17794
17927
  var widgets$1 = /*#__PURE__*/Object.freeze({
17795
- __proto__: null,
17796
- registerWidget: registerWidget,
17797
- registerWidgets: registerWidgets,
17798
- replaceWidget: replaceWidget,
17799
- replaceDeprecatedWidgets: replaceDeprecatedWidgets,
17800
- registerEditors: registerEditors,
17801
- replaceEditor: replaceEditor,
17802
- replaceDeprecatedEditors: replaceDeprecatedEditors,
17803
- getWidget: getWidget,
17804
- getWidgetExport: getWidgetExport,
17805
- getEditor: getEditor,
17806
- getTransform: getTransform,
17807
- getVersion: getVersion,
17808
- getVersionVector: getVersionVector,
17809
- getPublicWidgets: getPublicWidgets,
17810
- isAccessible: isAccessible,
17811
- getAllWidgetTypes: getAllWidgetTypes,
17812
- getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
17813
- traverseChildWidgets: traverseChildWidgets$3,
17814
- supportsStaticMode: supportsStaticMode,
17815
- getStaticTransform: getStaticTransform,
17816
- getTracking: getTracking,
17817
- isLintable: isLintable
17928
+ __proto__: null,
17929
+ registerWidget: registerWidget,
17930
+ registerWidgets: registerWidgets,
17931
+ replaceWidget: replaceWidget,
17932
+ replaceDeprecatedWidgets: replaceDeprecatedWidgets,
17933
+ registerEditors: registerEditors,
17934
+ replaceEditor: replaceEditor,
17935
+ replaceDeprecatedEditors: replaceDeprecatedEditors,
17936
+ getWidget: getWidget,
17937
+ getWidgetExport: getWidgetExport,
17938
+ getEditor: getEditor,
17939
+ getTransform: getTransform,
17940
+ getVersion: getVersion,
17941
+ getVersionVector: getVersionVector,
17942
+ getPublicWidgets: getPublicWidgets,
17943
+ isAccessible: isAccessible,
17944
+ getAllWidgetTypes: getAllWidgetTypes,
17945
+ getRendererPropsForWidgetInfo: getRendererPropsForWidgetInfo,
17946
+ traverseChildWidgets: traverseChildWidgets$3,
17947
+ supportsStaticMode: supportsStaticMode,
17948
+ getStaticTransform: getStaticTransform,
17949
+ getTracking: getTracking,
17950
+ isLintable: isLintable
17818
17951
  });
17819
17952
 
17820
17953
  class WidgetContainer extends React.Component {
@@ -17857,7 +17990,6 @@ class WidgetContainer extends React.Component {
17857
17990
  /* eslint-enable react/no-did-mount-set-state */
17858
17991
  }
17859
17992
  }
17860
-
17861
17993
  UNSAFE_componentWillReceiveProps(nextProps) {
17862
17994
  if (this.props.type !== nextProps.type) {
17863
17995
  throw new Error("WidgetContainer can't change widget type; set a different " + "key instead to recreate the container.");
@@ -19311,620 +19443,123 @@ const styles$A = {
19311
19443
  }
19312
19444
  };
19313
19445
 
19314
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
19315
- let lastId = 0;
19316
- function uniqueIdForInput(prefix = "input-") {
19317
- lastId++;
19318
- return `${prefix}${lastId}`;
19319
- }
19320
- class TextInput extends React.Component {
19321
- constructor(props) {
19322
- super(props);
19323
- this.inputRef = /*#__PURE__*/React.createRef();
19324
- this.id = void 0;
19325
- this._getInput = () => {
19326
- if (!this.inputRef.current) {
19327
- throw new PerseusError("Input ref accessed before set", Errors.Internal);
19446
+ const {
19447
+ captureScratchpadTouchStart: captureScratchpadTouchStart$2
19448
+ } = Util;
19449
+ // [LEMS-2411](Jan 2025) Third: This component has been moved to the NumericInput
19450
+ // folder as we are actively working towards removing the InputNumber widget.
19451
+ // This comment can be removed as part of LEMS-2411.
19452
+
19453
+ /**
19454
+ * The InputWithExamples component is a child component of the NumericInput
19455
+ * and InputNumber components. It is responsible for rendering the UI elements
19456
+ * for the desktop versions of these widgets, and displays a tooltip with
19457
+ * examples of how to input the selected answer forms.
19458
+ */
19459
+ const InputWithExamples = /*#__PURE__*/forwardRef((props, ref) => {
19460
+ // Desctructure the props to set default values
19461
+ const {
19462
+ shouldShowExamples = true,
19463
+ onFocus = () => {},
19464
+ onBlur = () => {},
19465
+ disabled = false,
19466
+ linterContext = PerseusLinter.linterContextDefault,
19467
+ className = ""
19468
+ } = props;
19469
+ const context = React.useContext(PerseusI18nContext);
19470
+ const inputRef = React.useRef(null);
19471
+ const [inputFocused, setInputFocused] = React.useState(false);
19472
+ const id = useId();
19473
+ const ariaId = `aria-for-${id}`;
19474
+ useImperativeHandle(ref, () => ({
19475
+ current: inputRef.current,
19476
+ focus: () => {
19477
+ if (inputRef.current) {
19478
+ inputRef.current.focus();
19479
+ }
19480
+ },
19481
+ blur: () => {
19482
+ if (inputRef.current) {
19483
+ inputRef.current.blur();
19328
19484
  }
19329
- return this.inputRef.current;
19330
- };
19331
- this.focus = () => {
19332
- this._getInput().focus();
19333
- };
19334
- this.blur = () => {
19335
- this._getInput().blur();
19336
- };
19337
- this.getValue = () => {
19338
- var _this$inputRef$curren;
19339
- return (_this$inputRef$curren = this.inputRef.current) == null ? void 0 : _this$inputRef$curren.value;
19340
- };
19341
- this.getStringValue = () => {
19342
- var _this$inputRef$curren2;
19343
- return (_this$inputRef$curren2 = this.inputRef.current) == null ? void 0 : _this$inputRef$curren2.value.toString();
19344
- };
19345
- this.setSelectionRange = (selectionStart, selectionEnd) => {
19346
- this._getInput().setSelectionRange(selectionStart, selectionEnd);
19347
- };
19348
- this.getSelectionStart = () => {
19349
- return this._getInput().selectionStart;
19350
- };
19351
- this.getSelectionEnd = () => {
19352
- return this._getInput().selectionEnd;
19353
- };
19354
- if (props.id) {
19355
- this.id = props.id;
19356
- } else {
19357
- this.id = uniqueIdForInput();
19358
19485
  }
19359
- }
19360
- render() {
19361
- const {
19362
- labelText,
19363
- value,
19364
- onFocus,
19365
- onBlur,
19366
- disabled,
19367
- placeholder,
19368
- onKeyDown,
19369
- style
19370
- } = this.props;
19371
- const formattedValue = value === null ? "" : value.toString();
19372
- return /*#__PURE__*/React.createElement(TextField, {
19373
- ref: this.inputRef,
19374
- style: style,
19486
+ }));
19487
+ const getInputClassName = () => {
19488
+ let inputClassName = ClassNames.INPUT;
19489
+ if (inputFocused) {
19490
+ inputClassName += " " + ClassNames.FOCUSED;
19491
+ }
19492
+ if (className) {
19493
+ inputClassName += " " + className;
19494
+ }
19495
+ return inputClassName;
19496
+ };
19497
+ const handleFocus = () => {
19498
+ onFocus();
19499
+ setInputFocused(true);
19500
+ };
19501
+ const handleBlur = () => {
19502
+ onBlur();
19503
+ setInputFocused(false);
19504
+ };
19505
+ const renderInput = () => {
19506
+ // Generate the provided examples in simple language for screen readers.
19507
+ const examplesAria = props.shouldShowExamples ? `${props.examples[0]}
19508
+ ${props.examples.slice(1).join(", or\n")}`
19509
+ // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
19510
+ .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ") : "";
19511
+ const inputProps = {
19512
+ id: id,
19513
+ // If we have examples, we want to provide the aria-describedby attribute
19514
+ "aria-describedby": props.shouldShowExamples ? ariaId : undefined,
19515
+ ref: inputRef,
19516
+ className: getInputClassName(),
19517
+ labelText: props.labelText,
19518
+ value: props.value,
19519
+ onFocus: handleFocus,
19520
+ onBlur: handleBlur,
19375
19521
  disabled: disabled,
19376
- id: this.id,
19377
- value: formattedValue,
19378
- type: "text",
19379
- "aria-label": labelText,
19380
- "aria-describedby": this.props["aria-describedby"],
19381
- onChange: value => this.props.onChange(value),
19382
- placeholder: placeholder,
19383
- onFocus: onFocus,
19384
- onBlur: onBlur,
19385
- onKeyDown: onKeyDown
19386
- // @ts-expect-error - TS2322 - Type '{ style: StyleType; disabled: boolean | undefined; id: string; value: string; type: "text"; "aria-label": string | undefined; onChange: (value: string) => void; placeholder: string | undefined; ... 5 more ...; autoComplete: string; }' is not assignable to type 'IntrinsicAttributes & ExportProps & RefAttributes<HTMLInputElement>'.
19387
- ,
19388
- autoCorrect: "off",
19522
+ style: props.style,
19523
+ onChange: props.onChange,
19524
+ onTouchStart: captureScratchpadTouchStart$2,
19389
19525
  autoCapitalize: "off",
19390
- autoComplete: "off"
19391
- });
19392
- }
19393
- }
19394
- TextInput.defaultProps = {
19395
- value: "",
19396
- disabled: false
19397
- };
19398
-
19399
- function _objectWithoutPropertiesLoose(r, e) {
19400
- if (null == r) return {};
19401
- var t = {};
19402
- for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
19403
- if (-1 !== e.indexOf(n)) continue;
19404
- t[n] = r[n];
19405
- }
19406
- return t;
19407
- }
19408
-
19409
- const _excluded$4 = ["position", "visibility", "left", "top"];
19410
- const zIndex = 10;
19411
- let HorizontalDirection = /*#__PURE__*/function (HorizontalDirection) {
19412
- HorizontalDirection["Left"] = "left";
19413
- HorizontalDirection["Right"] = "right";
19414
- return HorizontalDirection;
19415
- }({});
19416
- let VerticalDirection = /*#__PURE__*/function (VerticalDirection) {
19417
- VerticalDirection["Top"] = "top";
19418
- VerticalDirection["Bottom"] = "bottom";
19419
- return VerticalDirection;
19420
- }({});
19421
- const Triangle = props => {
19422
- let borderLeft;
19423
- let borderRight;
19424
- let borderTop;
19425
- let borderBottom;
19426
- const hBorder = `${props.width}px solid transparent`;
19427
- if (props.horizontalDirection === "right") {
19428
- borderLeft = hBorder;
19429
- } else {
19430
- borderRight = hBorder;
19431
- }
19432
- const vBorder = `${props.height}px solid ${props.color}`;
19433
- if (props.verticalDirection === "top") {
19434
- borderTop = vBorder;
19435
- } else {
19436
- borderBottom = vBorder;
19437
- }
19438
- return /*#__PURE__*/React.createElement("div", {
19439
- style: {
19440
- display: "block",
19441
- height: 0,
19442
- width: 0,
19443
- position: "absolute",
19444
- left: props.left,
19445
- top: props.top,
19446
- borderLeft: borderLeft,
19447
- borderRight: borderRight,
19448
- borderTop: borderTop,
19449
- borderBottom: borderBottom
19450
- }
19451
- });
19452
- };
19453
- const TooltipArrow = _ref => {
19454
- let {
19455
- position = "relative",
19456
- visibility = "visible",
19457
- left = 100,
19458
- top = 0
19459
- } = _ref,
19460
- props = _objectWithoutPropertiesLoose(_ref, _excluded$4);
19461
- // TODO(aria): Think about adding a box-shadow to the triangle here
19462
- // See http://css-tricks.com/triangle-with-shadow/
19463
-
19464
- //const isRight = (this.props.horizontalDirection === "right");
19465
- const isTop = props.verticalDirection === "top";
19466
- const frontTopOffset = isTop ? 0 : 1;
19467
- const borderTopOffset = isTop ? 0 : -1;
19468
- return /*#__PURE__*/React.createElement("div", {
19469
- style: {
19470
- display: "block",
19471
- position: position,
19472
- visibility: visibility,
19473
- left: left,
19474
- top: top,
19475
- width: props.width + 2,
19476
- height: props.height + 1,
19477
- marginTop: -1,
19478
- marginBottom: -2,
19479
- zIndex: zIndex
19480
- }
19481
- }, /*#__PURE__*/React.createElement(Triangle, {
19482
- horizontalDirection: props.horizontalDirection,
19483
- verticalDirection: props.verticalDirection,
19484
- color: props.border,
19485
- left: 0,
19486
- top: borderTopOffset,
19487
- width: props.width + 2 // one extra for the diagonal
19488
- ,
19489
- height: props.height + 2
19490
- }), /*#__PURE__*/React.createElement(Triangle, {
19491
- horizontalDirection: props.horizontalDirection,
19492
- verticalDirection: props.verticalDirection,
19493
- color: props.color,
19494
- left: 1,
19495
- top: frontTopOffset,
19496
- width: props.width,
19497
- height: props.height
19498
- }));
19499
- };
19500
- const VERTICAL_CORNERS = {
19501
- top: {
19502
- top: "-100%"
19503
- },
19504
- bottom: {
19505
- top: 0
19506
- }
19507
- };
19508
- const HORIZONTAL_CORNERS = {
19509
- left: {
19510
- targetLeft: 0
19511
- },
19512
- right: {
19513
- targetLeft: "100%"
19514
- }
19515
- };
19516
- const HORIZONTAL_ALIGNMNENTS = {
19517
- left: {
19518
- tooltipLeft: 0,
19519
- arrowLeft: arrowSize => 0
19520
- },
19521
- right: {
19522
- tooltipLeft: "-100%",
19523
- arrowLeft: arrowSize => -arrowSize - 2
19524
- }
19525
- };
19526
- /**
19527
- * DEPRECATED! Use Wonder Blocks tooltip instead.
19528
- *
19529
- * A generic tooltip library for React.js
19530
- *
19531
- * ```
19532
- * import Tooltip from "./tooltip";
19533
- * <Tooltip
19534
- * className="class-for-tooltip-contents"
19535
- * horizontalPosition={HoriziontalDirection.Left}
19536
- * horizontalAlign={HoriziontalDirection.Left}
19537
- * verticalPosition={VerticalDirection.Top}
19538
- * arrowSize={10} // arrow size in pixels
19539
- * borderColor="#ccc" // color of the border for the tooltip
19540
- * show={true} // whether the tooltip should currently be visible
19541
- * targetContainerStyle={targetContainerStyle}
19542
- * >
19543
- * <TargetElementOfTheTooltip />
19544
- * <TooltipContents1 />
19545
- * <TooltipContents2 />
19546
- * </Tooltip>
19547
- * ```
19548
- *
19549
- * To show/hide the tooltip, the parent component should call the
19550
- * `.show()` and `.hide()` methods of the tooltip when appropriate.
19551
- * (These are usually set up as handlers of events on the target element.)
19552
- *
19553
- * Notes:
19554
- * `className` should not specify a border; that is handled by `borderColor`
19555
- * so that the arrow and tooltip match
19556
- *
19557
- * ```
19558
- * __,,--``\\
19559
- * _,,-''`` \\ ,
19560
- * '----------_.------'-.___|\__
19561
- * _.--''`` `)__ )__ @\__
19562
- * ( .. ''---/___,,E/__,E'------`
19563
- * `-''`''
19564
- * Here be dragons.
19565
- * ```
19566
- */
19567
- class Tooltip extends React.Component {
19568
- constructor(...args) {
19569
- super(...args);
19570
- this.tooltipContainerRef = /*#__PURE__*/React.createRef();
19571
- this.state = {
19572
- height: null // used for offsetting "top" positioned tooltips
19526
+ autoComplete: "off",
19527
+ autoCorrect: "off",
19528
+ spellCheck: "false"
19573
19529
  };
19574
- }
19575
-
19576
- componentDidMount() {
19577
- this._updateHeight();
19578
- }
19579
- UNSAFE_componentWillReceiveProps() {
19580
- // If the contents have changed, reset our measure of the height
19581
- this.setState({
19582
- height: null
19583
- });
19584
- }
19585
- componentDidUpdate() {
19586
- this._updateHeight();
19587
- }
19588
- _renderToolTipDiv(isTooltipAbove) {
19589
- const settings = Object.assign({}, HORIZONTAL_CORNERS[this.props.horizontalPosition], HORIZONTAL_ALIGNMNENTS[this.props.horizontalAlign], VERTICAL_CORNERS[this.props.verticalPosition]);
19590
- let arrowAbove;
19591
- let arrowBelow;
19592
- if (isTooltipAbove) {
19593
- // We put an absolutely positioned arrow in the correct place
19594
- arrowAbove = /*#__PURE__*/React.createElement(TooltipArrow, {
19595
- verticalDirection: VerticalDirection.Top,
19596
- horizontalDirection: this.props.horizontalAlign,
19597
- position: "absolute",
19598
- color: "white",
19599
- border: this.props.borderColor,
19600
- left: settings.arrowLeft(this.props.arrowSize),
19601
- top: -this.props.arrowSize + 2,
19602
- width: this.props.arrowSize,
19603
- height: this.props.arrowSize,
19604
- zIndex: zIndex
19605
- });
19606
-
19607
- // And we use a visibility: hidden arrow below to shift up the
19608
- // content by the correct amount
19609
- arrowBelow = /*#__PURE__*/React.createElement(TooltipArrow, {
19610
- verticalDirection: VerticalDirection.Top,
19611
- horizontalDirection: this.props.horizontalAlign,
19612
- visibility: "hidden",
19613
- color: "white",
19614
- border: this.props.borderColor,
19615
- left: settings.arrowLeft(this.props.arrowSize),
19616
- top: -1,
19617
- width: this.props.arrowSize,
19618
- height: this.props.arrowSize,
19619
- zIndex: zIndex
19620
- });
19621
- } else {
19622
- arrowAbove = /*#__PURE__*/React.createElement(TooltipArrow, {
19623
- verticalDirection: VerticalDirection.Bottom,
19624
- horizontalDirection: this.props.horizontalAlign,
19625
- color: "white",
19626
- border: this.props.borderColor,
19627
- left: settings.arrowLeft(this.props.arrowSize),
19628
- top: -1,
19629
- width: this.props.arrowSize,
19630
- height: this.props.arrowSize,
19631
- zIndex: zIndex
19632
- });
19633
- arrowBelow = null;
19634
- }
19635
-
19636
- /* A positioned div below the input to be the parent for our
19637
- tooltip */
19638
- return /*#__PURE__*/React.createElement("div", {
19530
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, inputProps), /*#__PURE__*/React.createElement("span", {
19531
+ id: ariaId,
19639
19532
  style: {
19640
- position: "relative",
19641
- height: 0,
19642
- display: this.props.show ? "block" : "none"
19533
+ display: "none"
19643
19534
  }
19644
- }, /*#__PURE__*/React.createElement("div", {
19645
- ref: this.tooltipContainerRef,
19646
- className: "tooltipContainer",
19647
- role: "tooltip",
19648
- style: {
19649
- position: "absolute",
19650
- // height must start out undefined, not null, so that
19651
- // we can measure the actual height with jquery.
19652
- // This is used to position the tooltip with top: -100%
19653
- // when in verticalPosition: "top" mode
19654
- height: this.state.height || undefined,
19655
- left: settings.targetLeft
19656
- }
19657
- }, arrowAbove, /*#__PURE__*/React.createElement("div", {
19658
- className: this.props.className
19659
- // eslint-disable-next-line react/no-string-refs
19660
- ,
19661
- ref: "tooltipContent",
19662
- style: {
19663
- position: "relative",
19664
- top: settings["top"],
19665
- left: settings.tooltipLeft,
19666
- border: "1px solid " + this.props.borderColor,
19667
- boxShadow: "0 1px 3px " + this.props.borderColor,
19668
- zIndex: zIndex - 1
19669
- }
19670
- }, this.props.children.slice(1)), arrowBelow));
19671
- }
19672
- _updateHeight() {
19673
- const tooltipContainer = this.tooltipContainerRef.current;
19674
- const height = (tooltipContainer == null ? void 0 : tooltipContainer.offsetHeight) || 0;
19675
- if (height !== this.state.height) {
19676
- this.setState({
19677
- height
19678
- });
19679
- }
19680
- }
19681
- render() {
19682
- const isTooltipAbove = this.props.verticalPosition === "top";
19683
-
19684
- /* We wrap the entire output in a span so that it displays inline */
19685
- return /*#__PURE__*/React.createElement("span", null, isTooltipAbove && this._renderToolTipDiv(isTooltipAbove), /*#__PURE__*/React.createElement("div", {
19686
- style: this.props.targetContainerStyle
19687
- }, this.props.children[0]), !isTooltipAbove && this._renderToolTipDiv());
19688
- }
19689
- }
19690
-
19691
- // Sorry. // Apology-Oriented-Programming
19692
- Tooltip.defaultProps = {
19693
- className: "",
19694
- arrowSize: 10,
19695
- borderColor: "#ccc",
19696
- verticalPosition: VerticalDirection.Bottom,
19697
- horizontalPosition: HorizontalDirection.Left,
19698
- horizontalAlign: HorizontalDirection.Left,
19699
- targetContainerStyle: {}
19700
- };
19701
-
19702
- const {
19703
- captureScratchpadTouchStart: captureScratchpadTouchStart$2
19704
- } = Util;
19705
- class InputWithExamples extends React.Component {
19706
- constructor(...args) {
19707
- super(...args);
19708
- this.state = {
19709
- focused: false,
19710
- showExamples: false
19711
- };
19712
- this._getUniqueId = () => {
19713
- return `input-with-examples-${btoa(this.props.id).replace(/=/g, "")}`;
19714
- };
19715
- this._getInputClassName = () => {
19716
- // Otherwise, we need to add these INPUT and FOCUSED tags here.
19717
- let className = ClassNames.INPUT;
19718
- if (this.state.focused) {
19719
- className += " " + ClassNames.FOCUSED;
19720
- }
19721
- if (this.props.className) {
19722
- className += " " + this.props.className;
19723
- }
19724
- return className;
19725
- };
19726
- this._renderInput = () => {
19727
- const id = this._getUniqueId();
19728
- const ariaId = `aria-for-${id}`;
19729
- // Generate text from a known set of format options that will read well in a screen reader
19730
- const examplesAria = this.props.examples.length === 7 ? "" : `${this.props.examples[0]}
19731
- ${this.props.examples.slice(1).join(", or\n")}`
19732
- // @ts-expect-error TS2550: Property replaceAll does not exist on type string.
19733
- .replaceAll("*", "").replaceAll("$", "").replaceAll("\\ \\text{pi}", " pi").replaceAll("\\ ", " and ");
19734
- const inputProps = {
19735
- id: id,
19736
- "aria-describedby": ariaId,
19737
- ref: "input",
19738
- className: this._getInputClassName(),
19739
- labelText: this.props.labelText,
19740
- value: this.props.value,
19741
- onFocus: this._handleFocus,
19742
- onBlur: this._handleBlur,
19743
- disabled: this.props.disabled,
19744
- style: this.props.style,
19745
- onChange: this.props.onChange,
19746
- onTouchStart: captureScratchpadTouchStart$2,
19747
- autoCapitalize: "off",
19748
- autoComplete: "off",
19749
- autoCorrect: "off",
19750
- spellCheck: "false"
19751
- };
19752
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(TextInput, inputProps), /*#__PURE__*/React.createElement("span", {
19753
- id: ariaId,
19754
- style: {
19755
- display: "none"
19756
- }
19757
- }, examplesAria));
19758
- };
19759
- this._handleFocus = () => {
19760
- this.props.onFocus();
19761
- this.setState({
19762
- focused: true,
19763
- showExamples: true
19764
- });
19765
- };
19766
- this.show = () => {
19767
- this.setState({
19768
- showExamples: true
19769
- });
19770
- };
19771
- this.hide = () => {
19772
- this.setState({
19773
- showExamples: false
19774
- });
19775
- };
19776
- this._handleBlur = () => {
19777
- this.props.onBlur();
19778
- this.setState({
19779
- focused: false,
19780
- showExamples: false
19781
- });
19782
- };
19783
- this.focus = () => {
19784
- // eslint-disable-next-line react/no-string-refs
19785
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
19786
- this.refs.input.focus();
19787
- };
19788
- this.blur = () => {
19789
- // eslint-disable-next-line react/no-string-refs
19790
- // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
19791
- this.refs.input.blur();
19792
- };
19793
- this.handleChange = e => {
19794
- this.props.onChange(e.target.value);
19795
- };
19796
- }
19797
- render() {
19798
- const input = this._renderInput();
19799
- const examplesContent = this.props.examples.length <= 2 ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list"
19800
- : this.props.examples // 2 or more items should display as a list
19801
- .map((example, index) => {
19802
- // If the first example is bold, then it is most likely a heading/leading text.
19803
- // So, it shouldn't be part of the list.
19804
- return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
19805
- }).join("\n");
19806
- const showExamples = this.props.shouldShowExamples && this.state.showExamples;
19807
- return /*#__PURE__*/React.createElement(Tooltip, {
19808
- className: "perseus-formats-tooltip preview-measure",
19809
- horizontalPosition: HorizontalDirection.Left,
19810
- horizontalAlign: HorizontalDirection.Left,
19811
- verticalPosition: VerticalDirection.Bottom,
19812
- arrowSize: 10,
19813
- borderColor: "#ccc",
19814
- show: showExamples
19815
- }, input, /*#__PURE__*/React.createElement("div", {
19816
- id: this._getUniqueId()
19535
+ }, examplesAria));
19536
+ };
19537
+ const renderTooltipContent = () => {
19538
+ return /*#__PURE__*/React.createElement(TooltipContent, null, /*#__PURE__*/React.createElement("div", {
19539
+ id: id,
19540
+ className: "input-with-examples-tooltip"
19817
19541
  }, /*#__PURE__*/React.createElement(Renderer, {
19818
19542
  content: examplesContent,
19819
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "input-with-examples"),
19820
- strings: this.context.strings
19543
+ linterContext: PerseusLinter.pushContextStack(linterContext, "input-with-examples"),
19544
+ strings: context.strings
19821
19545
  })));
19822
- }
19823
- }
19824
- InputWithExamples.contextType = PerseusI18nContext;
19825
- InputWithExamples.defaultProps = {
19826
- shouldShowExamples: true,
19827
- onFocus: function () {},
19828
- onBlur: function () {},
19829
- disabled: false,
19830
- linterContext: PerseusLinter.linterContextDefault,
19831
- className: ""
19832
- };
19833
-
19834
- const _excluded$3 = ["keypadElement", "onFocus", "value"];
19835
- class SimpleKeypadInput extends React.Component {
19836
- constructor(...args) {
19837
- super(...args);
19838
- this._isMounted = false;
19839
- }
19840
- componentDidMount() {
19841
- // TODO(scottgrant): This is a hack to remove the deprecated call to
19842
- // this.isMounted() but is still considered an anti-pattern.
19843
- this._isMounted = true;
19844
- }
19845
- componentWillUnmount() {
19846
- this._isMounted = false;
19847
- }
19848
- focus() {
19849
- // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'.
19850
- this.refs.input.focus(); // eslint-disable-line react/no-string-refs
19851
- }
19852
-
19853
- blur() {
19854
- var _this$refs$input;
19855
- // eslint-disable-next-line react/no-string-refs
19856
- // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
19857
- if (typeof ((_this$refs$input = this.refs.input) == null ? void 0 : _this$refs$input.blur) === "function") {
19858
- var _this$refs$input2;
19859
- // eslint-disable-next-line react/no-string-refs
19860
- // @ts-expect-error - TS2339 - Property 'blur' does not exist on type 'ReactInstance'.
19861
- (_this$refs$input2 = this.refs.input) == null || _this$refs$input2.blur();
19862
- }
19863
- }
19864
- getValue() {
19865
- return this.props.value;
19866
- }
19867
- render() {
19868
- const _this = this;
19869
- // Intercept the `onFocus` prop, as we need to configure the keypad
19870
- // before continuing with the default focus logic for Perseus inputs.
19871
- // Intercept the `value` prop so as to map `null` to the empty string,
19872
- // as the `KeypadInput` does not support `null` values.
19873
- const _this$props = _this.props,
19874
- {
19875
- keypadElement,
19876
- onFocus,
19877
- value
19878
- } = _this$props,
19879
- rest = _objectWithoutPropertiesLoose(_this$props, _excluded$3);
19880
- return (
19881
- /*#__PURE__*/
19882
- // @ts-expect-error - TS2769 - No overload matches this call.
19883
- React.createElement(KeypadInput
19884
- // eslint-disable-next-line react/no-string-refs
19885
- , _extends({
19886
- ref: "input",
19887
- keypadElement: keypadElement,
19888
- onFocus: () => {
19889
- if (keypadElement) {
19890
- keypadElement.configure({
19891
- keypadType: KeypadType.FRACTION
19892
- }, () => {
19893
- if (_this._isMounted) {
19894
- onFocus == null || onFocus();
19895
- }
19896
- });
19897
- } else {
19898
- onFocus == null || onFocus();
19899
- }
19900
- },
19901
- value: value == null ? "" : "" + value
19902
- }, rest))
19903
- );
19904
- }
19905
- }
19906
-
19907
- // @ts-expect-error - TS2339 - Property 'propTypes' does not exist on type 'typeof SimpleKeypadInput'.
19908
- SimpleKeypadInput.propTypes = {
19909
- keypadElement: keypadElementPropType,
19910
- onFocus: PropTypes.func,
19911
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
19912
- };
19913
-
19914
- const getPromptJSON$s = (renderProps, userInput) => {
19915
- return {
19916
- type: "input-number",
19917
- options: {
19918
- simplify: renderProps.simplify,
19919
- answerType: renderProps.answerType
19920
- },
19921
- userInput: {
19922
- value: userInput.currentValue
19923
- }
19924
19546
  };
19925
- };
19547
+ // Display the examples as a string when there are less than or equal to 2 examples.
19548
+ // Otherwise, display the examples as a list.
19549
+ const examplesContent = props.examples.length <= 2 ? props.examples.join(" ") : props.examples.map((example, index) => {
19550
+ return index === 0 && example.startsWith("**") ? `${example}\n` : `- ${example}`;
19551
+ }).join("\n");
19552
+
19553
+ // Display the examples when they are enabled (shouldShowExamples) and the input is focused.
19554
+ const showExamplesTooltip = shouldShowExamples && inputFocused;
19555
+ return /*#__PURE__*/React.createElement(Tooltip, {
19556
+ content: renderTooltipContent(),
19557
+ opened: showExamplesTooltip,
19558
+ placement: "bottom"
19559
+ }, renderInput());
19560
+ });
19926
19561
 
19927
- const formExamples$1 = {
19562
+ const formExamples = {
19928
19563
  integer: function (options, strings) {
19929
19564
  return strings.integerExample;
19930
19565
  },
@@ -20027,7 +19662,7 @@ class InputNumber extends React.Component {
20027
19662
  } = this.context;
20028
19663
  const type = this.props.answerType;
20029
19664
  const forms = inputNumberAnswerTypes[type].forms.split(/\s*,\s*/);
20030
- const examples = _.map(forms, form => formExamples$1[form](this.props, strings));
19665
+ const examples = _.map(forms, form => formExamples[form](this.props, strings));
20031
19666
  return [strings.yourAnswer].concat(examples);
20032
19667
  }
20033
19668
  render() {
@@ -20152,7 +19787,11 @@ const getPromptJSON$r = (renderProps, userInput) => {
20152
19787
  };
20153
19788
  };
20154
19789
 
20155
- const formExamples = {
19790
+ /**
19791
+ * The full list of available strings for the numeric input widget,
19792
+ * based on whether the Content Creator has specified that the answer must be simplified.
19793
+ */
19794
+ const NumericExampleStrings = {
20156
19795
  integer: (form, strings) => strings.integerExample,
20157
19796
  proper: (form, strings) => form.simplify === "optional" ? strings.properExample : strings.simplifiedProperExample,
20158
19797
  improper: (form, strings) => form.simplify === "optional" ? strings.improperExample : strings.simplifiedImproperExample,
@@ -20160,38 +19799,200 @@ const formExamples = {
20160
19799
  decimal: (form, strings) => strings.decimalExample,
20161
19800
  pi: (form, strings) => strings.piExample
20162
19801
  };
19802
+
19803
+ /**
19804
+ * Generates the specific set of examples for the current question.
19805
+ * This string is shown as examples to the user in a tooltip.
19806
+ */
19807
+ const generateExamples = (answerForms, strings) => {
19808
+ // If the Content Creator has not specified any answer forms,
19809
+ // we do not need to show any examples.
19810
+ if (answerForms.length === 0) {
19811
+ return [];
19812
+ }
19813
+
19814
+ // Generate a list of the unique answer forms.
19815
+ const uniqueForms = getUniqueAnswerForms(answerForms);
19816
+
19817
+ // Generate the example strings for each unique form.
19818
+ const examples = uniqueForms.map(form => {
19819
+ return NumericExampleStrings[form.name](form, strings);
19820
+ });
19821
+
19822
+ // Add the "Your answer" string to the beginning of the examples list.
19823
+ return [strings.yourAnswer].concat(examples);
19824
+ };
19825
+
19826
+ /**
19827
+ * Determines whether to show examples of how to input the various supported answer forms.
19828
+ * We do not show examples if all forms are accepted or if no forms are accepted.
19829
+ */
19830
+ const shouldShowExamples = answerForms => {
19831
+ // If the Content Creator has not specified any answer forms,
19832
+ // we do not need to show any examples.
19833
+ if (answerForms.length === 0) {
19834
+ return false;
19835
+ }
19836
+
19837
+ // Generate a list of the unique names of the selected answer forms.
19838
+ const answerFormNames = getUniqueAnswerForms(answerForms).map(form => form.name);
19839
+
19840
+ // If all forms are accepted, we do not need to show any examples.
19841
+ const allFormsAccepted = answerFormNames.length >= Object.keys(NumericExampleStrings).length;
19842
+ return !allFormsAccepted;
19843
+ };
19844
+
19845
+ /**
19846
+ * uniqueAnswerForms takes a list of answer forms and returns a list of unique
19847
+ * answer forms. This is useful for ensuring that we don't show duplicate examples
19848
+ * to the user.
19849
+ */
19850
+ const getUniqueAnswerForms = function getUniqueAnswerForms(list) {
19851
+ // We use a Set to keep track of the forms we've already seen.
19852
+ const foundForms = new Set();
19853
+ return list.filter(form => {
19854
+ // If we've already seen this form, skip it.
19855
+ if (foundForms.has(form.name)) {
19856
+ return false;
19857
+ }
19858
+ // Otherwise, add it to the set and return true.
19859
+ foundForms.add(form.name);
19860
+ return true;
19861
+ });
19862
+ };
19863
+
19864
+ /**
19865
+ * Takes a list of lists of answer forms, and returns a list of the forms
19866
+ * in each of these lists in the same order that they're listed in the
19867
+ * `formExamples` forms from above.
19868
+ */
19869
+ const unionAnswerForms = function unionAnswerForms(answerFormsList) {
19870
+ // Pull out all of the forms from the different lists.
19871
+ const allForms = answerFormsList.flat();
19872
+ // Pull out the unique forms using getUniqueAnswerForms.
19873
+ const uniqueForms = getUniqueAnswerForms(allForms);
19874
+ // Sort them by the order they appear in the `formExamples` list.
19875
+ const formExampleKeys = Object.keys(NumericExampleStrings);
19876
+ return uniqueForms.sort((a, b) => {
19877
+ return formExampleKeys.indexOf(a.name) - formExampleKeys.indexOf(b.name);
19878
+ });
19879
+ };
19880
+
19881
+ /**
19882
+ * The NumericInputComponent is a child component of the NumericInput class
19883
+ * component. It is responsible for rendering the UI elements of the Numeric
19884
+ * Input widget.
19885
+ */
19886
+ const NumericInputComponent = /*#__PURE__*/forwardRef((props, ref) => {
19887
+ const context = useContext(PerseusI18nContext);
19888
+ const inputRef = useRef(null);
19889
+ const [isFocused, setIsFocused] = useState(false);
19890
+
19891
+ // Pass the focus and blur methods to the Numeric Input Class component
19892
+ useImperativeHandle(ref, () => ({
19893
+ current: inputRef.current,
19894
+ focus: () => {
19895
+ if (inputRef.current) {
19896
+ var _inputRef$current;
19897
+ (_inputRef$current = inputRef.current) == null || _inputRef$current.focus();
19898
+ setIsFocused(true);
19899
+ }
19900
+ },
19901
+ blur: () => {
19902
+ if (inputRef.current) {
19903
+ var _inputRef$current2;
19904
+ (_inputRef$current2 = inputRef.current) == null || _inputRef$current2.blur();
19905
+ setIsFocused(false);
19906
+ }
19907
+ }
19908
+ }));
19909
+ const handleChange = (newValue, cb) => {
19910
+ props.onChange({
19911
+ currentValue: newValue
19912
+ }, cb);
19913
+ props.trackInteraction();
19914
+ };
19915
+ const handleFocus = () => {
19916
+ props.onFocus([]);
19917
+ setIsFocused(true);
19918
+ };
19919
+ const handleBlur = () => {
19920
+ props.onBlur([]);
19921
+ setIsFocused(false);
19922
+ };
19923
+
19924
+ // Styles for the InputWithExamples
19925
+ const styles = StyleSheet.create({
19926
+ inputWithExamples: {
19927
+ borderRadius: "3px",
19928
+ borderWidth: isFocused ? "2px" : "1px",
19929
+ display: "inline-block",
19930
+ fontFamily: `Symbola, "Times New Roman", serif`,
19931
+ fontSize: "18px",
19932
+ height: "32px",
19933
+ lineHeight: "18px",
19934
+ padding: isFocused ? "4px" : "4px 5px",
19935
+ textAlign: props.rightAlign ? "right" : "left",
19936
+ width: props.size === "small" ? 40 : 80
19937
+ }
19938
+ });
19939
+
19940
+ // (mobile-only) If the custom keypad is enabled, use the SimpleKeypadInput component
19941
+ if (props.apiOptions.customKeypad) {
19942
+ const alignmentClass = props.rightAlign ? "perseus-input-right-align" : undefined;
19943
+ return /*#__PURE__*/React.createElement("div", {
19944
+ className: alignmentClass
19945
+ }, /*#__PURE__*/React.createElement(SimpleKeypadInput, {
19946
+ ref: inputRef,
19947
+ value: props.currentValue,
19948
+ keypadElement: props.keypadElement,
19949
+ onChange: handleChange,
19950
+ onFocus: handleFocus,
19951
+ onBlur: handleBlur
19952
+ }));
19953
+ }
19954
+ // (desktop-only) Otherwise, use the InputWithExamples component
19955
+ return /*#__PURE__*/React.createElement(InputWithExamples, {
19956
+ ref: inputRef,
19957
+ value: props.currentValue,
19958
+ onChange: handleChange,
19959
+ labelText: props.labelText,
19960
+ examples: generateExamples(props.answerForms, context.strings),
19961
+ shouldShowExamples: shouldShowExamples(props.answerForms),
19962
+ onFocus: handleFocus,
19963
+ onBlur: handleBlur,
19964
+ id: props.widgetId,
19965
+ disabled: props.apiOptions.readOnly,
19966
+ style: styles.inputWithExamples
19967
+ });
19968
+ });
19969
+
19970
+ const _excluded$3 = ["answers"];
19971
+
19972
+ /**
19973
+ * The NumericInput widget is a numeric input field that supports a variety of
19974
+ * answer forms, including integers, decimals, fractions, and mixed numbers.
19975
+ *
19976
+ * [Jan 2025] We're currenly migrating from class-based components to functional
19977
+ * components. While we cannot fully migrate this component yet, we can start
19978
+ * by using the functional component for the rendering the UI of the widget.
19979
+ */
20163
19980
  class NumericInput extends React.Component {
20164
19981
  constructor(...args) {
20165
19982
  super(...args);
20166
- this.inputRef = void 0;
20167
- this.state = {
20168
- // keeps track of the other set of values when switching
20169
- // between 0 and finite solutions
20170
- previousValues: [""],
20171
- isFocused: false
20172
- };
20173
- this.shouldShowExamples = () => {
20174
- var _this$props$answerFor, _this$props$answerFor2;
20175
- const noFormsAccepted = ((_this$props$answerFor = this.props.answerForms) == null ? void 0 : _this$props$answerFor.length) === 0;
20176
- // To check if all answer forms are accepted, we must first
20177
- // find the *names* of all accepted forms, and see if they are
20178
- // all present, ignoring duplicates
20179
- const answerFormNames = _.uniq((_this$props$answerFor2 = this.props.answerForms) == null ? void 0 : _this$props$answerFor2.map(form => form.name));
20180
- const allFormsAccepted = answerFormNames.length >= Object.keys(formExamples).length;
20181
- return !noFormsAccepted && !allFormsAccepted;
20182
- };
19983
+ this.inputRef = /*#__PURE__*/React.createRef();
20183
19984
  this.focus = () => {
20184
- var _this$inputRef;
20185
- (_this$inputRef = this.inputRef) == null || _this$inputRef.focus();
19985
+ var _this$inputRef$curren;
19986
+ (_this$inputRef$curren = this.inputRef.current) == null || _this$inputRef$curren.focus();
20186
19987
  return true;
20187
19988
  };
20188
19989
  this.focusInputPath = () => {
20189
- var _this$inputRef2;
20190
- (_this$inputRef2 = this.inputRef) == null || _this$inputRef2.focus();
19990
+ var _this$inputRef$curren2;
19991
+ (_this$inputRef$curren2 = this.inputRef.current) == null || _this$inputRef$curren2.focus();
20191
19992
  };
20192
19993
  this.blurInputPath = () => {
20193
- var _this$inputRef3;
20194
- (_this$inputRef3 = this.inputRef) == null || _this$inputRef3.blur();
19994
+ var _this$inputRef$curren3;
19995
+ (_this$inputRef$curren3 = this.inputRef.current) == null || _this$inputRef$curren3.blur();
20195
19996
  };
20196
19997
  this.getInputPaths = () => {
20197
19998
  // The widget itself is an input, so we return a single empty list to
@@ -20199,34 +20000,14 @@ class NumericInput extends React.Component {
20199
20000
  /* c8 ignore next */
20200
20001
  return [[]];
20201
20002
  };
20003
+ /**
20004
+ * Sets the value of the input at the given path.
20005
+ */
20202
20006
  this.setInputValue = (path, newValue, cb) => {
20203
- /* c8 ignore next */
20204
20007
  this.props.onChange({
20205
20008
  currentValue: newValue
20206
20009
  }, cb);
20207
20010
  };
20208
- this.handleChange = (newValue, cb) => {
20209
- this.props.onChange({
20210
- currentValue: newValue
20211
- }, cb);
20212
- this.props.trackInteraction();
20213
- };
20214
- this._handleFocus = () => {
20215
- this.props.onFocus([]);
20216
- this.setState(currentState => {
20217
- return _extends({}, currentState, {
20218
- isFocused: true
20219
- });
20220
- });
20221
- };
20222
- this._handleBlur = () => {
20223
- this.props.onBlur([]);
20224
- this.setState(currentState => {
20225
- return _extends({}, currentState, {
20226
- isFocused: false
20227
- });
20228
- });
20229
- };
20230
20011
  }
20231
20012
  static getUserInputFromProps(props) {
20232
20013
  return {
@@ -20234,97 +20015,26 @@ class NumericInput extends React.Component {
20234
20015
  };
20235
20016
  }
20236
20017
  /**
20237
- * Generates a string that demonstrates how to input the various supported
20238
- * answer forms.
20018
+ * Returns the value the user has currently input for this widget.
20239
20019
  */
20240
- examples() {
20241
- var _this$props$answerFor3;
20242
- // if the set of specified forms are empty, allow all forms
20243
- const forms = ((_this$props$answerFor3 = this.props.answerForms) == null ? void 0 : _this$props$answerFor3.length) !== 0 ? this.props.answerForms : Object.keys(formExamples).map(name => {
20244
- return {
20245
- name: name,
20246
- simplify: "required"
20247
- };
20248
- });
20249
- let examples = _.map(forms, form => {
20250
- return formExamples[form.name](form, this.context.strings);
20251
- });
20252
- // Ensure no duplicate tooltip text from simplified and unsimplified
20253
- // versions of the same format
20254
- examples = _.uniq(examples);
20255
- return [this.context.strings.yourAnswer].concat(examples);
20256
- }
20257
20020
  getUserInput() {
20258
20021
  return NumericInput.getUserInputFromProps(this.props);
20259
20022
  }
20023
+
20024
+ /**
20025
+ * Returns the JSON representation of the prompt for this widget.
20026
+ * This is used by the AI to determine the prompt for the widget.
20027
+ */
20260
20028
  getPromptJSON() {
20261
20029
  return getPromptJSON$r(this.props, this.getUserInput());
20262
20030
  }
20263
20031
  render() {
20264
- let labelText = this.props.labelText;
20265
- if (labelText == null || labelText === "") {
20266
- labelText = this.context.strings.yourAnswerLabel;
20267
- }
20268
-
20269
- // To right align a custom keypad we need to wrap it.
20270
- const maybeRightAlignKeypadInput = keypadInput => {
20271
- return this.props.rightAlign ? /*#__PURE__*/React.createElement("div", {
20272
- className: "perseus-input-right-align"
20273
- }, keypadInput) : keypadInput;
20274
- };
20275
- if (this.props.apiOptions.customKeypad) {
20276
- // TODO(charlie): Support "Review Mode".
20277
- return maybeRightAlignKeypadInput( /*#__PURE__*/React.createElement(SimpleKeypadInput, {
20278
- ref: ref => this.inputRef = ref,
20279
- value: this.props.currentValue,
20280
- keypadElement: this.props.keypadElement,
20281
- onChange: this.handleChange,
20282
- onFocus: this._handleFocus,
20283
- onBlur: this._handleBlur
20284
- }));
20285
- }
20286
-
20287
- // Note: This is _very_ similar to what `input-number.jsx` does. If
20288
- // you modify this, double-check if you also need to modify that
20289
- // component.
20290
- const styles = StyleSheet.create({
20291
- input: {
20292
- borderRadius: "3px",
20293
- borderWidth: this.state.isFocused ? "2px" : "1px",
20294
- display: "inline-block",
20295
- fontFamily: `Symbola, "Times New Roman", serif`,
20296
- fontSize: "18px",
20297
- height: "32px",
20298
- lineHeight: "18px",
20299
- padding: this.state.isFocused ? "4px" : "4px 5px",
20300
- // account for added focus border thickness
20301
- textAlign: this.props.rightAlign ? "right" : "left",
20302
- width: this.props.size === "small" ? 40 : 80
20303
- }
20304
- });
20305
- return /*#__PURE__*/React.createElement(InputWithExamples, {
20306
- ref: ref => this.inputRef = ref,
20307
- value: this.props.currentValue,
20308
- onChange: this.handleChange,
20309
- labelText: labelText,
20310
- examples: this.examples(),
20311
- shouldShowExamples: this.shouldShowExamples(),
20312
- onFocus: this._handleFocus,
20313
- onBlur: this._handleBlur,
20314
- id: this.props.widgetId,
20315
- disabled: this.props.apiOptions.readOnly,
20316
- style: styles.input
20317
- });
20032
+ return /*#__PURE__*/React.createElement(NumericInputComponent, _extends({}, this.props, {
20033
+ ref: this.inputRef
20034
+ }));
20318
20035
  }
20319
20036
  }
20320
-
20321
- // TODO(thomas): Currently we receive a list of lists of acceptable answer types
20322
- // and union them down into a single set. It's worth considering whether it
20323
- // wouldn't make more sense to have a single set of acceptable answer types for
20324
- // a given *problem* rather than for each possible [correct/wrong] *answer*.
20325
- // When should two answers to a problem take different answer types?
20326
- // See D27790 for more discussion.
20327
- NumericInput.contextType = PerseusI18nContext;
20037
+ // Transforms the widget options to the props used to render the widget.
20328
20038
  NumericInput.defaultProps = {
20329
20039
  currentValue: "",
20330
20040
  size: "normal",
@@ -20335,52 +20045,23 @@ NumericInput.defaultProps = {
20335
20045
  labelText: "",
20336
20046
  linterContext: linterContextDefault
20337
20047
  };
20338
- const unionAnswerForms = function unionAnswerForms(answerFormsList) {
20339
- // Takes a list of lists of answer forms, and returns a list of the forms
20340
- // in each of these lists in the same order that they're listed in the
20341
- // `formExamples` forms from above.
20342
-
20343
- // uniqueBy takes a list of elements and a function which compares whether
20344
- // two elements are equal, and returns a list of unique elements. This is
20345
- // just a helper function here, but works generally.
20346
- const uniqueBy = function uniqueBy(list, iteratee) {
20347
- // @ts-expect-error - TS2347 - Untyped function calls may not accept type arguments.
20348
- return list.reduce((uniqueList, element) => {
20349
- // For each element, decide whether it's already in the list of
20350
- // unique items.
20351
- const inList = _.find(uniqueList, iteratee.bind(null, element));
20352
- if (inList) {
20353
- return uniqueList;
20354
- }
20355
- return uniqueList.concat([element]);
20356
- }, []);
20357
- };
20358
-
20359
- // Pull out all of the forms from the different lists.
20360
- const allForms = answerFormsList.flat();
20361
- // Pull out the unique forms using uniqueBy.
20362
- const uniqueForms = uniqueBy(allForms, _.isEqual);
20363
- // Sort them by the order they appear in the `formExamples` list.
20364
- const formExampleKeys = Object.keys(formExamples);
20365
- return _.sortBy(uniqueForms, form => {
20366
- return formExampleKeys.indexOf(form.name);
20367
- });
20368
- };
20369
20048
  const propsTransform = function propsTransform(widgetOptions) {
20370
- const rendererProps = _.extend(_.omit(widgetOptions, "answers"), {
20371
- answerForms: unionAnswerForms(
20372
- // Pull out the name of each form and whether that form has
20373
- // required simplification.
20374
- widgetOptions.answers.map(answer => {
20375
- // @ts-expect-error - TS2345 - Argument of type 'readonly MathFormat[] | undefined' is not assignable to parameter of type 'Collection<any>'.
20376
- return _.map(answer.answerForms, form => {
20377
- return {
20378
- simplify: answer.simplify,
20379
- name: form
20380
- };
20381
- });
20382
- }))
20383
- });
20049
+ // Omit the answers from the widget options since they are
20050
+ // not needed for rendering the widget.
20051
+ const _widgetOptions$answer = _extends({}, widgetOptions, {
20052
+ answerForms: unionAnswerForms(
20053
+ // Filter out the correct answers and map them to the answer forms
20054
+ // so that we can generate the examples for the widget.
20055
+ widgetOptions.answers.filter(answer => answer.status === "correct").map(answer => {
20056
+ return (answer.answerForms || []).map(form => {
20057
+ return {
20058
+ simplify: answer.simplify,
20059
+ name: form
20060
+ };
20061
+ });
20062
+ }))
20063
+ }),
20064
+ rendererProps = _objectWithoutPropertiesLoose(_widgetOptions$answer, _excluded$3);
20384
20065
  return rendererProps;
20385
20066
  };
20386
20067
  var NumericInput$1 = {
@@ -20395,23 +20076,11 @@ var NumericInput$1 = {
20395
20076
  getOneCorrectAnswerFromRubric(rubric) {
20396
20077
  const correctAnswers = rubric.answers.filter(answer => answer.status === "correct");
20397
20078
  const answerStrings = correctAnswers.map(answer => {
20398
- // Figure out how this answer is supposed to be
20399
- // displayed
20400
- let format = "decimal";
20401
- if (answer.answerForms && answer.answerForms[0]) {
20402
- // NOTE(johnsullivan): This isn't exactly ideal, but
20403
- // it does behave well for all the currently known
20404
- // problems. See D14742 for some discussion on
20405
- // alternate strategies.
20406
- format = answer.answerForms[0];
20407
- }
20408
-
20409
- // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'.
20079
+ // Either get the first answer form or default to decimal
20080
+ const format = answer.answerForms && answer.answerForms[0] ? answer.answerForms[0] : "decimal";
20410
20081
  let answerString = KhanMath.toNumericString(answer.value, format);
20411
20082
  if (answer.maxError) {
20412
- answerString += " \u00B1 " +
20413
- // @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'MathFormat | undefined'.
20414
- KhanMath.toNumericString(answer.maxError, format);
20083
+ answerString += " \u00B1 " + KhanMath.toNumericString(answer.maxError, format);
20415
20084
  }
20416
20085
  return answerString;
20417
20086
  });
@@ -21215,7 +20884,7 @@ class HighlightTooltip extends React.PureComponent {
21215
20884
  style: style,
21216
20885
  onMouseEnter: this.props.onMouseEnter,
21217
20886
  onMouseLeave: this.props.onMouseLeave
21218
- }, /*#__PURE__*/React.createElement(Tooltip$1, {
20887
+ }, /*#__PURE__*/React.createElement(Tooltip, {
21219
20888
  content: content,
21220
20889
  opened: true
21221
20890
  }, /*#__PURE__*/React.createElement("div", null)))
@@ -22938,6 +22607,7 @@ const BASE_ICON_SIZE = 10;
22938
22607
  // If you pass in an object you need to specify an SVG path to
22939
22608
  // render along with the width and height of the path (to ensure
22940
22609
  // that it gets scaled correctly).
22610
+
22941
22611
  /**
22942
22612
  * An SVG Icon
22943
22613
  *
@@ -24023,7 +23693,6 @@ const styles$o = StyleSheet.create({
24023
23693
  paddingLeft: phoneMargin
24024
23694
  // paddingRight is handled by responsiveFieldset
24025
23695
  },
24026
-
24027
23696
  responsiveFieldset: {
24028
23697
  paddingRight: phoneMargin,
24029
23698
  minWidth: "auto"
@@ -24369,11 +24038,6 @@ Radio$1.defaultProps = {
24369
24038
  showSolutions: "none"
24370
24039
  };
24371
24040
 
24372
- const {
24373
- shuffle: shuffle$2,
24374
- random
24375
- } = Util;
24376
-
24377
24041
  // Transforms the choices for display.
24378
24042
  const _choiceTransform = (widgetOptions, strings, problemNum) => {
24379
24043
  const _maybeRandomize = function _maybeRandomize(array) {
@@ -24384,7 +24048,7 @@ const _choiceTransform = (widgetOptions, strings, problemNum) => {
24384
24048
  // or if rendered within an article. This results in a predictable
24385
24049
  // shuffle order. To avoid this we use a random seed when `problemNum`
24386
24050
  // is `undefined`.
24387
- return widgetOptions.randomize ? shuffle$2(array, randomSeed != null ? randomSeed : 0) : array;
24051
+ return widgetOptions.randomize ? shuffle(array, randomSeed != null ? randomSeed : 0) : array;
24388
24052
  };
24389
24053
  const _addNoneOfAbove = function _addNoneOfAbove(choices) {
24390
24054
  let noneOfTheAbove = null;
@@ -24434,7 +24098,7 @@ const _choiceTransform = (widgetOptions, strings, problemNum) => {
24434
24098
  // 1) we randomize the order first
24435
24099
  _maybeRandomize(choices)));
24436
24100
  };
24437
- const transform = (widgetOptions, strings, problemNum) => {
24101
+ const transform$1 = (widgetOptions, strings, problemNum) => {
24438
24102
  const choices = _choiceTransform(widgetOptions, strings, problemNum);
24439
24103
  const numCorrect = _.reduce(widgetOptions.choices, function (memo, choice) {
24440
24104
  return choice.correct ? memo + 1 : memo;
@@ -24460,8 +24124,8 @@ var Radio = {
24460
24124
  displayName: "Radio / Multiple choice",
24461
24125
  accessible: true,
24462
24126
  widget: Radio$1,
24463
- transform: transform,
24464
- staticTransform: transform,
24127
+ transform: transform$1,
24128
+ staticTransform: transform$1,
24465
24129
  version: radioLogic.version,
24466
24130
  propUpgrades: radioLogic.widgetOptionsUpgrades,
24467
24131
  isLintable: true
@@ -24614,8 +24278,7 @@ class Categorizer extends React.Component {
24614
24278
  const isMobile = this.props.apiOptions.isMobile;
24615
24279
  let indexedItems = this.props.items.map((item, n) => [item, n]);
24616
24280
  if (this.props.randomizeItems) {
24617
- // @ts-expect-error - TS4104 - The type 'readonly (string | number)[][]' is 'readonly' and cannot be assigned to the mutable type '(string | number)[][]'. | TS2345 - Argument of type 'number | null | undefined' is not assignable to parameter of type 'number | RNG'.
24618
- indexedItems = Util.shuffle(indexedItems, this.props.problemNum);
24281
+ indexedItems = shuffle(indexedItems, this.props.problemNum);
24619
24282
  }
24620
24283
  const table = /*#__PURE__*/React.createElement("table", {
24621
24284
  className: "categorizer-table " + css(styles$n.mobileTable)
@@ -24641,9 +24304,7 @@ class Categorizer extends React.Component {
24641
24304
  const uniqueId = self.state.uniqueId + "_" + itemNum;
24642
24305
  return /*#__PURE__*/React.createElement("tr", {
24643
24306
  key: itemNum
24644
- }, /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement(Renderer
24645
- // @ts-expect-error - TS2322 - Type 'string | number' is not assignable to type 'string | undefined'.
24646
- , {
24307
+ }, /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement(Renderer, {
24647
24308
  content: item,
24648
24309
  linterContext: this.props.linterContext,
24649
24310
  strings: this.context.strings
@@ -25035,6 +24696,7 @@ var Definition$1 = {
25035
24696
 
25036
24697
  // There are no required props, but this component can receive props from any
25037
24698
  // deprecated widget
24699
+
25038
24700
  class DeprecatedStandin extends React__default.Component {
25039
24701
  constructor(...args) {
25040
24702
  super(...args);
@@ -25161,20 +24823,20 @@ Dropdown.defaultProps = {
25161
24823
  placeholder: "",
25162
24824
  apiOptions: ApiOptions.defaults
25163
24825
  };
25164
- const optionsTransform = widgetOptions => {
24826
+ function transform(widgetOptions) {
25165
24827
  return {
25166
24828
  placeholder: widgetOptions.placeholder,
25167
24829
  visibleLabel: widgetOptions.visibleLabel,
25168
24830
  ariaLabel: widgetOptions.ariaLabel,
25169
24831
  choices: widgetOptions.choices.map(choice => choice.content)
25170
24832
  };
25171
- };
24833
+ }
25172
24834
  var Dropdown$1 = {
25173
24835
  name: "dropdown",
25174
24836
  displayName: "Drop down",
25175
24837
  accessible: true,
25176
24838
  widget: Dropdown,
25177
- transform: optionsTransform
24839
+ transform
25178
24840
  };
25179
24841
 
25180
24842
  const getPromptJSON$j = renderProps => {
@@ -25238,7 +24900,6 @@ class Explanation extends React.Component {
25238
24900
  // adds space between the right border and any text that follows
25239
24901
  paddingLeft: "2px" // adds space between the left border and the button text
25240
24902
  };
25241
-
25242
24903
  const labelStyle = {
25243
24904
  fontSize: "18px",
25244
24905
  lineHeight: "inherit",
@@ -27562,7 +27223,6 @@ class Interaction extends React.Component {
27562
27223
  }
27563
27224
  });
27564
27225
  }
27565
-
27566
27226
  if (element.type === "parametric") {
27567
27227
  const fn = t => {
27568
27228
  return [
@@ -27615,7 +27275,6 @@ class Interaction extends React.Component {
27615
27275
  }
27616
27276
  });
27617
27277
  }
27618
-
27619
27278
  if (element.type === "label") {
27620
27279
  const coord = [
27621
27280
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
@@ -27960,7 +27619,6 @@ function scaleCommandBy(scaleFactor) {
27960
27619
  command.args[6] * scaleFactor // end y
27961
27620
  ]
27962
27621
  });
27963
-
27964
27622
  default:
27965
27623
  return _extends({}, command, {
27966
27624
  args: command.args.map(c => c * scaleFactor)
@@ -28702,7 +28360,6 @@ const styles$f = StyleSheet.create({
28702
28360
  userSelect: "none",
28703
28361
  borderWidth: 0 // <button> gives us a border by default
28704
28362
  },
28705
-
28706
28363
  // We want to render the element in-place in our fixtures, so allow us to
28707
28364
  // render in the bottom corner in production and in-place in testing.
28708
28365
  hudFixedPosition: {
@@ -28732,7 +28389,6 @@ const styles$f = StyleSheet.create({
28732
28389
  ":hover": {
28733
28390
  backgroundColor: "#a1a5a9" // in between those two grays
28734
28391
  },
28735
-
28736
28392
  ":active": {
28737
28393
  backgroundColor: gray68
28738
28394
  }
@@ -28740,7 +28396,7 @@ const styles$f = StyleSheet.create({
28740
28396
  });
28741
28397
 
28742
28398
  const InfoTipBase = props => {
28743
- return /*#__PURE__*/React.createElement(Tooltip$1, {
28399
+ return /*#__PURE__*/React.createElement(Tooltip, {
28744
28400
  content: /*#__PURE__*/React.createElement(Body, {
28745
28401
  style: styles$e.body
28746
28402
  }, props.children),
@@ -28780,7 +28436,6 @@ class InfoTip extends React.Component {
28780
28436
  });
28781
28437
  /* eslint-enable react/no-did-mount-set-state */
28782
28438
  }
28783
-
28784
28439
  render() {
28785
28440
  if (this.state.didMount) {
28786
28441
  return /*#__PURE__*/React.createElement(InfoTipBase, this.props);
@@ -29012,7 +28667,6 @@ class NumberInput extends React.Component {
29012
28667
  if (val !== Math.floor(val)) {
29013
28668
  return; // bail if not an integer
29014
28669
  }
29015
-
29016
28670
  if (e.key === "ArrowUp") {
29017
28671
  val = val + 1;
29018
28672
  } else if (e.key === "ArrowDown") {
@@ -29146,20 +28800,20 @@ RangeInput.defaultProps = {
29146
28800
  };
29147
28801
 
29148
28802
  var components = /*#__PURE__*/Object.freeze({
29149
- __proto__: null,
29150
- ButtonGroup: ButtonGroup,
29151
- Graphie: Graphie,
29152
- HUD: HUD,
29153
- Icon: Icon,
29154
- InfoTip: InfoTip,
29155
- InlineIcon: InlineIcon,
29156
- MathInput: MathInput,
29157
- MultiButtonGroup: MultiButtonGroup,
29158
- NumberInput: NumberInput,
29159
- RangeInput: RangeInput,
29160
- SvgImage: SvgImage,
29161
- TextInput: TextInput,
29162
- TextListEditor: TextListEditor
28803
+ __proto__: null,
28804
+ ButtonGroup: ButtonGroup,
28805
+ Graphie: Graphie,
28806
+ HUD: HUD,
28807
+ Icon: Icon,
28808
+ InfoTip: InfoTip,
28809
+ InlineIcon: InlineIcon,
28810
+ MathInput: MathInput,
28811
+ MultiButtonGroup: MultiButtonGroup,
28812
+ NumberInput: NumberInput,
28813
+ RangeInput: RangeInput,
28814
+ SvgImage: SvgImage,
28815
+ TextInput: TextInput,
28816
+ TextListEditor: TextListEditor
29163
28817
  });
29164
28818
 
29165
28819
  /**
@@ -29288,9 +28942,9 @@ const LockedFunction = props => {
29288
28942
  const {
29289
28943
  color,
29290
28944
  strokeStyle,
29291
- directionalAxis
28945
+ directionalAxis,
28946
+ domain
29292
28947
  } = props;
29293
- const domain = props.domain ? [Number.isFinite(props.domain[0]) ? props.domain[0] : -Infinity, Number.isFinite(props.domain[1]) ? props.domain[1] : Infinity] : undefined;
29294
28948
  const plotProps = {
29295
28949
  color: lockedFigureColors[color],
29296
28950
  style: strokeStyle,
@@ -30702,7 +30356,7 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30702
30356
  }));
30703
30357
  return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && /*#__PURE__*/React.createElement(Hairlines, {
30704
30358
  point: point
30705
- }), showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
30359
+ }), showTooltips ? /*#__PURE__*/React.createElement(Tooltip, {
30706
30360
  autoUpdate: true,
30707
30361
  backgroundColor: wbColorName,
30708
30362
  content: `(${point[X]}, ${point[Y]})`,
@@ -30835,6 +30489,9 @@ const MovableLine = props => {
30835
30489
  onMoveLine = () => {},
30836
30490
  onMovePoint = () => {}
30837
30491
  } = props;
30492
+ const {
30493
+ snapStep
30494
+ } = useGraphConfig();
30838
30495
 
30839
30496
  // Aria live states for (0) point 1, (1) point 2, and (2) grab handle.
30840
30497
  // When moving an element, set its aria live to "polite" and the others
@@ -30866,7 +30523,8 @@ const MovableLine = props => {
30866
30523
  onMove: p => {
30867
30524
  setAriaLives(["polite", "off", "off"]);
30868
30525
  onMovePoint(0, p);
30869
- }
30526
+ },
30527
+ constrain: getMovableLineKeyboardConstraint([start, end], snapStep, 0)
30870
30528
  });
30871
30529
  const {
30872
30530
  visiblePoint: visiblePoint2,
@@ -30881,7 +30539,8 @@ const MovableLine = props => {
30881
30539
  onMove: p => {
30882
30540
  setAriaLives(["off", "polite", "off"]);
30883
30541
  onMovePoint(1, p);
30884
- }
30542
+ },
30543
+ constrain: getMovableLineKeyboardConstraint([start, end], snapStep, 1)
30885
30544
  });
30886
30545
  const line = /*#__PURE__*/React.createElement(Line$1, {
30887
30546
  ariaLabel: ariaLabels == null ? void 0 : ariaLabels.grabHandleAriaLabel,
@@ -30987,6 +30646,34 @@ const Line$1 = props => {
30987
30646
  color: stroke
30988
30647
  }));
30989
30648
  };
30649
+ const getMovableLineKeyboardConstraint = (line, snapStep, pointIndex) => {
30650
+ // Separate the two points into their own variables, and determine which point is being moved
30651
+ const coordToBeMoved = line[pointIndex];
30652
+ const otherPoint = line[1 - pointIndex];
30653
+
30654
+ // Create a helper function that moves the point and then checks
30655
+ // if it overlaps with the other point in the line after the move.
30656
+ const movePointWithConstraint = moveFunc => {
30657
+ // Move the point
30658
+ let movedCoord = moveFunc(coordToBeMoved);
30659
+ // If the moved point overlaps with the other point in the line,
30660
+ // move the point again.
30661
+ if (vec.dist(movedCoord, otherPoint) === 0) {
30662
+ movedCoord = moveFunc(movedCoord);
30663
+ }
30664
+ return movedCoord;
30665
+ };
30666
+
30667
+ // Check if the new point will overlap the other point.
30668
+ // If it will, we need to snap the point to the left or right an additional
30669
+ // snapStep to avoid overlap.
30670
+ return {
30671
+ up: movePointWithConstraint(coord => vec.add(coord, [0, snapStep[1]])),
30672
+ down: movePointWithConstraint(coord => vec.sub(coord, [0, snapStep[1]])),
30673
+ left: movePointWithConstraint(coord => vec.sub(coord, [snapStep[0], 0])),
30674
+ right: movePointWithConstraint(coord => vec.add(coord, [snapStep[0], 0]))
30675
+ };
30676
+ };
30990
30677
  function trimRange(range, graphDimensionsInPixels) {
30991
30678
  const pixelsToTrim = 4;
30992
30679
  const [xRange, yRange] = range;
@@ -31328,7 +31015,8 @@ function CircleGraph(props) {
31328
31015
  } = props;
31329
31016
  const {
31330
31017
  center,
31331
- radiusPoint
31018
+ radiusPoint,
31019
+ snapStep
31332
31020
  } = graphState;
31333
31021
  const {
31334
31022
  strings,
@@ -31391,7 +31079,8 @@ function CircleGraph(props) {
31391
31079
  onMove: newRadiusPoint => {
31392
31080
  setRadiusPointAriaLive("polite");
31393
31081
  dispatch(actions.circle.moveRadiusPoint(newRadiusPoint));
31394
- }
31082
+ },
31083
+ constrain: getCircleKeyboardConstraint(center, radiusPoint, snapStep)
31395
31084
  }), /*#__PURE__*/React.createElement("g", {
31396
31085
  id: radiusId,
31397
31086
  style: a11y.srOnly
@@ -31502,6 +31191,30 @@ function getCircleGraphDescription(state, i18n) {
31502
31191
  const strings = describeCircleGraph(state, i18n);
31503
31192
  return strings.srCircleInteractiveElement;
31504
31193
  }
31194
+ const getCircleKeyboardConstraint = (center, radiusPoint, snapStep) => {
31195
+ // Create a helper function that moves the point and then checks
31196
+ // if it overlaps with the center point after the move.
31197
+ const movePointWithConstraint = moveFunc => {
31198
+ // Move the point
31199
+ let movedCoord = moveFunc(radiusPoint);
31200
+ // If the moved point overlaps with the center point,
31201
+ // move the point again.
31202
+ if (vec.dist(movedCoord, center) === 0) {
31203
+ movedCoord = moveFunc(movedCoord);
31204
+ }
31205
+ return movedCoord;
31206
+ };
31207
+
31208
+ // Check if the new point overlaps the center point.
31209
+ // If it does, we need to snap the point to the left
31210
+ // or right an additional snapStep to avoid the overlap.
31211
+ return {
31212
+ up: movePointWithConstraint(coord => vec.add(coord, [0, snapStep[1]])),
31213
+ down: movePointWithConstraint(coord => vec.sub(coord, [0, snapStep[1]])),
31214
+ left: movePointWithConstraint(coord => vec.sub(coord, [snapStep[0], 0])),
31215
+ right: movePointWithConstraint(coord => vec.add(coord, [snapStep[0], 0]))
31216
+ };
31217
+ };
31505
31218
 
31506
31219
  // Exported for testing
31507
31220
  function describeCircleGraph(state, i18n) {
@@ -31514,6 +31227,7 @@ function describeCircleGraph(state, i18n) {
31514
31227
  radiusPoint
31515
31228
  } = state;
31516
31229
  const radius = getRadius(state);
31230
+ const isRadiusOnRight = radiusPoint[X] >= center[X];
31517
31231
 
31518
31232
  // Aria label strings
31519
31233
  const srCircleGraph = strings.srCircleGraph;
@@ -31521,7 +31235,10 @@ function describeCircleGraph(state, i18n) {
31521
31235
  centerX: srFormatNumber(center[0], locale),
31522
31236
  centerY: srFormatNumber(center[1], locale)
31523
31237
  });
31524
- const srCircleRadiusPoint = strings.srCircleRadiusPoint({
31238
+ const srCircleRadiusPoint = isRadiusOnRight ? strings.srCircleRadiusPointRight({
31239
+ radiusPointX: srFormatNumber(radiusPoint[0], locale),
31240
+ radiusPointY: srFormatNumber(radiusPoint[1], locale)
31241
+ }) : strings.srCircleRadiusPointLeft({
31525
31242
  radiusPointX: srFormatNumber(radiusPoint[0], locale),
31526
31243
  radiusPointY: srFormatNumber(radiusPoint[1], locale)
31527
31244
  });
@@ -31687,6 +31404,12 @@ const LinearSystemGraph = props => {
31687
31404
  locale
31688
31405
  } = usePerseusI18n();
31689
31406
  const id = React.useId();
31407
+ const intersectionId = `${id}-intersection`;
31408
+ const intersectionPoint = geometry.getLineIntersection(lines[0], lines[1]);
31409
+ const intersectionDescription = intersectionPoint ? strings.srLinearSystemIntersection({
31410
+ x: srFormatNumber(intersectionPoint[0], locale),
31411
+ y: srFormatNumber(intersectionPoint[1], locale)
31412
+ }) : strings.srLinearSystemParallel;
31690
31413
  const linesAriaInfo = lines.map((line, i) => {
31691
31414
  return {
31692
31415
  pointsDescriptionId: `${id}-line${i + 1}-points`,
@@ -31703,13 +31426,14 @@ const LinearSystemGraph = props => {
31703
31426
  slopeDescription: getSlopeStringForLine(line, strings)
31704
31427
  };
31705
31428
  });
31429
+ const individualLineDescriptions = linesAriaInfo.map(({
31430
+ pointsDescriptionId,
31431
+ interceptDescriptionId,
31432
+ slopeDescriptionId
31433
+ }) => `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`).join(" ");
31706
31434
  return /*#__PURE__*/React.createElement("g", {
31707
31435
  "aria-label": strings.srLinearSystemGraph,
31708
- "aria-describedby": linesAriaInfo.map(({
31709
- pointsDescriptionId,
31710
- interceptDescriptionId,
31711
- slopeDescriptionId
31712
- }) => `${pointsDescriptionId} ${interceptDescriptionId} ${slopeDescriptionId}`).join(" ")
31436
+ "aria-describedby": `${individualLineDescriptions} ${intersectionId}`
31713
31437
  }, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(MovableLine, {
31714
31438
  key: i,
31715
31439
  points: line,
@@ -31726,9 +31450,15 @@ const LinearSystemGraph = props => {
31726
31450
  x: srFormatNumber(line[1][0], locale),
31727
31451
  y: srFormatNumber(line[1][1], locale)
31728
31452
  }),
31729
- grabHandleAriaLabel: `${linesAriaInfo[i].interceptDescription} ${linesAriaInfo[i].slopeDescription}`
31453
+ grabHandleAriaLabel: strings.srLinearSystemGrabHandle({
31454
+ lineNumber: i + 1,
31455
+ point1X: srFormatNumber(line[0][0], locale),
31456
+ point1Y: srFormatNumber(line[0][1], locale),
31457
+ point2X: srFormatNumber(line[1][0], locale),
31458
+ point2Y: srFormatNumber(line[1][1], locale)
31459
+ })
31730
31460
  },
31731
- ariaDescribedBy: `${linesAriaInfo[i].interceptDescriptionId} ${linesAriaInfo[i].slopeDescriptionId}`,
31461
+ ariaDescribedBy: `${linesAriaInfo[i].interceptDescriptionId} ${linesAriaInfo[i].slopeDescriptionId} ${intersectionId}`,
31732
31462
  onMoveLine: delta => {
31733
31463
  dispatch(actions.linearSystem.moveLine(i, delta));
31734
31464
  },
@@ -31757,7 +31487,10 @@ const LinearSystemGraph = props => {
31757
31487
  key: slopeDescriptionId,
31758
31488
  id: slopeDescriptionId,
31759
31489
  style: a11y.srOnly
31760
- }, slopeDescription))));
31490
+ }, slopeDescription))), /*#__PURE__*/React.createElement("g", {
31491
+ id: intersectionId,
31492
+ style: a11y.srOnly
31493
+ }, intersectionDescription));
31761
31494
  };
31762
31495
 
31763
31496
  // Exported for testing
@@ -32418,7 +32151,8 @@ function QuadraticGraph(props) {
32418
32151
  graphState
32419
32152
  } = props;
32420
32153
  const {
32421
- coords
32154
+ coords,
32155
+ snapStep
32422
32156
  } = graphState;
32423
32157
  const {
32424
32158
  strings,
@@ -32471,6 +32205,7 @@ function QuadraticGraph(props) {
32471
32205
  ariaLabel: `${srQuadraticPoint}${srVertex}`,
32472
32206
  point: coord,
32473
32207
  sequenceNumber: i + 1,
32208
+ constrain: getQuadraticKeyboardConstraint(coords, snapStep, i),
32474
32209
  onMove: destination => dispatch(actions.quadratic.movePoint(i, destination))
32475
32210
  });
32476
32211
  }), srQuadraticDirection && /*#__PURE__*/React.createElement("g", {
@@ -32559,6 +32294,60 @@ function describeQuadraticGraph(state, i18n) {
32559
32294
  srQuadraticInteractiveElements
32560
32295
  };
32561
32296
  }
32297
+ const getQuadraticKeyboardConstraint = (coords, snapStep, pointMoved) => {
32298
+ // Make newCoords mutable
32299
+ const newCoords = [coords[0], coords[1], coords[2]];
32300
+
32301
+ // Get the point that is being moved
32302
+ const coordToBeMoved = newCoords[pointMoved];
32303
+
32304
+ // Create a function to validate and adjust the movement of the point
32305
+ const movePointWithConstraint = moveFunc => {
32306
+ // Move the point the desired direction
32307
+ let movedCoord = moveFunc(coordToBeMoved);
32308
+ newCoords[pointMoved] = movedCoord;
32309
+
32310
+ // If these new coordinates are valid, return the moved coord
32311
+ if (areCoordsValid(newCoords)) {
32312
+ return movedCoord;
32313
+ }
32314
+
32315
+ // If the new coordinates are invalid, we need to move the point an additional
32316
+ // snapStep to avoid creating an invalid quadratic graph.
32317
+ movedCoord = moveFunc(movedCoord);
32318
+ newCoords[pointMoved] = movedCoord;
32319
+
32320
+ // If these updated coordinates are valid now, we can return the new coordinates.
32321
+ if (areCoordsValid(newCoords)) {
32322
+ return movedCoord;
32323
+ }
32324
+
32325
+ // Otherwise, if the new coordinates are still invalid, we need to move the point one final snapStep.
32326
+ // This is to support the edge case where all of the points are each exactly one snapStep away
32327
+ // from each other.
32328
+ // Eg. When coords = [[0, 0], [1, 0], [2, 0]], snapStep = [1, 1], and we're moving coords[0] to the right.
32329
+ return moveFunc(movedCoord);
32330
+ };
32331
+ return {
32332
+ up: vec.add(coordToBeMoved, [0, snapStep[1]]),
32333
+ down: vec.sub(coordToBeMoved, [0, snapStep[1]]),
32334
+ // For horizontal movement, we need to ensure that the points are not on the same vertical line.
32335
+ left: movePointWithConstraint(coord => vec.sub(coord, [snapStep[0], 0])),
32336
+ right: movePointWithConstraint(coord => vec.add(coord, [snapStep[0], 0]))
32337
+ };
32338
+ };
32339
+ const areCoordsValid = coords => {
32340
+ const p1 = coords[0];
32341
+ const p2 = coords[1];
32342
+ const p3 = coords[2];
32343
+
32344
+ // If any of the points share the same x-coordinate,
32345
+ // we are unable to calculate the coefficients, and the graph is invalid.
32346
+ if (p1[0] === p2[0] || p2[0] === p3[0] || p1[0] === p3[0]) {
32347
+ return false;
32348
+ }
32349
+ return true;
32350
+ };
32562
32351
 
32563
32352
  function renderRayGraph(state, dispatch, i18n) {
32564
32353
  return {
@@ -32809,7 +32598,8 @@ function SinusoidGraph(props) {
32809
32598
  // Note: The order of the coordinates is important:
32810
32599
  // The coords[0] is the root and the coords[1] is the first peak
32811
32600
  const {
32812
- coords
32601
+ coords,
32602
+ snapStep
32813
32603
  } = graphState;
32814
32604
 
32815
32605
  // The coefficients are used to calculate the sinusoid equation, plot the graph, and to indicate
@@ -32848,11 +32638,37 @@ function SinusoidGraph(props) {
32848
32638
  key: "point-" + i,
32849
32639
  point: coord,
32850
32640
  sequenceNumber: i + 1,
32641
+ constrain: getSinusoidKeyboardConstraint(coords, snapStep, i),
32851
32642
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
32852
32643
  })), /*#__PURE__*/React.createElement("g", {
32853
32644
  id: descriptionId
32854
32645
  }, srSinusoidDescription));
32855
32646
  }
32647
+ const getSinusoidKeyboardConstraint = (coords, snapStep, pointIndex) => {
32648
+ // Separate the two points into their own variables, and determine which point is being moved
32649
+ const coordToBeMoved = coords[pointIndex];
32650
+ const otherPoint = coords[1 - pointIndex];
32651
+
32652
+ // Create a helper function that checks if the new point is on the same
32653
+ // vertical line as the other point. If it is, we need to move the point
32654
+ // an additional snapStep.
32655
+ const movePointWithConstraint = moveFunc => {
32656
+ // Move the point
32657
+ let movedCoord = moveFunc(coordToBeMoved);
32658
+ // If the moved point overlaps with the other point in the line,
32659
+ // move the point again.
32660
+ if (movedCoord[X] === otherPoint[X]) {
32661
+ movedCoord = moveFunc(movedCoord);
32662
+ }
32663
+ return movedCoord;
32664
+ };
32665
+ return {
32666
+ up: movePointWithConstraint(coord => vec.add(coord, [0, snapStep[1]])),
32667
+ down: movePointWithConstraint(coord => vec.sub(coord, [0, snapStep[1]])),
32668
+ left: movePointWithConstraint(coord => vec.sub(coord, [snapStep[0], 0])),
32669
+ right: movePointWithConstraint(coord => vec.add(coord, [snapStep[0], 0]))
32670
+ };
32671
+ };
32856
32672
 
32857
32673
  // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
32858
32674
  const computeSine = function computeSine(x,
@@ -32904,6 +32720,14 @@ function describeSinusoidGraph(state, i18n) {
32904
32720
  const [root, peak] = coords;
32905
32721
  const diffX = Math.abs(peak[X] - root[X]);
32906
32722
  const diffY = Math.abs(peak[Y] - root[Y]);
32723
+ const formattedRoot = {
32724
+ x: srFormatNumber(root[X], locale),
32725
+ y: srFormatNumber(root[Y], locale)
32726
+ };
32727
+ const formattedPeak = {
32728
+ x: srFormatNumber(peak[X], locale),
32729
+ y: srFormatNumber(peak[Y], locale)
32730
+ };
32907
32731
  const srSinusoidGraph = strings.srSinusoidGraph;
32908
32732
  const srSinusoidDescription = strings.srSinusoidDescription({
32909
32733
  minValue: srFormatNumber(root[Y] - diffY, locale),
@@ -32911,14 +32735,8 @@ function describeSinusoidGraph(state, i18n) {
32911
32735
  cycleStart: srFormatNumber(root[X] - 2 * diffX, locale),
32912
32736
  cycleEnd: srFormatNumber(root[X] + 2 * diffX, locale)
32913
32737
  });
32914
- const srSinusoidRootPoint = strings.srSinusoidRootPoint({
32915
- x: srFormatNumber(root[X], locale),
32916
- y: srFormatNumber(root[Y], locale)
32917
- });
32918
- const srSinusoidPeakPoint = strings.srSinusoidPeakPoint({
32919
- x: srFormatNumber(peak[X], locale),
32920
- y: srFormatNumber(peak[Y], locale)
32921
- });
32738
+ const srSinusoidRootPoint = strings.srSinusoidRootPoint(formattedRoot);
32739
+ const srSinusoidPeakPoint = peak[Y] === root[Y] ? strings.srSinusoidFlatPoint(formattedPeak) : peak[Y] > root[Y] ? strings.srSinusoidMaxPoint(formattedPeak) : strings.srSinusoidMinPoint(formattedPeak);
32922
32740
  const srSinusoidInteractiveElements = strings.srInteractiveElements({
32923
32741
  elements: strings.srSinusoidInteractiveElements({
32924
32742
  point1X: srFormatNumber(root[X], locale),
@@ -32940,16 +32758,17 @@ const {
32940
32758
  calculateAngleInDegrees,
32941
32759
  convertDegreesToRadians
32942
32760
  } = angles;
32943
- const protractorImage = "https://ka-perseus-graphie.s3.amazonaws.com/e9d032f2ab8b95979f674fbfa67056442ba1ff6a.png";
32761
+ const protractorImage = "https://cdn.kastatic.org/images/perseus/protractor.svg";
32944
32762
 
32945
32763
  // The vector from the center of the protractor to the top left corner of the
32946
32764
  // protractor image, in pixels. Used for positioning.
32947
- const centerToTopLeft = [-180, -170];
32765
+ const centerToTopLeft = [-195, -190];
32948
32766
 
32949
32767
  // The vector from the center of the protractor to the center of the rotation
32950
32768
  // handle.
32951
- const centerToRotationHandle = [-176, -15];
32769
+ const centerToRotationHandle = [-201, -15];
32952
32770
  function Protractor() {
32771
+ const staticUrl = getDependencies().staticUrl;
32953
32772
  const {
32954
32773
  range,
32955
32774
  snapStep
@@ -32961,7 +32780,9 @@ function Protractor() {
32961
32780
  const [center, setCenter] = useState(initialCenter);
32962
32781
  const [rotationHandleOffset, setRotationHandleOffset] = useState(centerToRotationHandle);
32963
32782
  const draggableRef = useRef(null);
32964
- useDraggable({
32783
+ const {
32784
+ dragging
32785
+ } = useDraggable({
32965
32786
  gestureTarget: draggableRef,
32966
32787
  onMove: setCenter,
32967
32788
  point: center,
@@ -32985,10 +32806,11 @@ function Protractor() {
32985
32806
  ref: draggableRef,
32986
32807
  transform: `translate(${topLeftPx[X]}, ${topLeftPx[Y]}), rotate(${angle})`,
32987
32808
  style: {
32988
- transformOrigin: `${-centerToTopLeft[X]}px ${-centerToTopLeft[Y]}px`
32809
+ transformOrigin: `${-centerToTopLeft[X]}px ${-centerToTopLeft[Y]}px`,
32810
+ cursor: dragging ? "grabbing" : "grab"
32989
32811
  }
32990
32812
  }, /*#__PURE__*/React.createElement("image", {
32991
- href: protractorImage
32813
+ href: staticUrl(protractorImage)
32992
32814
  }), /*#__PURE__*/React.createElement("g", {
32993
32815
  transform: `translate(5, ${-centerToTopLeft[1]})`,
32994
32816
  ref: rotationHandleRef
@@ -34863,7 +34685,7 @@ const StatefulMafsGraph = /*#__PURE__*/React.forwardRef((props, ref) => {
34863
34685
 
34864
34686
  // If the graph is static, it always displays the correct answer. This is
34865
34687
  // standard behavior for Perseus widgets (e.g. compare the Radio widget).
34866
- if (props.static) {
34688
+ if (props.static && props.correct) {
34867
34689
  return /*#__PURE__*/React.createElement(MafsGraph, _extends({}, props, {
34868
34690
  state: initializeGraphState(_extends({}, props, {
34869
34691
  graph: props.correct
@@ -34882,7 +34704,7 @@ const {
34882
34704
  } = angles;
34883
34705
  const {
34884
34706
  getLineEquation,
34885
- getLineIntersection,
34707
+ getLineIntersectionString,
34886
34708
  magnitude,
34887
34709
  vector
34888
34710
  } = geometry;
@@ -35240,7 +35062,7 @@ class InteractiveGraph extends React.Component {
35240
35062
  }
35241
35063
  static getLinearSystemEquationString(props) {
35242
35064
  const coords = InteractiveGraph.getLinearSystemCoords(props.graph, props);
35243
- return "\n" + getLineEquation(coords[0][0], coords[0][1]) + "\n" + getLineEquation(coords[1][0], coords[1][1]) + "\n" + getLineIntersection(coords[0], coords[1]);
35065
+ return "\n" + getLineEquation(coords[0][0], coords[0][1]) + "\n" + getLineEquation(coords[1][0], coords[1][1]) + "\n" + getLineIntersectionString(coords[0], coords[1]);
35244
35066
  }
35245
35067
  static getPointEquationString(props) {
35246
35068
  if (props.graph.type !== "point") {
@@ -35688,7 +35510,6 @@ const styles$a = StyleSheet.create({
35688
35510
  markerCorrect: {
35689
35511
  background: "#00880b" // WB green darkened by 18%
35690
35512
  },
35691
-
35692
35513
  markerIncorrect: {
35693
35514
  background: color.offBlack64
35694
35515
  }
@@ -36081,7 +35902,6 @@ class LabelImage extends React.Component {
36081
35902
  const adjustPillDistance = {
36082
35903
  [`margin${markerPosition.charAt(0).toUpperCase() + markerPosition.slice(1)}`]: 10 // move pill further from marker
36083
35904
  };
36084
-
36085
35905
  const answerChoicesActive = index === activeMarkerIndex;
36086
35906
  const showAnswerChoice = marker.selected && !answerChoicesActive && !this.state.hideAnswers;
36087
35907
  return /*#__PURE__*/React.createElement(View, {
@@ -36526,7 +36346,6 @@ class Draggable extends React.Component {
36526
36346
  if (this.props.width) {
36527
36347
  style.width = this.props.width + 1; // Fix for non-integer widths
36528
36348
  }
36529
-
36530
36349
  if (this.props.height) {
36531
36350
  style.height = this.props.height;
36532
36351
  }
@@ -36868,8 +36687,7 @@ class Sortable extends React.Component {
36868
36687
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36869
36688
  margin = "0 0 " + this.props.margin + "px 0"; // bottom
36870
36689
  }
36871
-
36872
- cards.push( /*#__PURE__*/React.createElement(Draggable, {
36690
+ cards.push(/*#__PURE__*/React.createElement(Draggable, {
36873
36691
  content: item.option,
36874
36692
  key: item.key,
36875
36693
  state: item.state
@@ -36922,7 +36740,7 @@ class Sortable extends React.Component {
36922
36740
  this, item.key)
36923
36741
  }));
36924
36742
  if (item.state === ItemState.DRAGGING || item.state === ItemState.ANIMATING) {
36925
- cards.push( /*#__PURE__*/React.createElement(Placeholder, {
36743
+ cards.push(/*#__PURE__*/React.createElement(Placeholder, {
36926
36744
  key: "placeholder_" + item.key,
36927
36745
  ref: "placeholder_" + item.key,
36928
36746
  width: item.width,
@@ -37013,10 +36831,6 @@ const getPromptJSON$9 = (renderProps, userInput) => {
37013
36831
  };
37014
36832
  };
37015
36833
 
37016
- const {
37017
- shuffle: shuffle$1,
37018
- seededRNG
37019
- } = Util;
37020
36834
  const HACKY_CSS_CLASSNAME = "perseus-widget-matcher";
37021
36835
  class Matcher extends React.Component {
37022
36836
  constructor(...args) {
@@ -37108,16 +36922,16 @@ class Matcher extends React.Component {
37108
36922
  }, "1")));
37109
36923
  }
37110
36924
 
37111
- // Use the same random() function to shuffle both columns sequentially
37112
- const rng = seededRNG(this.props.problemNum);
37113
- let left;
37114
- if (!this.props.orderMatters) {
37115
- // If the order doesn't matter, don't shuffle the left column
37116
- left = this.props.left;
37117
- } else {
37118
- left = shuffle$1(this.props.left, rng, /* ensurePermuted */true);
37119
- }
37120
- const right = shuffle$1(this.props.right, rng, /* ensurePermuted */true);
36925
+ /* TODO(LEMS-2841):
36926
+ Once the getMatcherPublicWidgetOptions function gets connected to the
36927
+ widget, we'll need to update this to the line below to only shuffle
36928
+ on the server.
36929
+ const {left, right} = this.props;
36930
+ */
36931
+ const {
36932
+ left,
36933
+ right
36934
+ } = shuffleMatcher(this.props);
37121
36935
  const showLabels = _.any(this.props.labels);
37122
36936
  const constraints = {
37123
36937
  height: _.max([this.state.leftHeight, this.state.rightHeight])
@@ -38716,7 +38530,6 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
38716
38530
  if (!_.isFinite(props.tickStep) || props.tickStep <= 0) {
38717
38531
  return []; // this has screwed me for the last time!
38718
38532
  }
38719
-
38720
38533
  const results = [];
38721
38534
 
38722
38535
  // For convenience, extract some props into separate variables
@@ -38787,6 +38600,7 @@ const TickMarks = Graphie.createSimpleClass((graphie, props) => {
38787
38600
 
38788
38601
  // TODO: most widgets use some like Widget<Something, PerseusNumberLineWidgetOptions>
38789
38602
  // should this one?
38603
+
38790
38604
  class NumberLine extends React.Component {
38791
38605
  constructor(..._args) {
38792
38606
  super(..._args);
@@ -39152,7 +38966,6 @@ const numberLineTransform = editorProps => {
39152
38966
  } else {
39153
38967
  numDivisions = undefined; // send to getDefaultProps()
39154
38968
  }
39155
-
39156
38969
  _.extend(props, {
39157
38970
  numLinePosition: numLinePosition,
39158
38971
  numDivisions: numDivisions,
@@ -39177,7 +38990,6 @@ const staticTransform$1 = editorProps => {
39177
38990
  } else {
39178
38991
  numDivisions = undefined; // send to getDefaultProps()
39179
38992
  }
39180
-
39181
38993
  _.extend(props, {
39182
38994
  numLinePosition: numLinePosition,
39183
38995
  numDivisions: numDivisions,
@@ -40166,7 +39978,6 @@ class Plotter extends React.Component {
40166
39978
  const initialY = isMobile ? c.scaleY : 0;
40167
39979
  for (let y = initialY; y <= c.dimY; y += c.scaleY) {
40168
39980
  graphie.label([0, y], KhanMath.roundToApprox(y, 2), "left", /* isTeX */true /* for the \approx symbol */);
40169
-
40170
39981
  graphie.style({
40171
39982
  stroke: isMobile ? "#e9ebec" : "#000",
40172
39983
  strokeWidth: 1,
@@ -40893,9 +40704,6 @@ const getPromptJSON$1 = userInput => {
40893
40704
  };
40894
40705
  };
40895
40706
 
40896
- const {
40897
- shuffle
40898
- } = Util;
40899
40707
  class Sorter extends React.Component {
40900
40708
  constructor(...args) {
40901
40709
  super(...args);
@@ -40947,6 +40755,10 @@ class Sorter extends React.Component {
40947
40755
  return getPromptJSON$1(this.getUserInput());
40948
40756
  }
40949
40757
  render() {
40758
+ // TODO(LEMS-2841): Remove client-side shuffle once receiving public
40759
+ // options. The correct field will already be shuffled.
40760
+ // Probably easiest to replace "options" with "this.props.correct" when
40761
+ // setting up Sortable below. Or use the assignment to rename for clarity.
40950
40762
  const options = shuffle(this.props.correct, this.props.problemNum, /* ensurePermuted */true);
40951
40763
  const {
40952
40764
  apiOptions
@@ -41375,9 +41187,51 @@ const init = function init() {
41375
41187
  replaceDeprecatedWidgets();
41376
41188
  };
41377
41189
 
41190
+ /**
41191
+ * Adds the given perseus library version information to the __perseus_debug__
41192
+ * object and ensures that the object is attached to `globalThis` (`window` in
41193
+ * browser environments).
41194
+ *
41195
+ * This allows each library to provide runtime version information to assist in
41196
+ * debugging in production environments.
41197
+ */
41198
+ const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
41199
+ // If the library version is the default value, then we don't want to
41200
+ // prefix it with a "v" to indicate that it is a version number.
41201
+ let prefix = "v";
41202
+ if (libraryVersion === "__lib_version__") {
41203
+ prefix = "";
41204
+ }
41205
+ const formattedVersion = `${prefix}${libraryVersion}`;
41206
+ if (typeof globalThis !== "undefined") {
41207
+ var _globalThis$__perseus;
41208
+ globalThis.__perseus_debug__ = (_globalThis$__perseus = globalThis.__perseus_debug__) != null ? _globalThis$__perseus : {};
41209
+ const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
41210
+ if (existingVersionEntry) {
41211
+ // If we already have an entry and it doesn't match the registered
41212
+ // version, we morph the entry into an array and log a warning.
41213
+ if (existingVersionEntry !== formattedVersion) {
41214
+ // Existing entry might be an array already (oops, at least 2
41215
+ // versions of the library already loaded!).
41216
+ const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
41217
+ allVersions.push(formattedVersion);
41218
+ globalThis.__perseus_debug__[libraryName] = allVersions;
41219
+
41220
+ // eslint-disable-next-line no-console
41221
+ console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
41222
+ }
41223
+ } else {
41224
+ globalThis.__perseus_debug__[libraryName] = formattedVersion;
41225
+ }
41226
+ } else {
41227
+ // eslint-disable-next-line no-console
41228
+ console.warn(`globalThis not found found (${formattedVersion})`);
41229
+ }
41230
+ };
41231
+
41378
41232
  // This file is processed by a Rollup plugin (replace) to inject the production
41379
41233
  const libName = "@khanacademy/perseus";
41380
- const libVersion = "54.0.0";
41234
+ const libVersion = "55.0.0";
41381
41235
  addLibraryVersionToPerseusDebug(libName, libVersion);
41382
41236
 
41383
41237
  const apiVersion = {
@@ -41771,7 +41625,6 @@ class HintsRenderer extends React.Component {
41771
41625
  ReactDOM__default.findDOMNode(this.refs["hintRenderer" + pos]).focus(); // eslint-disable-line react/no-string-refs
41772
41626
  }
41773
41627
  }
41774
-
41775
41628
  render() {
41776
41629
  const apiOptions = this.getApiOptions();
41777
41630
  const hintsVisible = this._hintsVisible();