@khanacademy/perseus 53.0.0 → 54.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 (52) hide show
  1. package/dist/components/highlighting/types.d.ts +2 -1
  2. package/dist/components.d.ts +0 -1
  3. package/dist/es/index.js +1101 -2034
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/es/strings.js +101 -14
  6. package/dist/es/strings.js.map +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1180 -2058
  9. package/dist/index.js.map +1 -1
  10. package/dist/interactive2/movable.d.ts +2 -1
  11. package/dist/server-item-renderer.d.ts +0 -3
  12. package/dist/strings.d.ts +93 -6
  13. package/dist/strings.js +184 -58
  14. package/dist/strings.js.map +1 -1
  15. package/dist/types.d.ts +3 -24
  16. package/dist/util/graphie.d.ts +1 -1
  17. package/dist/widget-ai-utils/radio/radio-ai-utils.d.ts +2 -1
  18. package/dist/widget-type-utils.d.ts +1 -0
  19. package/dist/widgets/categorizer/categorizer.d.ts +1 -2
  20. package/dist/widgets/cs-program/cs-program.d.ts +0 -2
  21. package/dist/widgets/dropdown/dropdown.d.ts +0 -2
  22. package/dist/widgets/expression/expression.d.ts +3 -10
  23. package/dist/widgets/iframe/iframe.d.ts +1 -1
  24. package/dist/widgets/interactive-graph.d.ts +2 -86
  25. package/dist/widgets/interactive-graphs/backgrounds/axis-labels.d.ts +4 -1
  26. package/dist/widgets/interactive-graphs/graphs/angle.d.ts +2 -1
  27. package/dist/widgets/interactive-graphs/graphs/circle.d.ts +6 -2
  28. package/dist/widgets/interactive-graphs/graphs/components/hairlines.d.ts +7 -0
  29. package/dist/widgets/interactive-graphs/graphs/components/movable-point-view.d.ts +1 -0
  30. package/dist/widgets/interactive-graphs/graphs/components/svg-line.d.ts +2 -1
  31. package/dist/widgets/interactive-graphs/graphs/linear-system.d.ts +2 -2
  32. package/dist/widgets/interactive-graphs/graphs/linear.d.ts +1 -1
  33. package/dist/widgets/interactive-graphs/graphs/point.d.ts +3 -2
  34. package/dist/widgets/interactive-graphs/graphs/polygon.d.ts +3 -2
  35. package/dist/widgets/interactive-graphs/graphs/ray.d.ts +1 -1
  36. package/dist/widgets/interactive-graphs/graphs/screenreader-text.d.ts +2 -1
  37. package/dist/widgets/interactive-graphs/graphs/segment.d.ts +2 -2
  38. package/dist/widgets/interactive-graphs/graphs/sinusoid.d.ts +2 -1
  39. package/dist/widgets/interactive-graphs/graphs/utils.d.ts +16 -4
  40. package/dist/widgets/interactive-graphs/interactive-graph-question-builder.d.ts +1 -2
  41. package/dist/widgets/interactive-graphs/mafs-graph.d.ts +0 -2
  42. package/dist/widgets/interactive-graphs/reducer/interactive-graph-action.d.ts +3 -3
  43. package/dist/widgets/interactive-graphs/stateful-mafs-graph.d.ts +0 -2
  44. package/dist/widgets/label-image/label-image.d.ts +1 -3
  45. package/dist/widgets/number-line/number-line.d.ts +0 -2
  46. package/dist/widgets/numeric-input/numeric-input.d.ts +0 -2
  47. package/dist/widgets/orderer/orderer.d.ts +0 -2
  48. package/dist/widgets/sorter/sorter.d.ts +1 -2
  49. package/dist/widgets.d.ts +1 -2
  50. package/package.json +46 -46
  51. package/dist/components/graph.d.ts +0 -61
  52. package/dist/widgets/interactive-graphs/mafs-supported-graph-types.d.ts +0 -4
package/dist/es/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
- import { PerseusError, Errors, expressionLogic, getExpressionPublicWidgetOptions, getDecimalSeparator, approximateDeepEqual, pluck, CoreWidgetRegistry, getUpgradedWidgetOptions, mapObject, getNumericInputPublicWidgetOptions, passageRefLogic, radioLogic, getCategorizerPublicWidgetOptions, getCSProgramPublicWidgetOptions, getDropdownPublicWidgetOptions, GrapherUtil as GrapherUtil$1, lockedFigureColors, lockedFigureFillStyles, approximateEqual, getLabelImagePublicWidgetOptions, getMatrixSize, measurerLogic, getNumberLinePublicWidgetOptions, getOrdererPublicWidgetOptions, getSorterPublicWidgetOptions, addLibraryVersionToPerseusDebug, upgradeWidgetInfoToLatestVersion, getWidgetIdsFromContentByType } from '@khanacademy/perseus-core';
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
4
  export { isFailure, isSuccess, parseAndMigratePerseusArticle, parseAndMigratePerseusItem, parsePerseusItem } from '@khanacademy/perseus-core';
5
5
  import * as PerseusLinter from '@khanacademy/perseus-linter';
6
6
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
@@ -33,7 +33,6 @@ import Banner from '@khanacademy/wonder-blocks-banner';
33
33
  import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-blocks-dropdown';
34
34
  import caretDown from '@phosphor-icons/core/regular/caret-down.svg';
35
35
  import caretUp from '@phosphor-icons/core/regular/caret-up.svg';
36
- import debounce$2 from 'lodash.debounce';
37
36
  import { vec, Coordinates, Ellipse, Plot as Plot$2, Line as Line$4, Point as Point$2, Polygon, Text as Text$1, useTransformContext, Polyline, Mafs } from 'mafs';
38
37
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
39
38
  import questionIcon from '@phosphor-icons/core/regular/question.svg';
@@ -240,6 +239,10 @@ const mockStrings = {
240
239
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
241
240
  selectAnAnswer: "Select an answer",
242
241
  // The following strings are used for interactive graph SR descriptions.
242
+ srGraphInstructions: "Use the Tab key to move through the interactive elements in the graph. When an interactive element has focus, use the Arrow keys to move it.",
243
+ srUnlimitedGraphInstructions: "Press Shift + Enter to interact with the graph. Use the Tab key to move through the interactive elements in the graph and access the graph Action Bar. When an interactive element has focus, use the Arrow keys to move it or use the Delete key to remove it from the graph. Use the buttons in the Action Bar to add or adjust elements within the graph.",
244
+ xAxis: "X-axis",
245
+ yAxis: "Y-axis",
243
246
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
244
247
  addPoint: "Add Point",
245
248
  removePoint: "Remove Point",
@@ -249,7 +252,7 @@ const mockStrings = {
249
252
  num,
250
253
  x,
251
254
  y
252
- }) => `Point ${num} at ${x} comma ${y}`,
255
+ }) => `Point ${num} at ${x} comma ${y}.`,
253
256
  srInteractiveElements: ({
254
257
  elements
255
258
  }) => `Interactive elements: ${elements}`,
@@ -304,17 +307,23 @@ const mockStrings = {
304
307
  point2X,
305
308
  point2Y
306
309
  }) => `Line from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`,
307
- srAngleSideAtCoordinates: ({
308
- point,
309
- side,
310
+ srAngleStartingSide: ({
310
311
  x,
311
312
  y
312
- }) => `Point ${point}, ${side} at ${x} comma ${y}`,
313
- srAngleVertexAtCoordinatesWithAngleMeasure: ({
313
+ }) => `Point 3, starting side at ${x} comma ${y}.`,
314
+ srAngleEndingSide: ({
315
+ x,
316
+ y
317
+ }) => `Point 2, ending side at ${x} comma ${y}.`,
318
+ srAngleVertex: ({
319
+ x,
320
+ y
321
+ }) => `Point 1, vertex at ${x} comma ${y}.`,
322
+ srAngleVertexWithAngleMeasure: ({
314
323
  x,
315
324
  y,
316
325
  angleMeasure
317
- }) => `Point 2, vertex at ${x} comma ${y}. Angle ${angleMeasure} degrees`,
326
+ }) => `Point 1, vertex at ${x} comma ${y}. Angle ${angleMeasure} degrees.`,
318
327
  srAngleGraphAriaLabel: "An angle on a coordinate plane.",
319
328
  srAngleGraphAriaDescription: ({
320
329
  angleMeasure,
@@ -325,6 +334,14 @@ const mockStrings = {
325
334
  endingSideX,
326
335
  endingSideY
327
336
  }) => `The angle measure is ${angleMeasure} degrees with a vertex at ${vertexX} comma ${vertexY}, a point on the starting side at ${startingSideX} comma ${startingSideY} and a point on the ending side at ${endingSideX} comma ${endingSideY}.`,
337
+ srAngleInteractiveElements: ({
338
+ vertexX,
339
+ vertexY,
340
+ startingSideX,
341
+ startingSideY,
342
+ endingSideX,
343
+ endingSideY
344
+ }) => `An angle formed by 3 points. The vertex is at ${vertexX} comma ${vertexY}. The starting side point is at ${startingSideX} comma ${startingSideY}. The ending side point is at ${endingSideX} comma ${endingSideY}.`,
328
345
  srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.",
329
346
  srMultipleSegmentGraphAriaLabel: ({
330
347
  countOfSegments
@@ -437,7 +454,53 @@ const mockStrings = {
437
454
  point2Y,
438
455
  point3X,
439
456
  point3Y
440
- }) => `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`
457
+ }) => `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`,
458
+ srPolygonGraph: "A polygon.",
459
+ srPolygonGraphCoordinatePlane: "A polygon on a coordinate plane.",
460
+ srPolygonGraphPointsNum: ({
461
+ num
462
+ }) => `The polygon has ${num} points.`,
463
+ srPolygonGraphPointsOne: "The polygon has 1 point.",
464
+ srPolygonElementsNum: ({
465
+ num
466
+ }) => `A polygon with ${num} points.`,
467
+ srPolygonElementsOne: "A polygon with 1 point.",
468
+ srPolygonPointAngleApprox: ({
469
+ angle
470
+ }) => `Angle approximately equal to ${angle} degrees.`,
471
+ srPolygonPointAngle: ({
472
+ angle
473
+ }) => `Angle equal to ${angle} degrees.`,
474
+ srPolygonSideLength: ({
475
+ pointNum,
476
+ length
477
+ }) => `A line segment, length equal to ${length} units, connects to point ${pointNum}.`,
478
+ srPolygonSideLengthApprox: ({
479
+ pointNum,
480
+ length
481
+ }) => `A line segment, length approximately equal to ${length} units, connects to point ${pointNum}.`,
482
+ srUnlimitedPolygonEmpty: "An empty coordinate plane.",
483
+ srSinusoidGraph: "A sinusoid function on a coordinate plane.",
484
+ srSinusoidRootPoint: ({
485
+ x,
486
+ y
487
+ }) => `Midline intersection at ${x} comma ${y}.`,
488
+ srSinusoidPeakPoint: ({
489
+ x,
490
+ y
491
+ }) => `Extremum point at ${x} comma ${y}.`,
492
+ srSinusoidDescription: ({
493
+ minValue,
494
+ maxValue,
495
+ cycleStart,
496
+ cycleEnd
497
+ }) => `The graph shows a wave with a minimum value of ${minValue} and a maximum value of ${maxValue}. The wave completes a full cycle from ${cycleStart} to ${cycleEnd}.`,
498
+ srSinusoidInteractiveElements: ({
499
+ point1X,
500
+ point1Y,
501
+ point2X,
502
+ point2Y
503
+ }) => `Sinusoid graph with midline intersection point at ${point1X} comma ${point1Y} and extremum point at ${point2X} comma ${point2Y}.`
441
504
  // The above strings are used for interactive graph SR descriptions.
442
505
  };
443
506
 
@@ -1369,6 +1432,7 @@ const ApiOptions = {
1369
1432
  },
1370
1433
  baseElements: {
1371
1434
  Link: props => {
1435
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- TODO(LEMS-2871): Address a11y error
1372
1436
  return /*#__PURE__*/React.createElement("a", props);
1373
1437
  }
1374
1438
  },
@@ -1772,7 +1836,6 @@ var Expression$1 = {
1772
1836
  propUpgrades: expressionLogic.widgetOptionsUpgrades,
1773
1837
  // For use by the editor
1774
1838
  isLintable: true,
1775
- getPublicWidgetOptions: getExpressionPublicWidgetOptions,
1776
1839
  // TODO(LEMS-2656): remove TS suppression
1777
1840
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
1778
1841
  getOneCorrectAnswerFromRubric(rubric) {
@@ -9838,7 +9901,7 @@ class WrappedLine extends WrappedDrawing {
9838
9901
 
9839
9902
  /* eslint-disable @babel/no-invalid-this */
9840
9903
  const {
9841
- clockwise: clockwise$3,
9904
+ clockwise: clockwise$2,
9842
9905
  reverseVector: reverseVector$1
9843
9906
  } = geometry;
9844
9907
  function scaledDistanceFromAngle(angle) {
@@ -11076,7 +11139,7 @@ _.extend(GraphUtils.Graphie.prototype, {
11076
11139
  // Temporary objects
11077
11140
  _.invoke(polygon.temp, "remove");
11078
11141
  polygon.temp = [];
11079
- const isClockwise = clockwise$3(polygon.coords);
11142
+ const isClockwise = clockwise$2(polygon.coords);
11080
11143
 
11081
11144
  // Update angle labels
11082
11145
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -14467,7 +14530,7 @@ function sum(array) {
14467
14530
  return memo + arg;
14468
14531
  }, 0);
14469
14532
  }
14470
- function clockwise$2(points) {
14533
+ function clockwise$1(points) {
14471
14534
  const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
14472
14535
  const areas = _.map(segments, function (segment) {
14473
14536
  const p1 = segment[0];
@@ -14533,7 +14596,7 @@ const draw$1 = {
14533
14596
  const graphie = this.graphie;
14534
14597
  const self = this;
14535
14598
  const coords = _.invoke(state.points, "coord");
14536
- const isClockwise = clockwise$2(coords);
14599
+ const isClockwise = clockwise$1(coords);
14537
14600
  const n = coords.length;
14538
14601
 
14539
14602
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -15674,12 +15737,17 @@ class ImageLoader extends React.Component {
15674
15737
  };
15675
15738
  }
15676
15739
  const staticUrl = getDependencies().staticUrl;
15677
- return /*#__PURE__*/React.createElement("img", _extends({
15678
- tabIndex: 0,
15679
- src: staticUrl(src),
15680
- onKeyUp: onKeyUp,
15681
- onKeyDown: onKeyDown
15682
- }, imgProps));
15740
+ return (
15741
+ /*#__PURE__*/
15742
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
15743
+ React.createElement("img", _extends({
15744
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- TODO(LEMS-2871): Address a11y error
15745
+ tabIndex: 0,
15746
+ src: staticUrl(src),
15747
+ onKeyUp: onKeyUp,
15748
+ onKeyDown: onKeyDown
15749
+ }, imgProps))
15750
+ );
15683
15751
  };
15684
15752
  this.state = {
15685
15753
  status: props.src ? Status.LOADING : Status.PENDING
@@ -16270,9 +16338,29 @@ class Zoomable extends React.Component {
16270
16338
  if (!this._isMounted) {
16271
16339
  return;
16272
16340
  }
16341
+
16342
+ // When a zoomable item is contained within a collapsed parent (such as an Explanation widget),
16343
+ // the parent node has zero width, which causes the scale to be miscalculated.
16344
+ // Therefore, we check to see if the node has a valid width, and if it doesn't,
16345
+ // then we traverse up the DOM tree and look for the closest "perseus-renderer"
16346
+ // (which should have a width).
16347
+ // To be cautious, we also account for the possibility that we can't find any parents with a valid width
16348
+ // (not that they don't exist, just that we can't find them).
16349
+ let parentNode = this._node;
16350
+ let currentNode = parentNode;
16351
+ while (currentNode && currentNode.offsetWidth === 0) {
16352
+ var _currentNode$parentEl, _currentNode$parentEl2;
16353
+ // When traversing up the DOM tree, we need to start at the "parentElement"
16354
+ // because "closest()" can match the current element,
16355
+ // and if it does so, then we are stuck in an infinite loop.
16356
+ currentNode = (_currentNode$parentEl = (_currentNode$parentEl2 = currentNode.parentElement) == null ? void 0 : _currentNode$parentEl2.closest(".perseus-renderer")) != null ? _currentNode$parentEl : null;
16357
+ }
16358
+ if (currentNode) {
16359
+ parentNode = currentNode;
16360
+ }
16273
16361
  const parentBounds = {
16274
- width: this._node.offsetWidth,
16275
- height: this._node.offsetHeight
16362
+ width: parentNode.offsetWidth,
16363
+ height: parentNode.offsetHeight
16276
16364
  };
16277
16365
  const childBounds = this.props.computeChildBounds(this._node, parentBounds);
16278
16366
 
@@ -16344,16 +16432,20 @@ class Zoomable extends React.Component {
16344
16432
  opacity: opacity,
16345
16433
  WebkitTapHighlightColor: "transparent"
16346
16434
  }, transitionStyle);
16347
- return /*#__PURE__*/React.createElement("span", {
16348
- onClick: this.handleClick,
16349
- onClickCapture: this.handleClickIfZoomed,
16350
- onTouchCancelCapture: this.stopPropagationIfZoomed,
16351
- onTouchEndCapture: this.stopPropagationIfZoomed,
16352
- onTouchStartCapture: this.stopPropagationIfZoomed
16353
- // @ts-expect-error - TS2322 - Type '{ readonly transitionProperty: string; readonly transitionDuration: string; readonly transitionTimingFunction: string; readonly display: "block"; readonly width: "100%"; readonly height: number | ... 1 more ... | undefined; readonly transform: string; readonly transformOrigin: "0 0"; readonly opacity: 0 | 1; readonl...' is not assignable to type 'CSSProperties | undefined'.
16354
- ,
16355
- style: style
16356
- }, this.props.children);
16435
+ return (
16436
+ /*#__PURE__*/
16437
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- TODO(LEMS-2871): Address a11y error
16438
+ React.createElement("span", {
16439
+ onClick: this.handleClick,
16440
+ onClickCapture: this.handleClickIfZoomed,
16441
+ onTouchCancelCapture: this.stopPropagationIfZoomed,
16442
+ onTouchEndCapture: this.stopPropagationIfZoomed,
16443
+ onTouchStartCapture: this.stopPropagationIfZoomed
16444
+ // @ts-expect-error - TS2322 - Type '{ readonly transitionProperty: string; readonly transitionDuration: string; readonly transitionTimingFunction: string; readonly display: "block"; readonly width: "100%"; readonly height: number | ... 1 more ... | undefined; readonly transform: string; readonly transformOrigin: "0 0"; readonly opacity: 0 | 1; readonl...' is not assignable to type 'CSSProperties | undefined'.
16445
+ ,
16446
+ style: style
16447
+ }, this.props.children)
16448
+ );
16357
16449
  }
16358
16450
  }
16359
16451
  Zoomable.defaultProps = {
@@ -17574,10 +17666,6 @@ const getWidgetExport = name => {
17574
17666
  var _widgets$name3;
17575
17667
  return (_widgets$name3 = widgets[name]) != null ? _widgets$name3 : null;
17576
17668
  };
17577
- const getPublicWidgetOptionsFunction = name => {
17578
- var _widgets$name$getPubl, _widgets$name4;
17579
- return (_widgets$name$getPubl = (_widgets$name4 = widgets[name]) == null ? void 0 : _widgets$name4.getPublicWidgetOptions) != null ? _widgets$name$getPubl : i => i;
17580
- };
17581
17669
  const getEditor = name => {
17582
17670
  return _.has(editors, name) ? editors[name] : null;
17583
17671
  };
@@ -17714,7 +17802,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
17714
17802
  replaceDeprecatedEditors: replaceDeprecatedEditors,
17715
17803
  getWidget: getWidget,
17716
17804
  getWidgetExport: getWidgetExport,
17717
- getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
17718
17805
  getEditor: getEditor,
17719
17806
  getTransform: getTransform,
17720
17807
  getVersion: getVersion,
@@ -17790,6 +17877,7 @@ class WidgetContainer extends React.Component {
17790
17877
  "perseus-widget__definition": this.props.type === "definition"
17791
17878
  });
17792
17879
  const type = this.props.type;
17880
+ const userAgent = navigator.userAgent;
17793
17881
  const WidgetType = getWidget(type);
17794
17882
  if (WidgetType == null) {
17795
17883
  // This is for the good of all people!!
@@ -17800,6 +17888,12 @@ class WidgetContainer extends React.Component {
17800
17888
  className: className
17801
17889
  });
17802
17890
  }
17891
+ let subType = "null";
17892
+ if (type === "interactive-graph") {
17893
+ var _props$graph$type, _props$graph;
17894
+ const props = this.state.widgetProps;
17895
+ subType = (_props$graph$type = (_props$graph = props.graph) == null ? void 0 : _props$graph.type) != null ? _props$graph$type : "null";
17896
+ }
17803
17897
  let alignment = this.state.widgetProps.alignment;
17804
17898
  if (alignment === "default") {
17805
17899
  alignment = CoreWidgetRegistry.getDefaultAlignment(type);
@@ -17853,12 +17947,26 @@ class WidgetContainer extends React.Component {
17853
17947
  widget_type: type,
17854
17948
  widget_id: this.props.id
17855
17949
  },
17856
- onError: () => {
17950
+ onError: error => {
17951
+ // TODO(LEMS-2826): Remove analytics event in LEMS-2826 in favor of ti below.
17857
17952
  analytics.onAnalyticsEvent({
17858
17953
  type: "perseus:widget-rendering-error",
17859
17954
  payload: {
17955
+ widgetSubType: subType,
17860
17956
  widgetType: type,
17861
- widgetId: this.props.id
17957
+ widgetId: this.props.id,
17958
+ message: error.message,
17959
+ userAgent: userAgent
17960
+ }
17961
+ });
17962
+ analytics.onAnalyticsEvent({
17963
+ type: "perseus:widget-rendering-error:ti",
17964
+ payload: {
17965
+ widgetSubType: subType,
17966
+ widgetType: type,
17967
+ widgetId: this.props.id,
17968
+ message: error.message,
17969
+ userAgent: userAgent
17862
17970
  }
17863
17971
  });
17864
17972
  }
@@ -19292,7 +19400,7 @@ function _objectWithoutPropertiesLoose(r, e) {
19292
19400
  if (null == r) return {};
19293
19401
  var t = {};
19294
19402
  for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
19295
- if (e.includes(n)) continue;
19403
+ if (-1 !== e.indexOf(n)) continue;
19296
19404
  t[n] = r[n];
19297
19405
  }
19298
19406
  return t;
@@ -20282,7 +20390,6 @@ var NumericInput$1 = {
20282
20390
  widget: NumericInput,
20283
20391
  transform: propsTransform,
20284
20392
  isLintable: true,
20285
- getPublicWidgetOptions: getNumericInputPublicWidgetOptions,
20286
20393
  // TODO(LEMS-2656): remove TS suppression
20287
20394
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
20288
20395
  getOneCorrectAnswerFromRubric(rubric) {
@@ -21087,7 +21194,10 @@ class HighlightTooltip extends React.PureComponent {
21087
21194
  // using div instead of TooltipContent because
21088
21195
  // TooltipContent wouldn't let me overwrite
21089
21196
  // user-select and onClick
21090
- const content = /*#__PURE__*/React.createElement("div", {
21197
+ const content =
21198
+ /*#__PURE__*/
21199
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- TODO(LEMS-2871): Address a11y error
21200
+ React.createElement("div", {
21091
21201
  className: css(styles$x.tooltipLabel),
21092
21202
  onClick: this.props.onClick
21093
21203
  }, this.props.label);
@@ -21098,14 +21208,18 @@ class HighlightTooltip extends React.PureComponent {
21098
21208
  top: `calc(${Math.round(focusRect.top)}px + 0.95em)`,
21099
21209
  height: 0
21100
21210
  };
21101
- return /*#__PURE__*/React.createElement("div", {
21102
- style: style,
21103
- onMouseEnter: this.props.onMouseEnter,
21104
- onMouseLeave: this.props.onMouseLeave
21105
- }, /*#__PURE__*/React.createElement(Tooltip$1, {
21106
- content: content,
21107
- opened: true
21108
- }, /*#__PURE__*/React.createElement("div", null)));
21211
+ return (
21212
+ /*#__PURE__*/
21213
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO(LEMS-2871): Address a11y error
21214
+ React.createElement("div", {
21215
+ style: style,
21216
+ onMouseEnter: this.props.onMouseEnter,
21217
+ onMouseLeave: this.props.onMouseLeave
21218
+ }, /*#__PURE__*/React.createElement(Tooltip$1, {
21219
+ content: content,
21220
+ opened: true
21221
+ }, /*#__PURE__*/React.createElement("div", null)))
21222
+ );
21109
21223
  }
21110
21224
  }
21111
21225
  const styles$x = StyleSheet.create({
@@ -23061,8 +23175,8 @@ const styles$s = StyleSheet.create({
23061
23175
  crossOutLine: {
23062
23176
  // Center the icon within the container.
23063
23177
  position: "absolute",
23064
- top: `0px`,
23065
- left: `0px`
23178
+ top: `2px`,
23179
+ left: `2px`
23066
23180
  }
23067
23181
  });
23068
23182
 
@@ -23089,43 +23203,6 @@ function ChoiceInner(props) {
23089
23203
  }
23090
23204
  return /*#__PURE__*/React.createElement(InlineIcon, iconMinus);
23091
23205
  }
23092
-
23093
- // Handle dynamic styling of the multiple choice icon. Most
23094
- // MC icon styles are constant, but we do allow the caller
23095
- // to specify the selected color, and thus must control styles
23096
- // related to the selected state dynamically.
23097
- function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
23098
- let backgroundColor;
23099
- let borderColor;
23100
- let color$1;
23101
- if (!showCorrectness && pressed) {
23102
- borderColor = color.blue;
23103
- color$1 = color.blue;
23104
- backgroundColor = "transparent";
23105
- } else if (checked) {
23106
- const bg = showCorrectness && correct ? color.green : color.blue;
23107
- color$1 = white;
23108
- backgroundColor = bg;
23109
- borderColor = bg;
23110
- } else {
23111
- borderColor = color.offBlack64;
23112
- color$1 = color.offBlack64;
23113
- }
23114
-
23115
- // define shape
23116
- let borderRadius;
23117
- if (multipleSelect) {
23118
- borderRadius = 3;
23119
- } else {
23120
- borderRadius = CHOICE_ICON_SIZE;
23121
- }
23122
- return {
23123
- backgroundColor,
23124
- borderColor,
23125
- color: color$1,
23126
- borderRadius
23127
- };
23128
- }
23129
23206
  const ChoiceIcon = function ChoiceIcon(props) {
23130
23207
  const {
23131
23208
  checked,
@@ -23137,9 +23214,41 @@ const ChoiceIcon = function ChoiceIcon(props) {
23137
23214
  multipleSelect,
23138
23215
  pos,
23139
23216
  previouslyAnswered,
23217
+ // Used in "review mode"/"show rationale" to show that option was previously chosen
23140
23218
  pressed
23141
23219
  } = props;
23142
- const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
23220
+
23221
+ // Accounts for incorrect choice still displaying as learner tries again
23222
+ const choiceIsChecked = checked || showCorrectness && !correct && previouslyAnswered;
23223
+
23224
+ // Core styling
23225
+ const choiceStyling = [styles$r.choiceBase, multipleSelect ? styles$r.multiSelectShape : styles$r.singleSelectShape, showCorrectness ? styles$r.choiceHasIcon : styles$r.choiceHasLetter, choiceIsChecked ? styles$r.choiceIsChecked : styles$r.choiceIsUnchecked];
23226
+
23227
+ // Color styling
23228
+ // Handle dynamic styling of the multiple choice icon. Most
23229
+ // MC icon styles are constant, but we do allow the caller
23230
+ // to specify the selected color, and thus must control styles
23231
+ // related to the selected state dynamically.
23232
+ let crossOutColor;
23233
+ if (showCorrectness && correct && checked) {
23234
+ choiceStyling.push(styles$r.choiceCorrect);
23235
+ crossOutColor = color.green;
23236
+ } else if (showCorrectness && !correct && (checked || previouslyAnswered)) {
23237
+ choiceStyling.push(styles$r.choiceIncorrect);
23238
+ crossOutColor = color.red;
23239
+ } else if (checked) {
23240
+ // Show filled neutral blue color (showCorrectness is false)
23241
+ choiceStyling.push(styles$r.choiceNeutral);
23242
+ crossOutColor = color.blue;
23243
+ } else if (pressed) {
23244
+ // Show outlined neutral blue color (showCorrectness is false)
23245
+ choiceStyling.push(styles$r.activeNeutral);
23246
+ crossOutColor = color.blue;
23247
+ } else {
23248
+ // choice is not checked
23249
+ choiceStyling.push(styles$r.uncheckedColors);
23250
+ crossOutColor = color.offBlack64;
23251
+ }
23143
23252
  return /*#__PURE__*/React.createElement("div", {
23144
23253
  className: css(styles$t.iconWrapper)
23145
23254
  }, /*#__PURE__*/React.createElement(FocusRing, {
@@ -23147,9 +23256,8 @@ const ChoiceIcon = function ChoiceIcon(props) {
23147
23256
  visible: focused || hovered,
23148
23257
  multipleSelect: multipleSelect
23149
23258
  }, /*#__PURE__*/React.createElement("div", {
23150
- style: dynamicStyles,
23151
23259
  "data-testid": "choice-icon__library-choice-icon",
23152
- className: css(styles$r.circle, showCorrectness && correct && styles$r.circleCorrect, showCorrectness && !correct && styles$r.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$r.circleIncorrectAnswered)
23260
+ className: css(...choiceStyling)
23153
23261
  // used in BaseRadio editMode to check
23154
23262
  // if we actually clicked on the radio icon
23155
23263
  ,
@@ -23161,7 +23269,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
23161
23269
  showCorrectness: showCorrectness,
23162
23270
  correct: correct
23163
23271
  })))), crossedOut && /*#__PURE__*/React.createElement(CrossOutLine, {
23164
- color: dynamicStyles.borderColor
23272
+ color: crossOutColor
23165
23273
  }));
23166
23274
  };
23167
23275
  const styles$r = StyleSheet.create({
@@ -23173,8 +23281,7 @@ const styles$r = StyleSheet.create({
23173
23281
  alignItems: "center",
23174
23282
  justifyContent: "center"
23175
23283
  },
23176
- circle: {
23177
- // Make the circle
23284
+ choiceBase: {
23178
23285
  width: CHOICE_ICON_SIZE,
23179
23286
  height: CHOICE_ICON_SIZE,
23180
23287
  boxSizing: "border-box",
@@ -23186,30 +23293,48 @@ const styles$r = StyleSheet.create({
23186
23293
  // "bold font family" so that characters which fall back to the default
23187
23294
  // font get bolded too.
23188
23295
  fontWeight: "bold",
23189
- fontSize: 12,
23190
23296
  // Center the icon wrapper.
23191
23297
  display: "flex",
23192
23298
  alignItems: "center",
23193
- justifyContent: "center",
23194
- // HACK(emily): I don't know why adding this line height makes the text
23195
- // appear centered better than any other value, but it does. In
23196
- // particular, at large zoom levels this line height does almost
23197
- // nothing, but at the default size this shifts the letter down one
23198
- // pixel so it is much better centered.
23199
- lineHeight: "1px"
23200
- },
23201
- circleCorrect: {
23299
+ justifyContent: "center"
23300
+ },
23301
+ choiceHasLetter: {
23302
+ fontSize: 12
23303
+ },
23304
+ choiceHasIcon: {
23202
23305
  fontSize: CHOICE_ICON_SIZE
23203
23306
  },
23204
- circleIncorrect: {
23205
- fontSize: CHOICE_ICON_SIZE,
23206
- borderColor: gray68,
23207
- color: gray68
23307
+ choiceIsChecked: {
23308
+ color: color.white
23208
23309
  },
23209
- circleIncorrectAnswered: {
23310
+ choiceIsUnchecked: {
23311
+ color: color.offBlack64
23312
+ },
23313
+ choiceCorrect: {
23314
+ backgroundColor: color.green,
23315
+ borderColor: color.green
23316
+ },
23317
+ choiceIncorrect: {
23210
23318
  backgroundColor: color.red,
23211
- borderColor: color.red,
23212
- color: color.white
23319
+ borderColor: color.red
23320
+ },
23321
+ choiceNeutral: {
23322
+ backgroundColor: color.blue,
23323
+ borderColor: color.blue
23324
+ },
23325
+ activeNeutral: {
23326
+ color: color.blue,
23327
+ borderColor: color.blue,
23328
+ backgroundColor: "transparent"
23329
+ },
23330
+ multiSelectShape: {
23331
+ borderRadius: 3
23332
+ },
23333
+ singleSelectShape: {
23334
+ borderRadius: CHOICE_ICON_SIZE
23335
+ },
23336
+ uncheckedColors: {
23337
+ borderColor: color.offBlack64
23213
23338
  }
23214
23339
  });
23215
23340
 
@@ -23797,15 +23922,19 @@ const BaseRadio = function BaseRadio({
23797
23922
  // content somehow? Would changing our choice of key
23798
23923
  // somehow break something happening inside a choice's
23799
23924
  // child Renderers, by changing when we mount/unmount?
23800
- return /*#__PURE__*/React.createElement("li", {
23801
- key: i,
23802
- ref: e => listElem = e,
23803
- className: className,
23804
- onClick: clickHandler,
23805
- onTouchStart: labelWrap ? undefined : captureScratchpadTouchStart$1
23806
- }, /*#__PURE__*/React.createElement(Element, _extends({}, elementProps, {
23807
- ref: ref
23808
- })));
23925
+ return (
23926
+ /*#__PURE__*/
23927
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
23928
+ React.createElement("li", {
23929
+ key: i,
23930
+ ref: e => listElem = e,
23931
+ className: className,
23932
+ onClick: clickHandler,
23933
+ onTouchStart: labelWrap ? undefined : captureScratchpadTouchStart$1
23934
+ }, /*#__PURE__*/React.createElement(Element, _extends({}, elementProps, {
23935
+ ref: ref
23936
+ })))
23937
+ );
23809
23938
  })));
23810
23939
 
23811
23940
  // Allow for horizontal scrolling if content is too wide, which may be
@@ -24621,8 +24750,7 @@ var Categorizer$1 = {
24621
24750
  staticTransform: editorProps => {
24622
24751
  return _.pick(editorProps, "items", "categories", "values", "randomizeItems");
24623
24752
  },
24624
- isLintable: true,
24625
- getPublicWidgetOptions: getCategorizerPublicWidgetOptions
24753
+ isLintable: true
24626
24754
  };
24627
24755
 
24628
24756
  /**
@@ -24828,8 +24956,7 @@ var CSProgram$1 = {
24828
24956
  name: "cs-program",
24829
24957
  displayName: "CS Program",
24830
24958
  widget: CSProgram,
24831
- hidden: true,
24832
- getPublicWidgetOptions: getCSProgramPublicWidgetOptions
24959
+ hidden: true
24833
24960
  };
24834
24961
 
24835
24962
  const getPromptJSON$l = renderProps => {
@@ -25047,8 +25174,7 @@ var Dropdown$1 = {
25047
25174
  displayName: "Drop down",
25048
25175
  accessible: true,
25049
25176
  widget: Dropdown,
25050
- transform: optionsTransform,
25051
- getPublicWidgetOptions: getDropdownPublicWidgetOptions
25177
+ transform: optionsTransform
25052
25178
  };
25053
25179
 
25054
25180
  const getPromptJSON$j = renderProps => {
@@ -26807,12 +26933,16 @@ class Iframe extends React.Component {
26807
26933
  // that we need. This makes it a bit safer in case some content
26808
26934
  // creator "went wild".
26809
26935
  // http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
26810
- return /*#__PURE__*/React.createElement("iframe", {
26811
- sandbox: sandboxProperties,
26812
- style: style,
26813
- src: url,
26814
- allowFullScreen: this.props.allowFullScreen
26815
- });
26936
+ return (
26937
+ /*#__PURE__*/
26938
+ // eslint-disable-next-line jsx-a11y/iframe-has-title -- TODO(LEMS-2871): Address a11y error
26939
+ React.createElement("iframe", {
26940
+ sandbox: sandboxProperties,
26941
+ style: style,
26942
+ src: url,
26943
+ allowFullScreen: this.props.allowFullScreen
26944
+ })
26945
+ );
26816
26946
  }
26817
26947
  }
26818
26948
  Iframe.defaultProps = {
@@ -26844,7 +26974,7 @@ const getPromptJSON$d = renderProps => {
26844
26974
 
26845
26975
  const defaultBoxSize = 400;
26846
26976
  const defaultRange = [0, 10];
26847
- const defaultBackgroundImage$2 = {
26977
+ const defaultBackgroundImage$1 = {
26848
26978
  url: null,
26849
26979
  width: 0,
26850
26980
  height: 0
@@ -27001,7 +27131,7 @@ ImageWidget.defaultProps = {
27001
27131
  title: "",
27002
27132
  range: [defaultRange, defaultRange],
27003
27133
  box: [defaultBoxSize, defaultBoxSize],
27004
- backgroundImage: defaultBackgroundImage$2,
27134
+ backgroundImage: defaultBackgroundImage$1,
27005
27135
  labels: [],
27006
27136
  alt: "",
27007
27137
  caption: "",
@@ -27618,307 +27748,6 @@ var Interactive = {
27618
27748
  hidden: true
27619
27749
  };
27620
27750
 
27621
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
27622
- const defaultBackgroundImage$1 = {
27623
- url: null
27624
- };
27625
-
27626
- /* Style objects */
27627
- const defaultInstructionsStyle = {
27628
- fontStyle: "italic",
27629
- fontWeight: "bold",
27630
- fontSize: "32px",
27631
- width: "100%",
27632
- height: "100%",
27633
- textAlign: "center",
27634
- backgroundColor: "white",
27635
- position: "absolute",
27636
- zIndex: 1,
27637
- transition: "opacity .25s ease-in-out"
27638
- };
27639
- const instructionsTextStyle = {
27640
- position: "relative",
27641
- top: "25%"
27642
- };
27643
- function numSteps$1(range, step) {
27644
- return Math.floor((range[1] - range[0]) / step);
27645
- }
27646
- class Graph extends React.Component {
27647
- constructor(...args) {
27648
- super(...args);
27649
- this.graphieDivRef = /*#__PURE__*/React.createRef();
27650
- this.protractor = void 0;
27651
- this.ruler = void 0;
27652
- this._graphie = void 0;
27653
- // @ts-expect-error - TS2564 - Property '_hasSetupGraphieThisUpdate' has no initializer and is not definitely assigned in the constructor.
27654
- this._hasSetupGraphieThisUpdate = void 0;
27655
- // @ts-expect-error - TS2564 - Property '_shouldSetupGraphie' has no initializer and is not definitely assigned in the constructor.
27656
- this._shouldSetupGraphie = void 0;
27657
- /* Reset the graphie canvas to its initial state
27658
- *
27659
- * Use when re-rendering the parent component and you need a blank
27660
- * graphie.
27661
- */
27662
- this.reset = () => {
27663
- this._setupGraphie(false);
27664
- };
27665
- this.graphie = () => {
27666
- return this._graphie;
27667
- };
27668
- this.pointsFromNormalized = (coordsList, noSnap) => {
27669
- const self = this;
27670
- return _.map(coordsList, function (coords) {
27671
- return _.map(coords, function (coord, i) {
27672
- const range = self.props.range[i];
27673
- if (noSnap) {
27674
- return range[0] + (range[1] - range[0]) * coord;
27675
- }
27676
- const step = self.props.step[i];
27677
- const nSteps = numSteps$1(range, step);
27678
- const tick = Math.round(coord * nSteps);
27679
- return range[0] + step * tick;
27680
- });
27681
- });
27682
- };
27683
- this._setupGraphie = initialMount => {
27684
- // Only setupGraphie once per componentDidUpdate().
27685
- // This prevents this component from rendering graphie
27686
- // and then immediately re-render graphie because its
27687
- // parent component asked it to. This will happen when
27688
- // props on the parent and props on this component both
27689
- // require graphie to be re-rendered.
27690
- if (this._hasSetupGraphieThisUpdate) {
27691
- return;
27692
- }
27693
- const graphieDiv = this.graphieDivRef.current;
27694
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'.
27695
- $(graphieDiv).empty();
27696
-
27697
- // Content creators may need to explicitly add the dollar signs so the
27698
- // strings are picked up by our translation tools. However, these math
27699
- // annotations are redundant because we already render all graph labels
27700
- // in math mode. For example, a label value of `$\text{Dollars}$` will
27701
- // be translatable, but we only want to pass the string
27702
- // `\text{Dollars}` to the Graph widget.
27703
- const labels = this.props.labels.map(label => Util.unescapeMathMode(label));
27704
- const range = this.props.range;
27705
- // @ts-expect-error - TS2345: Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLElement'.
27706
- const graphie = this._graphie = GraphUtils.createGraphie(graphieDiv);
27707
- const gridConfig = this._getGridConfig();
27708
- // @ts-expect-error - TS2339: Property 'snap' does not exist on type 'Graphie'.
27709
- graphie.snap = this.props.snapStep;
27710
- if (this.props.markings === "graph") {
27711
- graphie.graphInit({
27712
- range: range,
27713
- scale: vector$3.map(gridConfig, g => g.scale),
27714
- axisArrows: "<->",
27715
- labelFormat: function (s) {
27716
- return "\\small{" + s + "}";
27717
- },
27718
- gridStep: this.props.gridStep,
27719
- tickStep: vector$3.map(gridConfig, g => g.tickStep),
27720
- labelStep: 1,
27721
- unityLabels: vector$3.map(gridConfig, g => g.unityLabel),
27722
- isMobile: this.props.isMobile
27723
- });
27724
- graphie.label([0, range[1][1]], labels[1], this.props.isMobile ? "below right" : "above");
27725
- graphie.label([range[0][1], 0], labels[0], this.props.isMobile ? "above left" : "right");
27726
- } else if (this.props.markings === "grid") {
27727
- graphie.graphInit({
27728
- range: range,
27729
- scale: vector$3.map(gridConfig, g => g.scale),
27730
- gridStep: this.props.gridStep,
27731
- axes: false,
27732
- ticks: false,
27733
- labels: false,
27734
- isMobile: this.props.isMobile
27735
- });
27736
- } else if (this.props.markings === "none") {
27737
- graphie.init({
27738
- range: range,
27739
- scale: vector$3.map(gridConfig, g => g.scale),
27740
- isMobile: this.props.isMobile
27741
- });
27742
- }
27743
-
27744
- // Add instructions just before mouse layer
27745
- const visible = 0.5;
27746
- const invisible = 0.0;
27747
- let $instructionsWrapper;
27748
- if (this.props.instructions) {
27749
- $instructionsWrapper = $("<div/>");
27750
- _.each(defaultInstructionsStyle, function (value, key) {
27751
- $instructionsWrapper.css(key, value);
27752
- });
27753
- $instructionsWrapper.css("opacity", visible);
27754
- const $instructions = $("<span/>", {
27755
- text: this.props.instructions
27756
- });
27757
- _.each(instructionsTextStyle, function (value, key) {
27758
- $instructions.css(key, value);
27759
- });
27760
- $instructionsWrapper.append($instructions);
27761
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'append' does not exist on type 'JQueryStatic'.
27762
- $(graphieDiv).append($instructionsWrapper);
27763
- } else {
27764
- $instructionsWrapper = undefined;
27765
- }
27766
-
27767
- // Add some handlers for instructions text (if necessary)
27768
- const onMouseDown = $instructionsWrapper || this.props.onMouseDown ? _.bind(function (coord) {
27769
- if ($instructionsWrapper) {
27770
- $instructionsWrapper.remove();
27771
- $instructionsWrapper = null;
27772
- }
27773
- // eslint-disable-next-line @babel/no-invalid-this
27774
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
27775
- // eslint-disable-next-line @babel/no-invalid-this
27776
- this.props.onMouseDown(coord);
27777
- }, this) : undefined;
27778
- const onMouseOver = $instructionsWrapper ? function () {
27779
- var _$instructionsWrapper;
27780
- (_$instructionsWrapper = $instructionsWrapper) == null || _$instructionsWrapper.css("opacity", invisible);
27781
- } : undefined;
27782
- const onMouseOut = $instructionsWrapper ? function () {
27783
- var _$instructionsWrapper2;
27784
- (_$instructionsWrapper2 = $instructionsWrapper) == null || _$instructionsWrapper2.css("opacity", visible);
27785
- } : undefined;
27786
- /* eslint-enable indent */
27787
-
27788
- graphie.addMouseLayer({
27789
- onClick: this.props.onClick,
27790
- onMouseDown: onMouseDown,
27791
- onMouseOver: onMouseOver,
27792
- onMouseOut: onMouseOut,
27793
- onMouseMove: this.props.onMouseMove,
27794
- allowScratchpad: true,
27795
- setDrawingAreaAvailable: this.props.setDrawingAreaAvailable
27796
- });
27797
- this._updateProtractor();
27798
- this._updateRuler();
27799
-
27800
- // We set this flag before jumping into our callback
27801
- // to avoid recursing if our callback calls reset() itself
27802
- this._hasSetupGraphieThisUpdate = true;
27803
- if (!initialMount && this.props.onGraphieUpdated) {
27804
- // Calling a parent callback in componentDidMount is bad and
27805
- // results in hard-to-reason-about lifecycle problems (esp. with
27806
- // refs), so we do it only on update and rely on the parent to
27807
- // query for the graphie object on initial mount
27808
- this.props.onGraphieUpdated(graphie);
27809
- }
27810
- };
27811
- this._getGridConfig = () => {
27812
- return vector$3.map(this.props.step, (step, i) => Util.gridDimensionConfig(step, this.props.range[i], this.props.box[i], this.props.gridStep[i]));
27813
- };
27814
- this._updateProtractor = () => {
27815
- if (this.protractor) {
27816
- this.protractor.remove();
27817
- }
27818
- if (this.props.showProtractor) {
27819
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
27820
- const coord = this.pointsFromNormalized([[0.5, 0.05]])[0];
27821
- this.protractor = this._graphie.protractor(coord);
27822
- }
27823
- };
27824
- this._updateRuler = () => {
27825
- if (this.ruler) {
27826
- this.ruler.remove();
27827
- }
27828
- if (this.props.showRuler) {
27829
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
27830
- const coord = this.pointsFromNormalized([[0.5, 0.25]])[0];
27831
- const extent = this._graphie.range[0][1] - this._graphie.range[0][0];
27832
- this.ruler = this._graphie.ruler({
27833
- center: coord,
27834
- label: this.props.rulerLabel,
27835
- pixelsPerUnit: this._graphie.scale[0],
27836
- ticksPerUnit: this.props.rulerTicks,
27837
- units: Math.round(0.8 * extent)
27838
- });
27839
- }
27840
- };
27841
- this.toJSON = () => {
27842
- return _.pick(this.props, "range", "step", "markings", "labels", "backgroundImage", "showProtractor", "showRuler", "rulerLabel", "rulerTicks", "gridStep", "snapStep");
27843
- };
27844
- }
27845
- componentDidMount() {
27846
- this._setupGraphie(true);
27847
- }
27848
- UNSAFE_componentWillReceiveProps(nextProps) {
27849
- const potentialChanges = ["labels", "range", "step", "markings", "showProtractor", "showRuler", "rulerLabel", "rulerTicks", "gridStep", "snapStep"];
27850
- const self = this;
27851
- _.each(potentialChanges, function (prop) {
27852
- if (!_.isEqual(self.props[prop], nextProps[prop])) {
27853
- self._shouldSetupGraphie = true;
27854
- }
27855
- });
27856
- }
27857
- componentDidUpdate() {
27858
- // Only setupGraphie once per componentDidUpdate().
27859
- // See explanation in setupGraphie().
27860
- this._hasSetupGraphieThisUpdate = false;
27861
- if (this._shouldSetupGraphie) {
27862
- this._setupGraphie(false);
27863
- this._shouldSetupGraphie = false;
27864
- }
27865
- }
27866
- render() {
27867
- let image;
27868
- const imageData = this.props.backgroundImage;
27869
- if (imageData.url) {
27870
- const scale = this.props.box[0] / interactiveSizes$1.defaultBoxSize;
27871
- image = /*#__PURE__*/React.createElement(context$1.Consumer, null, ({
27872
- setAssetStatus
27873
- }) => /*#__PURE__*/React.createElement(SvgImage
27874
- // @ts-expect-error - TS2741 - Property 'alt' is missing in type '{ src: any; width: any; height: any; scale: number; responsive: false; }' but required in type 'Pick<Readonly<Props> & Readonly<{ children?: ReactNode; }>, "children" | "height" | "width" | "title" | "alt" | "trackInteraction" | "preloader" | "allowFullBleed" | "extraGraphie" | "overrideAriaHidden">'.
27875
- , {
27876
- src: imageData.url,
27877
- width: imageData.width,
27878
- height: imageData.height,
27879
- scale: scale,
27880
- responsive: false,
27881
- setAssetStatus: setAssetStatus
27882
- }));
27883
- } else {
27884
- image = null;
27885
- }
27886
- return /*#__PURE__*/React.createElement("div", {
27887
- className: "graphie-container blank-background",
27888
- style: {
27889
- width: this.props.box[0],
27890
- height: this.props.box[1]
27891
- }
27892
- // @ts-expect-error - TS2339 - Property 'onMouseOut' does not exist on type 'Graph'.
27893
- ,
27894
- onMouseOut: this.onMouseOut
27895
- // @ts-expect-error - TS2339 - Property 'onMouseOver' does not exist on type 'Graph'.
27896
- ,
27897
- onMouseOver: this.onMouseOver
27898
- // @ts-expect-error - TS2339 - Property 'onClick' does not exist on type 'Graph'.
27899
- ,
27900
- onClick: this.onClick
27901
- }, image, /*#__PURE__*/React.createElement("div", {
27902
- className: "graphie",
27903
- ref: this.graphieDivRef
27904
- }));
27905
- }
27906
- }
27907
- Graph.defaultProps = {
27908
- labels: ["x", "y"],
27909
- range: [[-10, 10], [-10, 10]],
27910
- step: [1, 1],
27911
- gridStep: [1, 1],
27912
- snapStep: [0.5, 0.5],
27913
- markings: "graph",
27914
- backgroundImage: defaultBackgroundImage$1,
27915
- showProtractor: false,
27916
- showRuler: false,
27917
- rulerLabel: "",
27918
- rulerTicks: 10,
27919
- isMobile: false
27920
- };
27921
-
27922
27751
  const getPromptJSON$b = (props, userInput) => {
27923
27752
  var _props$backgroundImag;
27924
27753
  return {
@@ -28471,13 +28300,18 @@ function escapeIfUnescaped(character) {
28471
28300
  }
28472
28301
  }
28473
28302
 
28474
- function AxisLabels() {
28303
+ function AxisLabels({
28304
+ i18n
28305
+ }) {
28475
28306
  const {
28476
28307
  range,
28477
28308
  labels,
28478
28309
  width,
28479
28310
  height
28480
28311
  } = useGraphConfig();
28312
+ const {
28313
+ strings
28314
+ } = i18n;
28481
28315
  const yAxisLabelLocation = [0, range[Y][MAX]];
28482
28316
  const xAxisLabelLocation = [range[X][MAX], 0];
28483
28317
  const [xAxisLabelText, yAxisLabelText] = labels;
@@ -28495,6 +28329,7 @@ function AxisLabels() {
28495
28329
  TeX
28496
28330
  } = getDependencies();
28497
28331
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
28332
+ "aria-label": strings.xAxis,
28498
28333
  style: {
28499
28334
  position: "absolute",
28500
28335
  left: x1,
@@ -28503,6 +28338,7 @@ function AxisLabels() {
28503
28338
  transform: "translate(7px, -50%)"
28504
28339
  }
28505
28340
  }, /*#__PURE__*/React__default.createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default.createElement("span", {
28341
+ "aria-label": strings.yAxis,
28506
28342
  style: {
28507
28343
  position: "absolute",
28508
28344
  left: x2,
@@ -29312,7 +29148,6 @@ RangeInput.defaultProps = {
29312
29148
  var components = /*#__PURE__*/Object.freeze({
29313
29149
  __proto__: null,
29314
29150
  ButtonGroup: ButtonGroup,
29315
- Graph: Graph,
29316
29151
  Graphie: Graphie,
29317
29152
  HUD: HUD,
29318
29153
  Icon: Icon,
@@ -29453,9 +29288,9 @@ const LockedFunction = props => {
29453
29288
  const {
29454
29289
  color,
29455
29290
  strokeStyle,
29456
- directionalAxis,
29457
- domain
29291
+ directionalAxis
29458
29292
  } = 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;
29459
29294
  const plotProps = {
29460
29295
  color: lockedFigureColors[color],
29461
29296
  style: strokeStyle,
@@ -29534,14 +29369,90 @@ function Vector(props) {
29534
29369
  }));
29535
29370
  }
29536
29371
 
29537
- function srFormatNumber(a, locale) {
29372
+ function srFormatNumber(a, locale, maximumFractionDigits) {
29373
+ const piBasedNumber = getPiMultiple(a);
29374
+ if (piBasedNumber) {
29375
+ return piBasedNumber;
29376
+ }
29377
+
29538
29378
  // adding zero here converts negative zero to positive zero.
29539
29379
  return (0 + a).toLocaleString(locale, {
29540
- maximumFractionDigits: 3,
29380
+ maximumFractionDigits: maximumFractionDigits != null ? maximumFractionDigits : 3,
29541
29381
  useGrouping: false // no thousands separators
29542
29382
  });
29543
29383
  }
29544
29384
 
29385
+ // Exported for testing
29386
+ function getPiMultiple(a) {
29387
+ // - Save some calculations by checking if this is an integer first.
29388
+ // If a is an integer, it's definitely not a multiple of π.
29389
+ // - 0 should not be presented as a multiple of π.
29390
+ // - If a is greater than 1e12, then it thinks that it's always a
29391
+ // multiple of π even when it's not, due to rounding errors.
29392
+ if (Number.isInteger(a) || a === 0 || a > 1e12) {
29393
+ return null;
29394
+ }
29395
+
29396
+ // Figure out the coefficient before the π.
29397
+ // Example: If a = π/2, then piCoefficient = 0.5
29398
+ // Example: If a = 2π, then piCoefficient = 2
29399
+ const piCoefficient = a / Math.PI;
29400
+ // Truncate the coefficient to account for floating point errors.
29401
+ // π calculations are only accurate up to precision of 1e-12.
29402
+ const truncatedCoefficient = parseFloat(piCoefficient.toFixed(12));
29403
+
29404
+ // If the coefficient is already an integer, then the number
29405
+ // is a multiple of π. Return it here.
29406
+ // Example: If a = 2π, then truncatedCoefficient = 2, so return "2π"
29407
+ if (Number.isInteger(truncatedCoefficient)) {
29408
+ // Return "π" rather than "1π".
29409
+ if (truncatedCoefficient === 1) {
29410
+ return "π";
29411
+ }
29412
+
29413
+ // Return "-π" rather than "-1π".
29414
+ if (truncatedCoefficient === -1) {
29415
+ return "-π";
29416
+ }
29417
+ return truncatedCoefficient + "π";
29418
+ }
29419
+
29420
+ // If the coefficient is not an integer, then we need to
29421
+ // check if it's a multiple of π/2, π/3, π/4, or π/6.
29422
+ // These are π-based values on a unit circle.
29423
+ const acceptableDivisors = [2, 3, 4, 6];
29424
+
29425
+ // Loop through the acceptable divisors and check if the
29426
+ // coefficient is a multiple of (1/divisor).
29427
+ // Example: If a = 5π/6, the coeff is 5/6, which is a multiple of 1/6.
29428
+ // So we return "5π/6".
29429
+ for (const divisor of acceptableDivisors) {
29430
+ // Check if the coefficient is a multiple of (1/divisor) by
29431
+ // multiplying the coefficient by the divisor and checking if
29432
+ // the result is an integer.
29433
+ // Example: If a = 5π/6, then piCoefficient = 5/6. We multiply
29434
+ // by 6 (divisor) to get 5, which is an integer.
29435
+ const coefficientNumerator = parseFloat(
29436
+ // π calculations are only accurate up to precision of 1e-12.
29437
+ (piCoefficient * divisor).toFixed(12));
29438
+ if (Number.isInteger(coefficientNumerator)) {
29439
+ // Handle the case where the coefficient numberator is just 1.
29440
+ // We don't want to write "π/6" as "1π/6"
29441
+ if (coefficientNumerator === 1) {
29442
+ return "π/" + divisor;
29443
+ }
29444
+
29445
+ // Handle the case where the coefficient numberator is just -1.
29446
+ // We don't want to write "π/6" as "-1π/6"
29447
+ if (coefficientNumerator === -1) {
29448
+ return "-π/" + divisor;
29449
+ }
29450
+ return coefficientNumerator + "π/" + divisor;
29451
+ }
29452
+ }
29453
+ return null;
29454
+ }
29455
+
29545
29456
  /**
29546
29457
  * Given a ray and a rectangular box, find the point where the ray intersects
29547
29458
  * the edge of the box. Assumes the `initialPoint` is inside the box.
@@ -29713,6 +29624,95 @@ a, b, c) {
29713
29624
  return [x1, x2];
29714
29625
  }
29715
29626
 
29627
+ /**
29628
+ * Given a list of points, return the angle made by the point at index i
29629
+ * with the points at indices i - 1 and i + 1.
29630
+ */
29631
+ function getAngleFromPoints(points, i) {
29632
+ // Make sure the index is valid.
29633
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
29634
+ return null;
29635
+ }
29636
+
29637
+ // If there are fewer than 3 points, there is no angle.
29638
+ if (points.length < 3) {
29639
+ return null;
29640
+ }
29641
+
29642
+ // Get the points at the current index and the two adjacent points.
29643
+ const point = points.at(i);
29644
+ const pt1 = points.at(i - 1);
29645
+ const pt2 = points[(i + 1) % points.length];
29646
+ if (!point || !pt1 || !pt2) {
29647
+ return null;
29648
+ }
29649
+
29650
+ // Get the lengths of adjacent sides, making a triangle
29651
+ // we can use to calculate the angle.
29652
+ const a = vec.dist(point, pt1);
29653
+ const b = vec.dist(point, pt2);
29654
+ const c = vec.dist(pt1, pt2);
29655
+
29656
+ // Law of cosines
29657
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29658
+ return angle;
29659
+ }
29660
+ function getSideLengthsFromPoints(points, i, isPolygonOpen) {
29661
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
29662
+ return [];
29663
+ }
29664
+ if (points.length < 2) {
29665
+ return [];
29666
+ }
29667
+ const returnArray = [];
29668
+ const point = points[i];
29669
+ // If this point is the first point, then side 1 is the
29670
+ // last point in the list.
29671
+ const point1Index = i === 0 ? points.length - 1 : i - 1;
29672
+ const point1 = points[point1Index];
29673
+ // Make sure the previous point is not the same
29674
+ // as the current point.
29675
+ const side1 = i !== point1Index ? vec.dist(point, point1) : null;
29676
+ // The first side of the first point does not exist if
29677
+ // the polygon is open.
29678
+ if (side1 && !(isPolygonOpen && i === 0)) {
29679
+ returnArray.push({
29680
+ pointIndex: point1Index,
29681
+ sideLength: side1
29682
+ });
29683
+ }
29684
+
29685
+ // See if there is a side 2.
29686
+ const point2Index = (i + 1) % points.length;
29687
+ const point2 = points[point2Index];
29688
+ // Make sure that the next point is not the same as the
29689
+ // current point, and don't repeat the first point.
29690
+ const side2 = i !== point2Index && point2Index !== point1Index ? vec.dist(point, point2) : null;
29691
+ // The second side of the last point does not exist if
29692
+ // the polygon is open.
29693
+ if (side2 && !(isPolygonOpen && i === points.length - 1)) {
29694
+ returnArray.push({
29695
+ pointIndex: point2Index,
29696
+ sideLength: side2
29697
+ });
29698
+ }
29699
+ return returnArray;
29700
+ }
29701
+
29702
+ /**
29703
+ * Determine whether to determine the string with the
29704
+ * exact side length or the approximate side length.
29705
+ */
29706
+ function getPolygonSideString(sideLength, pointIndex, strings, locale) {
29707
+ return Number.isInteger(sideLength) ? strings.srPolygonSideLength({
29708
+ pointNum: pointIndex + 1,
29709
+ length: `${sideLength}`
29710
+ }) : strings.srPolygonSideLengthApprox({
29711
+ pointNum: pointIndex + 1,
29712
+ length: srFormatNumber(sideLength, locale, 1)
29713
+ });
29714
+ }
29715
+
29716
29716
  const {
29717
29717
  calculateAngleInDegrees: calculateAngleInDegrees$2
29718
29718
  } = angles;
@@ -30158,7 +30158,7 @@ const SvgDefs = () => /*#__PURE__*/React__default.createElement("defs", null, /*
30158
30158
  })));
30159
30159
 
30160
30160
  const {
30161
- clockwise: clockwise$1
30161
+ clockwise
30162
30162
  } = geometry;
30163
30163
  const {
30164
30164
  getAngleFromVertex: getAngleFromVertex$1
@@ -30172,7 +30172,7 @@ const PolygonAngle = ({
30172
30172
  snapTo
30173
30173
  }) => {
30174
30174
  const [centerX, centerY] = centerPoint;
30175
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
30175
+ const areClockwise = clockwise([centerPoint, ...endPoints]);
30176
30176
  const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
30177
30177
  const radius = 0.3;
30178
30178
  const a = vec.dist(centerPoint, endPoints[0]);
@@ -30244,7 +30244,7 @@ const Angle = ({
30244
30244
  range
30245
30245
  }) => {
30246
30246
  // Check if the points are clockwise or not, depending on whether we allow reflex angles
30247
- const areClockwise = clockwise$1([...coords, vertex]);
30247
+ const areClockwise = clockwise([...coords, vertex]);
30248
30248
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
30249
30249
 
30250
30250
  // Reverse the coordinates accordingly to ensure the angle is calculated correctly
@@ -30608,6 +30608,34 @@ function range(min, max, step = 1) {
30608
30608
  return result;
30609
30609
  }
30610
30610
 
30611
+ function Hairlines(props) {
30612
+ const {
30613
+ point
30614
+ } = props;
30615
+ const {
30616
+ range
30617
+ } = useGraphConfig();
30618
+ const [[xMin, xMax], [yMin, yMax]] = range;
30619
+ const [[x, y]] = useTransformVectorsToPixels(point);
30620
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
30621
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
30622
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
30623
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
30624
+ return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
30625
+ x1: verticalStartX,
30626
+ y1: y,
30627
+ x2: verticalEndX,
30628
+ y2: y,
30629
+ stroke: color.blue
30630
+ }), /*#__PURE__*/React.createElement("line", {
30631
+ x1: x,
30632
+ y1: horizontalStartY,
30633
+ x2: x,
30634
+ y2: horizontalEndY,
30635
+ stroke: color.blue
30636
+ }));
30637
+ }
30638
+
30611
30639
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
30612
30640
  // graph.
30613
30641
  const hitboxSizePx = 48;
@@ -30618,7 +30646,6 @@ const hitboxSizePx = 48;
30618
30646
  const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30619
30647
  var _Object$entries$find$, _Object$entries$find;
30620
30648
  const {
30621
- range,
30622
30649
  markings,
30623
30650
  showTooltips
30624
30651
  } = useGraphConfig();
@@ -30626,6 +30653,7 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30626
30653
  point,
30627
30654
  color: color$1 = color.blue,
30628
30655
  dragging,
30656
+ focused,
30629
30657
  cursor,
30630
30658
  showFocusRing,
30631
30659
  onClick = () => {}
@@ -30636,25 +30664,7 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30636
30664
  const wbColorName = (_Object$entries$find$ = (_Object$entries$find = Object.entries(color).find(([_, value]) => value === color$1)) == null ? void 0 : _Object$entries$find[0]) != null ? _Object$entries$find$ : "blue";
30637
30665
  const pointClasses = classNames("movable-point", dragging && "movable-point--dragging", showFocusRing && "movable-point--focus");
30638
30666
  const [[x, y]] = useTransformVectorsToPixels(point);
30639
- const [[xMin, xMax], [yMin, yMax]] = range;
30640
- const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
30641
- const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
30642
- const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
30643
- const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
30644
- const showHairlines = dragging && markings !== "none";
30645
- const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
30646
- x1: verticalStartX,
30647
- y1: y,
30648
- x2: verticalEndX,
30649
- y2: y,
30650
- stroke: color$1
30651
- }), /*#__PURE__*/React.createElement("line", {
30652
- x1: x,
30653
- y1: horizontalStartY,
30654
- x2: x,
30655
- y2: horizontalEndY,
30656
- stroke: color$1
30657
- }));
30667
+ const showHairlines = (dragging || focused) && markings !== "none";
30658
30668
  const svgForPoint = /*#__PURE__*/React.createElement("g", {
30659
30669
  ref: hitboxRef,
30660
30670
  className: pointClasses,
@@ -30690,7 +30700,9 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30690
30700
  },
30691
30701
  "data-testid": "movable-point__center"
30692
30702
  }));
30693
- return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
30703
+ return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && /*#__PURE__*/React.createElement(Hairlines, {
30704
+ point: point
30705
+ }), showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
30694
30706
  autoUpdate: true,
30695
30707
  backgroundColor: wbColorName,
30696
30708
  content: `(${point[X]}, ${point[Y]})`,
@@ -30784,6 +30796,7 @@ function useControlPoint(params) {
30784
30796
  },
30785
30797
  point: point,
30786
30798
  dragging: dragging,
30799
+ focused: focused,
30787
30800
  color: color,
30788
30801
  ref: visiblePointRef,
30789
30802
  showFocusRing: focused
@@ -31000,13 +31013,13 @@ const {
31000
31013
  getClockwiseAngle: getClockwiseAngle$2,
31001
31014
  polar: polar$1
31002
31015
  } = angles;
31003
- function renderAngleGraph(state, dispatch) {
31016
+ function renderAngleGraph(state, dispatch, i18n) {
31004
31017
  return {
31005
31018
  graph: /*#__PURE__*/React.createElement(AngleGraph, {
31006
31019
  graphState: state,
31007
31020
  dispatch: dispatch
31008
31021
  }),
31009
- interactiveElementsDescription: null
31022
+ interactiveElementsDescription: getAngleGraphDescription(state, i18n)
31010
31023
  };
31011
31024
  }
31012
31025
  function AngleGraph(props) {
@@ -31017,6 +31030,9 @@ function AngleGraph(props) {
31017
31030
  const {
31018
31031
  graphDimensionsInPixels
31019
31032
  } = useGraphConfig();
31033
+ const i18n = usePerseusI18n();
31034
+ const id = React.useId();
31035
+ const descriptionId = id + "-description";
31020
31036
  const {
31021
31037
  coords,
31022
31038
  showAngles,
@@ -31065,76 +31081,106 @@ function AngleGraph(props) {
31065
31081
  showAngles: showAngles || false // Whether to show the angle or not
31066
31082
  };
31067
31083
 
31084
+ // Aria strings
31068
31085
  const {
31069
- strings,
31070
- locale
31071
- } = usePerseusI18n();
31072
- const angleMeasure = srFormatNumber(getClockwiseAngle$2([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
31073
- const wholeAngleAriaLabel = strings.srAngleGraphAriaLabel;
31074
- const wholeAngleDescription = strings.srAngleGraphAriaDescription({
31075
- angleMeasure,
31076
- vertexX: srFormatNumber(coords[1][X], locale),
31077
- vertexY: srFormatNumber(coords[1][Y], locale),
31078
- startingSideX: srFormatNumber(coords[2][X], locale),
31079
- startingSideY: srFormatNumber(coords[2][Y], locale),
31080
- endingSideX: srFormatNumber(coords[0][X], locale),
31081
- endingSideY: srFormatNumber(coords[0][Y], locale)
31082
- });
31083
- const formatCoordinates = (x, y) => ({
31084
- x: srFormatNumber(x, locale),
31085
- y: srFormatNumber(y, locale)
31086
- });
31087
- const formatSideAriaLabel = (side, newPoint) => {
31088
- const formattedCoordinates = formatCoordinates(newPoint[X], newPoint[Y]);
31089
- const label = side === "terminal" ? strings.srAngleSideAtCoordinates(_extends({
31090
- point: 1,
31091
- side: "terminal side"
31092
- }, formattedCoordinates)) : strings.srAngleSideAtCoordinates(_extends({
31093
- point: 3,
31094
- side: "initial side"
31095
- }, formattedCoordinates));
31096
- return label;
31097
- };
31098
- const formatVertexAriaLabel = newPoint => {
31099
- const formattedVertexCoordinates = formatCoordinates(newPoint[X], newPoint[Y]);
31100
- const label = showAngles ? strings.srAngleVertexAtCoordinatesWithAngleMeasure(_extends({}, formattedVertexCoordinates, {
31101
- angleMeasure: angleMeasure
31102
- })) : strings.srAngleSideAtCoordinates(_extends({
31103
- point: 2,
31104
- side: "vertex"
31105
- }, formattedVertexCoordinates));
31106
- return label;
31107
- };
31108
- const initialSideAriaLabel = formatSideAriaLabel("initial", coords[2]);
31109
- const terminalSideAriaLabel = formatSideAriaLabel("terminal", coords[0]);
31110
- const vertexAriaLabel = formatVertexAriaLabel(coords[1]);
31086
+ srAngleGraphAriaLabel,
31087
+ srAngleGraphAriaDescription,
31088
+ srAngleStartingSide,
31089
+ srAngleEndingSide,
31090
+ srAngleVertex
31091
+ } = describeAngleGraph(graphState, i18n);
31111
31092
 
31112
31093
  // Render the lines, angle, and then movable points
31113
31094
  return /*#__PURE__*/React.createElement("g", {
31114
- "aria-label": wholeAngleAriaLabel,
31115
- "aria-describedby": "angle-description"
31095
+ "aria-label": srAngleGraphAriaLabel,
31096
+ "aria-describedby": descriptionId
31116
31097
  }, svgLines, /*#__PURE__*/React.createElement(Angle, angleParams), /*#__PURE__*/React.createElement(MovablePoint$1, {
31117
31098
  point: coords[1],
31118
31099
  sequenceNumber: 1,
31119
31100
  constrain: p => p,
31120
31101
  onMove: destination => dispatch(actions.angle.movePoint(1, destination)),
31121
- ariaLabel: vertexAriaLabel
31102
+ ariaLabel: srAngleVertex
31122
31103
  }), /*#__PURE__*/React.createElement(MovablePoint$1, {
31123
31104
  point: coords[0],
31124
31105
  sequenceNumber: 2,
31125
31106
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
31126
31107
  onMove: destination => dispatch(actions.angle.movePoint(0, destination)),
31127
- ariaLabel: terminalSideAriaLabel
31108
+ ariaLabel: srAngleEndingSide
31128
31109
  }), /*#__PURE__*/React.createElement(MovablePoint$1, {
31129
31110
  point: coords[2],
31130
31111
  sequenceNumber: 3,
31131
31112
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
31132
31113
  onMove: destination => dispatch(actions.angle.movePoint(2, destination)),
31133
- ariaLabel: initialSideAriaLabel
31114
+ ariaLabel: srAngleStartingSide
31134
31115
  }), /*#__PURE__*/React.createElement("g", {
31135
- id: "angle-description",
31116
+ id: descriptionId,
31136
31117
  style: a11y.srOnly
31137
- }, wholeAngleDescription));
31118
+ }, srAngleGraphAriaDescription));
31119
+ }
31120
+ function getAngleGraphDescription(state, i18n) {
31121
+ const {
31122
+ strings,
31123
+ locale
31124
+ } = i18n;
31125
+ const {
31126
+ coords
31127
+ } = state;
31128
+ return strings.srInteractiveElements({
31129
+ elements: strings.srAngleInteractiveElements({
31130
+ vertexX: srFormatNumber(coords[1][X], locale),
31131
+ vertexY: srFormatNumber(coords[1][Y], locale),
31132
+ startingSideX: srFormatNumber(coords[2][X], locale),
31133
+ startingSideY: srFormatNumber(coords[2][Y], locale),
31134
+ endingSideX: srFormatNumber(coords[0][X], locale),
31135
+ endingSideY: srFormatNumber(coords[0][Y], locale)
31136
+ })
31137
+ });
31138
+ }
31139
+ function describeAngleGraph(state, i18n) {
31140
+ const {
31141
+ strings,
31142
+ locale
31143
+ } = i18n;
31144
+ const {
31145
+ coords,
31146
+ allowReflexAngles,
31147
+ showAngles
31148
+ } = state;
31149
+ const [endingSide, vertex, startingSide] = coords;
31150
+ const angleMeasure = srFormatNumber(getClockwiseAngle$2(coords, allowReflexAngles), locale);
31151
+ const srAngleGraphAriaLabel = strings.srAngleGraphAriaLabel;
31152
+ const srAngleGraphAriaDescription = strings.srAngleGraphAriaDescription({
31153
+ angleMeasure,
31154
+ vertexX: srFormatNumber(vertex[X], locale),
31155
+ vertexY: srFormatNumber(vertex[Y], locale),
31156
+ startingSideX: srFormatNumber(startingSide[X], locale),
31157
+ startingSideY: srFormatNumber(startingSide[Y], locale),
31158
+ endingSideX: srFormatNumber(endingSide[X], locale),
31159
+ endingSideY: srFormatNumber(endingSide[Y], locale)
31160
+ });
31161
+ const srAngleStartingSide = strings.srAngleStartingSide({
31162
+ x: srFormatNumber(startingSide[X], locale),
31163
+ y: srFormatNumber(startingSide[Y], locale)
31164
+ });
31165
+ const srAngleEndingSide = strings.srAngleEndingSide({
31166
+ x: srFormatNumber(endingSide[X], locale),
31167
+ y: srFormatNumber(endingSide[Y], locale)
31168
+ });
31169
+ const srAngleVertex = showAngles ? strings.srAngleVertexWithAngleMeasure({
31170
+ x: srFormatNumber(vertex[X], locale),
31171
+ y: srFormatNumber(vertex[Y], locale),
31172
+ angleMeasure
31173
+ }) : strings.srAngleVertex({
31174
+ x: srFormatNumber(vertex[X], locale),
31175
+ y: srFormatNumber(vertex[Y], locale)
31176
+ });
31177
+ return {
31178
+ srAngleGraphAriaLabel,
31179
+ srAngleGraphAriaDescription,
31180
+ srAngleStartingSide,
31181
+ srAngleEndingSide,
31182
+ srAngleVertex
31183
+ };
31138
31184
  }
31139
31185
  const positiveX = [1, 0];
31140
31186
  const negativeX = [-1, 0];
@@ -31177,9 +31223,6 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
31177
31223
  };
31178
31224
  }
31179
31225
 
31180
- const {
31181
- clockwise
31182
- } = geometry;
31183
31226
  function getGradableGraph(state, initialGraph) {
31184
31227
  if (!state.hasBeenInteractedWith) {
31185
31228
  return _extends({}, initialGraph);
@@ -31243,16 +31286,8 @@ function getGradableGraph(state, initialGraph) {
31243
31286
  });
31244
31287
  }
31245
31288
  if (state.type === "angle" && initialGraph.type === "angle") {
31246
- // We're going to reverse the coords for scoring if the angle is clockwise and
31247
- // we don't allow reflex angles. This is because the angle is largely defined
31248
- // by the order of the points in the coords array, and we want to maintain
31249
- // the same angle scoring with the legacy graph (which had a bug).
31250
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
31251
- const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
31252
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
31253
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
31254
31289
  return _extends({}, initialGraph, {
31255
- coords,
31290
+ coords: state.coords,
31256
31291
  allowReflexAngles: state.allowReflexAngles
31257
31292
  });
31258
31293
  }
@@ -31276,17 +31311,16 @@ function getRadius(graph) {
31276
31311
  return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
31277
31312
  }
31278
31313
 
31279
- function renderCircleGraph(state, dispatch) {
31314
+ function renderCircleGraph(state, dispatch, i18n) {
31280
31315
  return {
31281
31316
  graph: /*#__PURE__*/React.createElement(CircleGraph, {
31282
31317
  graphState: state,
31283
31318
  dispatch: dispatch
31284
31319
  }),
31285
- interactiveElementsDescription: /*#__PURE__*/React.createElement(CircleGraphDescription, {
31286
- state: state
31287
- })
31320
+ interactiveElementsDescription: getCircleGraphDescription(state, i18n)
31288
31321
  };
31289
31322
  }
31323
+ // Exported for testing
31290
31324
  function CircleGraph(props) {
31291
31325
  const {
31292
31326
  dispatch,
@@ -31379,6 +31413,7 @@ function MovableCircle(props) {
31379
31413
  snapStep,
31380
31414
  disableKeyboardInteraction
31381
31415
  } = useGraphConfig();
31416
+ const [focused, setFocused] = React.useState(false);
31382
31417
  const draggableRef = useRef(null);
31383
31418
  const {
31384
31419
  dragging
@@ -31397,7 +31432,9 @@ function MovableCircle(props) {
31397
31432
  ref: draggableRef,
31398
31433
  role: "button",
31399
31434
  tabIndex: disableKeyboardInteraction ? -1 : 0,
31400
- className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
31435
+ className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`,
31436
+ onFocus: () => setFocused(true),
31437
+ onBlur: () => setFocused(false)
31401
31438
  }, /*#__PURE__*/React.createElement("ellipse", {
31402
31439
  className: "focus-ring",
31403
31440
  cx: centerPx[X],
@@ -31412,19 +31449,29 @@ function MovableCircle(props) {
31412
31449
  rx: radiiPx[X],
31413
31450
  ry: radiiPx[Y]
31414
31451
  }), /*#__PURE__*/React.createElement(DragHandle, {
31415
- center: center
31452
+ center: center,
31453
+ dragging: dragging,
31454
+ focused: focused
31416
31455
  }));
31417
31456
  }
31418
31457
  const dragHandleDimensions = [24, 14];
31419
31458
  const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
31420
31459
  function DragHandle(props) {
31421
31460
  const {
31422
- center
31461
+ center,
31462
+ dragging,
31463
+ focused
31423
31464
  } = props;
31424
- const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31425
31465
  const [centerPx] = useTransformVectorsToPixels(center);
31466
+ const {
31467
+ markings
31468
+ } = useGraphConfig();
31469
+ const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31426
31470
  const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
31427
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
31471
+ const showHairlines = (dragging || focused) && markings !== "none";
31472
+ return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && /*#__PURE__*/React.createElement(Hairlines, {
31473
+ point: center
31474
+ }), /*#__PURE__*/React.createElement("rect", {
31428
31475
  className: "movable-circle-handle",
31429
31476
  x: topLeft[X],
31430
31477
  y: topLeft[Y],
@@ -31451,13 +31498,7 @@ function crossProduct(as, bs) {
31451
31498
  }
31452
31499
  return result;
31453
31500
  }
31454
- function CircleGraphDescription({
31455
- state
31456
- }) {
31457
- // The reason that CircleGraphDescription is a component (rather than a
31458
- // function that returns a string) is because it needs to use a
31459
- // hook: `usePerseusI18n`.
31460
- const i18n = usePerseusI18n();
31501
+ function getCircleGraphDescription(state, i18n) {
31461
31502
  const strings = describeCircleGraph(state, i18n);
31462
31503
  return strings.srCircleInteractiveElement;
31463
31504
  }
@@ -31510,15 +31551,13 @@ function describeCircleGraph(state, i18n) {
31510
31551
  };
31511
31552
  }
31512
31553
 
31513
- function renderLinearGraph(state, dispatch) {
31554
+ function renderLinearGraph(state, dispatch, i18n) {
31514
31555
  return {
31515
31556
  graph: /*#__PURE__*/React.createElement(LinearGraph, {
31516
31557
  graphState: state,
31517
31558
  dispatch: dispatch
31518
31559
  }),
31519
- interactiveElementsDescription: /*#__PURE__*/React.createElement(LinearGraphDescription, {
31520
- state: state
31521
- })
31560
+ interactiveElementsDescription: getLinearGraphDescription(state, i18n)
31522
31561
  };
31523
31562
  }
31524
31563
  const LinearGraph = (props, key) => {
@@ -31583,13 +31622,7 @@ const LinearGraph = (props, key) => {
31583
31622
  style: a11y.srOnly
31584
31623
  }, slopeString));
31585
31624
  };
31586
- function LinearGraphDescription({
31587
- state
31588
- }) {
31589
- // The reason that LinearGraphDescription is a component (rather than a
31590
- // function that returns a string) is because it needs to use a
31591
- // hook: `usePerseusI18n`.
31592
- const i18n = usePerseusI18n();
31625
+ function getLinearGraphDescription(state, i18n) {
31593
31626
  const strings = describeLinearGraph(state, i18n);
31594
31627
  return strings.srLinearInteractiveElement;
31595
31628
  }
@@ -31633,15 +31666,13 @@ function describeLinearGraph(state, i18n) {
31633
31666
  };
31634
31667
  }
31635
31668
 
31636
- function renderLinearSystemGraph(state, dispatch) {
31669
+ function renderLinearSystemGraph(state, dispatch, i18n) {
31637
31670
  return {
31638
31671
  graph: /*#__PURE__*/React.createElement(LinearSystemGraph, {
31639
31672
  graphState: state,
31640
31673
  dispatch: dispatch
31641
31674
  }),
31642
- interactiveElementsDescription: /*#__PURE__*/React.createElement(LinearSystemGraphDescription, {
31643
- state: state
31644
- })
31675
+ interactiveElementsDescription: getLinearSystemGraphDescription(state, i18n)
31645
31676
  };
31646
31677
  }
31647
31678
  const LinearSystemGraph = props => {
@@ -31728,18 +31759,9 @@ const LinearSystemGraph = props => {
31728
31759
  style: a11y.srOnly
31729
31760
  }, slopeDescription))));
31730
31761
  };
31731
- function LinearSystemGraphDescription({
31732
- state
31733
- }) {
31734
- // The reason that LinearSystemGraphDescription is a component (rather
31735
- // than a function that returns a string) is because it needs to use a
31736
- // hook: `usePerseusI18n`.
31737
- const i18n = usePerseusI18n();
31738
- return describeLinearSystemGraph(state, i18n);
31739
- }
31740
31762
 
31741
31763
  // Exported for testing
31742
- function describeLinearSystemGraph(state, i18n) {
31764
+ function getLinearSystemGraphDescription(state, i18n) {
31743
31765
  const {
31744
31766
  strings,
31745
31767
  locale
@@ -31765,15 +31787,13 @@ function describeLinearSystemGraph(state, i18n) {
31765
31787
  });
31766
31788
  }
31767
31789
 
31768
- function renderPointGraph(state, dispatch) {
31790
+ function renderPointGraph(state, dispatch, i18n) {
31769
31791
  return {
31770
31792
  graph: /*#__PURE__*/React.createElement(PointGraph, {
31771
31793
  graphState: state,
31772
31794
  dispatch: dispatch
31773
31795
  }),
31774
- interactiveElementsDescription: /*#__PURE__*/React.createElement(PointGraphDescription, {
31775
- state: state
31776
- })
31796
+ interactiveElementsDescription: getPointGraphDescription(state, i18n)
31777
31797
  };
31778
31798
  }
31779
31799
  function PointGraph(props) {
@@ -31796,7 +31816,9 @@ function PointGraph(props) {
31796
31816
  var _pointsRef$current$fo;
31797
31817
  (_pointsRef$current$fo = pointsRef.current[focusedIndex]) == null || _pointsRef$current$fo.focus();
31798
31818
  }
31799
- }, [props.graphState.focusedPointIndex, pointsRef]);
31819
+ }, [props.graphState.focusedPointIndex,
31820
+ // Ensure that we re-focus if a point is deleted.
31821
+ props.graphState.coords.length, pointsRef]);
31800
31822
  const statefulProps = _extends({}, props, {
31801
31823
  graphConfig,
31802
31824
  pointsRef,
@@ -31867,17 +31889,9 @@ function UnlimitedPointGraph(statefulProps) {
31867
31889
  }
31868
31890
  })));
31869
31891
  }
31870
- function PointGraphDescription({
31871
- state
31872
- }) {
31873
- // PointGraphDescription needs to `usePerseusI18n`, so it has to be a
31874
- // component rather than a function that simply returns a string.
31875
- const i18n = usePerseusI18n();
31876
- return describePointGraph(state, i18n);
31877
- }
31878
31892
 
31879
31893
  // Exported for testing
31880
- function describePointGraph(state, i18n) {
31894
+ function getPointGraphDescription(state, i18n) {
31881
31895
  const {
31882
31896
  strings,
31883
31897
  locale
@@ -31891,17 +31905,20 @@ function describePointGraph(state, i18n) {
31891
31905
  y: srFormatNumber(y, locale)
31892
31906
  }));
31893
31907
  return strings.srInteractiveElements({
31894
- elements: pointDescriptions.join(", ")
31908
+ elements: pointDescriptions.join(" ")
31895
31909
  });
31896
31910
  }
31897
31911
 
31898
- function renderPolygonGraph(state, dispatch) {
31912
+ const {
31913
+ convertRadiansToDegrees
31914
+ } = angles;
31915
+ function renderPolygonGraph(state, dispatch, i18n, markings) {
31899
31916
  return {
31900
31917
  graph: /*#__PURE__*/React.createElement(PolygonGraph, {
31901
31918
  graphState: state,
31902
31919
  dispatch: dispatch
31903
31920
  }),
31904
- interactiveElementsDescription: null
31921
+ interactiveElementsDescription: getPolygonGraphDescription(state, i18n, markings)
31905
31922
  };
31906
31923
  }
31907
31924
  const PolygonGraph = props => {
@@ -31961,7 +31978,9 @@ const PolygonGraph = props => {
31961
31978
  var _pointsRef$current$fo;
31962
31979
  (_pointsRef$current$fo = pointsRef.current[focusedIndex]) == null || _pointsRef$current$fo.focus();
31963
31980
  }
31964
- }, [props.graphState.focusedPointIndex, pointsRef]);
31981
+ }, [props.graphState.focusedPointIndex,
31982
+ // Ensure that we re-focus if a point is deleted.
31983
+ props.graphState.coords.length, pointsRef]);
31965
31984
 
31966
31985
  // If the unlimited polygon is rendered with 3 or more coordinates
31967
31986
  // Close the polygon, but only on first render.
@@ -31986,7 +32005,7 @@ const PolygonGraph = props => {
31986
32005
  focusVisible,
31987
32006
  setFocusVisible
31988
32007
  });
31989
- return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
32008
+ return numSides === "unlimited" ? /*#__PURE__*/React.createElement(UnlimitedPolygonGraph, statefulProps) : /*#__PURE__*/React.createElement(LimitedPolygonGraph, statefulProps);
31990
32009
  };
31991
32010
  const LimitedPolygonGraph = statefulProps => {
31992
32011
  const {
@@ -32011,8 +32030,39 @@ const LimitedPolygonGraph = statefulProps => {
32011
32030
  const {
32012
32031
  disableKeyboardInteraction
32013
32032
  } = graphConfig;
32033
+ const {
32034
+ strings,
32035
+ locale
32036
+ } = usePerseusI18n();
32037
+ const id = React.useId();
32038
+ const pointsOffArray = Array(points.length).fill("off");
32039
+ // When moving an element, set its aria-live to "polite" and the others
32040
+ // to "off". Otherwise, other connected elements that move at the same
32041
+ // time might override the currently focused element's aria live.
32042
+ const [ariaLives, setAriaLives] = React.useState([
32043
+ // First one represents the aria-live value for the polygon itself.
32044
+ "off",
32045
+ // The rest represent the points.
32046
+ ...pointsOffArray]);
32014
32047
  const lines = getLines(points);
32015
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polygon, {
32048
+ const polygonPointsNumId = id + "-points-num";
32049
+ const polygonPointsId = id + "-points";
32050
+
32051
+ // Aria label srings
32052
+ const {
32053
+ srPolygonGraph,
32054
+ srPolygonGraphPointsNum,
32055
+ srPolygonGraphPoints,
32056
+ srPolygonElementsNum
32057
+ } = describePolygonGraph(statefulProps.graphState, {
32058
+ strings,
32059
+ locale
32060
+ }, statefulProps.graphConfig.markings);
32061
+ return /*#__PURE__*/React.createElement("g", {
32062
+ // Outer graph minimal description
32063
+ "aria-label": srPolygonGraph,
32064
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32065
+ }, /*#__PURE__*/React.createElement(Polygon, {
32016
32066
  points: [...points],
32017
32067
  color: "var(--movable-line-stroke-color)",
32018
32068
  svgPolygonProps: {
@@ -32037,13 +32087,17 @@ const LimitedPolygonGraph = statefulProps => {
32037
32087
  snapTo: snapTo
32038
32088
  });
32039
32089
  }), showSides && lines.map(([start, end], i) => {
32090
+ // Use x and y to find the position of the label
32040
32091
  const [x, y] = vec.midpoint(start, end);
32041
- const length = parseFloat(vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
32092
+ const length = vec.dist(start, end);
32093
+ // Check if the length needs to indicate
32094
+ // that it's an approximation.
32095
+ const isApprox = !Number.isInteger(length);
32042
32096
  return /*#__PURE__*/React.createElement(TextLabel, {
32043
32097
  key: "side-" + i,
32044
32098
  x: x,
32045
32099
  y: y
32046
- }, !Number.isInteger(length) && "≈ ", length);
32100
+ }, isApprox ? `≈ ${length.toFixed(snapTo === "sides" ? 0 : 1)}` : length);
32047
32101
  }), /*#__PURE__*/React.createElement(Polygon, {
32048
32102
  points: [...points],
32049
32103
  color: "transparent",
@@ -32063,27 +32117,80 @@ const LimitedPolygonGraph = statefulProps => {
32063
32117
  setFocusVisible(hasFocusVisible(polygonRef.current));
32064
32118
  },
32065
32119
  // Required for lines to darken on focus
32066
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32120
+ onFocus: () => {
32121
+ setFocusVisible(hasFocusVisible(polygonRef.current));
32122
+ setAriaLives(() => ["polite", ...pointsOffArray]);
32123
+ },
32067
32124
  // Required for line weighting to update on blur. Without this,
32068
32125
  // the user has to hover over the shape for it to update
32069
32126
  onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32070
- className: "movable-polygon"
32071
- }
32072
- }), points.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32073
- key: "point-" + i,
32074
- constrain: constrain,
32075
- point: point,
32076
- sequenceNumber: i + 1,
32077
- onMove: destination => {
32078
- const now = Date.now();
32079
- const targetFPS = 40;
32080
- const moveThresholdTime = 1000 / targetFPS;
32081
- if (now - lastMoveTimeRef.current > moveThresholdTime) {
32082
- dispatch(actions.polygon.movePoint(i, destination));
32083
- lastMoveTimeRef.current = now;
32084
- }
32127
+ className: "movable-polygon",
32128
+ // Accessibility-related fields
32129
+ role: "button",
32130
+ "aria-label": srPolygonGraphPoints ? `${srPolygonElementsNum} ${srPolygonGraphPoints}` : srPolygonElementsNum,
32131
+ "aria-live": ariaLives[0]
32085
32132
  }
32086
- })));
32133
+ }), points.map((point, i) => {
32134
+ const angleId = `${id}-angle-${i}`;
32135
+ const side1Id = `${id}-point-${i}-side-1`;
32136
+ const side2Id = `${id}-point-${i}-side-2`;
32137
+
32138
+ // Limited polygons always have an angle at each vertex.
32139
+ const angle = getAngleFromPoints(points, i);
32140
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32141
+
32142
+ // Limited polygons always two sides attached to each vertex.
32143
+ const sidesArray = getSideLengthsFromPoints(points, i);
32144
+ const {
32145
+ pointIndex: point1Index,
32146
+ sideLength: side1Length
32147
+ } = sidesArray[0];
32148
+ const {
32149
+ pointIndex: point2Index,
32150
+ sideLength: side2Length
32151
+ } = sidesArray[1];
32152
+ return /*#__PURE__*/React.createElement("g", {
32153
+ key: "point-" + i
32154
+ }, /*#__PURE__*/React.createElement(MovablePoint$1, {
32155
+ ariaDescribedBy: `${angleId} ${side1Id} ${side2Id}`,
32156
+ ariaLive: ariaLives[i + 1],
32157
+ constrain: constrain,
32158
+ point: point,
32159
+ sequenceNumber: i + 1,
32160
+ onMove: destination => {
32161
+ const now = Date.now();
32162
+ const targetFPS = 40;
32163
+ const moveThresholdTime = 1000 / targetFPS;
32164
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
32165
+ dispatch(actions.polygon.movePoint(i, destination));
32166
+ lastMoveTimeRef.current = now;
32167
+ }
32168
+ },
32169
+ onFocus: () => {
32170
+ const newPointAriaLives = [...pointsOffArray];
32171
+ newPointAriaLives[i] = "polite";
32172
+ // Whole polygon is "off", and the current
32173
+ // point is "polite".
32174
+ setAriaLives(["off", ...newPointAriaLives]);
32175
+ }
32176
+ }), angleDegree && /*#__PURE__*/React.createElement("g", {
32177
+ id: angleId
32178
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32179
+ angle: angleDegree
32180
+ }) : strings.srPolygonPointAngleApprox({
32181
+ angle: srFormatNumber(angleDegree, locale, 1)
32182
+ })), /*#__PURE__*/React.createElement("g", {
32183
+ id: side1Id
32184
+ }, getPolygonSideString(side1Length, point1Index, strings, locale)), /*#__PURE__*/React.createElement("g", {
32185
+ id: side2Id
32186
+ }, getPolygonSideString(side2Length, point2Index, strings, locale)));
32187
+ }), /*#__PURE__*/React.createElement("g", {
32188
+ id: polygonPointsNumId,
32189
+ style: a11y.srOnly
32190
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React.createElement("g", {
32191
+ id: polygonPointsId,
32192
+ style: a11y.srOnly
32193
+ }, srPolygonGraphPoints));
32087
32194
  };
32088
32195
  const UnlimitedPolygonGraph = statefulProps => {
32089
32196
  const {
@@ -32098,6 +32205,19 @@ const UnlimitedPolygonGraph = statefulProps => {
32098
32205
  coords,
32099
32206
  closedPolygon
32100
32207
  } = statefulProps.graphState;
32208
+ const {
32209
+ strings,
32210
+ locale
32211
+ } = usePerseusI18n();
32212
+ const id = React.useId();
32213
+ const polygonPointsNumId = id + "-points-num";
32214
+ const polygonPointsId = id + "-points";
32215
+
32216
+ // When moving an element, set its aria-live to "polite" and the others
32217
+ // to "off". Otherwise, other connected elements that move at the same
32218
+ // time might override the currently focused element's aria live.
32219
+ const pointsOffArray = Array(points.length).fill("off");
32220
+ const [ariaLives, setAriaLives] = React.useState(pointsOffArray);
32101
32221
 
32102
32222
  // If the polygon is closed, return a LimitedPolygon component.
32103
32223
  if (closedPolygon) {
@@ -32111,7 +32231,22 @@ const UnlimitedPolygonGraph = statefulProps => {
32111
32231
  } = graphConfig;
32112
32232
  const widthPx = graphDimensionsInPixels[0];
32113
32233
  const heightPx = graphDimensionsInPixels[1];
32114
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polyline, {
32234
+
32235
+ // Aria label srings
32236
+ const emptyGraph = coords.length === 0;
32237
+ const {
32238
+ srPolygonGraph,
32239
+ srPolygonGraphPointsNum,
32240
+ srPolygonGraphPoints
32241
+ } = describePolygonGraph(statefulProps.graphState, {
32242
+ strings,
32243
+ locale
32244
+ }, statefulProps.graphConfig.markings);
32245
+ return /*#__PURE__*/React.createElement("g", {
32246
+ // Outer graph minimal description
32247
+ "aria-label": emptyGraph ? strings.srUnlimitedPolygonEmpty : srPolygonGraph,
32248
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32249
+ }, /*#__PURE__*/React.createElement(Polyline, {
32115
32250
  points: [...points],
32116
32251
  color: "var(--movable-line-stroke-color)",
32117
32252
  svgPolylineProps: {
@@ -32136,28 +32271,68 @@ const UnlimitedPolygonGraph = statefulProps => {
32136
32271
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
32137
32272
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
32138
32273
  }
32139
- }), coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32140
- key: i,
32141
- point: point,
32142
- sequenceNumber: i + 1,
32143
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32144
- ref: ref => {
32145
- pointsRef.current[i] = ref;
32146
- },
32147
- onFocus: () => {
32148
- dispatch(actions.polygon.focusPoint(i));
32149
- },
32150
- onClick: () => {
32151
- // If the point being clicked is the first point and
32152
- // there's enough non-duplicated points to form
32153
- // a polygon (3 or more), close the shape before
32154
- // setting focus.
32155
- if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32156
- dispatch(actions.polygon.closePolygon());
32157
- }
32158
- dispatch(actions.polygon.clickPoint(i));
32274
+ }), coords.map((point, i) => {
32275
+ const angleId = `${id}-angle-${i}`;
32276
+ let sideIds = "";
32277
+
32278
+ // This point only has an angle if it's not the first
32279
+ // or last point in this open polygon.
32280
+ const hasAngle = i > 0 && i < coords.length - 1;
32281
+ const angle = hasAngle ? getAngleFromPoints(points, i) : null;
32282
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32283
+ const sidesArray = getSideLengthsFromPoints(points, i, true);
32284
+ for (let sideIndex = 0; sideIndex < sidesArray.length; sideIndex++) {
32285
+ sideIds += `${id}-point-${i}-side-${sideIndex} `;
32159
32286
  }
32160
- })));
32287
+ return /*#__PURE__*/React.createElement("g", {
32288
+ key: "point-" + i
32289
+ }, /*#__PURE__*/React.createElement(MovablePoint$1, {
32290
+ ariaDescribedBy: `${angleId} ${sideIds}`,
32291
+ ariaLive: ariaLives[i],
32292
+ point: point,
32293
+ sequenceNumber: i + 1,
32294
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32295
+ ref: ref => {
32296
+ pointsRef.current[i] = ref;
32297
+ },
32298
+ onFocus: () => {
32299
+ dispatch(actions.polygon.focusPoint(i));
32300
+ const newPointAriaLives = [...pointsOffArray];
32301
+ newPointAriaLives[i] = "polite";
32302
+ // Whole polygon is "off", and the current
32303
+ // point is "polite".
32304
+ setAriaLives([...newPointAriaLives]);
32305
+ },
32306
+ onClick: () => {
32307
+ // If the point being clicked is the first point and
32308
+ // there's enough non-duplicated points to form
32309
+ // a polygon (3 or more), close the shape before
32310
+ // setting focus.
32311
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32312
+ dispatch(actions.polygon.closePolygon());
32313
+ }
32314
+ dispatch(actions.polygon.clickPoint(i));
32315
+ }
32316
+ }), angleDegree && /*#__PURE__*/React.createElement("g", {
32317
+ id: angleId
32318
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32319
+ angle: angleDegree
32320
+ }) : strings.srPolygonPointAngleApprox({
32321
+ angle: srFormatNumber(angleDegree, locale, 1)
32322
+ })), sidesArray.map(({
32323
+ pointIndex,
32324
+ sideLength
32325
+ }, j) => /*#__PURE__*/React.createElement("g", {
32326
+ key: `${id}-point-${i}-side-${j}`,
32327
+ id: `${id}-point-${i}-side-${j}`
32328
+ }, getPolygonSideString(sideLength, pointIndex, strings, locale))));
32329
+ }), coords.length > 0 && /*#__PURE__*/React.createElement("g", {
32330
+ id: polygonPointsNumId,
32331
+ style: a11y.srOnly
32332
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React.createElement("g", {
32333
+ id: polygonPointsId,
32334
+ style: a11y.srOnly
32335
+ }, srPolygonGraphPoints));
32161
32336
  };
32162
32337
  function getLines(points) {
32163
32338
  return points.map((point, i) => {
@@ -32179,6 +32354,54 @@ const hasFocusVisible = element => {
32179
32354
  return matches(":focus");
32180
32355
  }
32181
32356
  };
32357
+ function getPolygonGraphDescription(state, i18n, markings) {
32358
+ const strings = describePolygonGraph(state, i18n, markings);
32359
+ return strings.srPolygonInteractiveElements;
32360
+ }
32361
+ function describePolygonGraph(state, i18n, markings) {
32362
+ const {
32363
+ strings,
32364
+ locale
32365
+ } = i18n;
32366
+ const {
32367
+ coords
32368
+ } = state;
32369
+ const isCoordinatePlane = markings === "axes" || markings === "graph";
32370
+ const hasOnePoint = coords.length === 1;
32371
+
32372
+ // Figure out graph aria label based on markings.
32373
+ const srPolygonGraph = isCoordinatePlane ? strings.srPolygonGraphCoordinatePlane : strings.srPolygonGraph;
32374
+ const srPolygonGraphPointsNum = hasOnePoint ? strings.srPolygonGraphPointsOne : strings.srPolygonGraphPointsNum({
32375
+ num: coords.length
32376
+ });
32377
+ let srPolygonGraphPoints;
32378
+ // Figure out graph description based on markings.
32379
+ // If the graph is not on a coordinate plane, we should not include
32380
+ // the points' coordinates in the description.
32381
+ if (isCoordinatePlane) {
32382
+ const pointsString = coords.map((coord, i) => {
32383
+ return strings.srPointAtCoordinates({
32384
+ num: i + 1,
32385
+ x: srFormatNumber(coord[0], locale),
32386
+ y: srFormatNumber(coord[1], locale)
32387
+ });
32388
+ });
32389
+ srPolygonGraphPoints = pointsString.join(" ");
32390
+ }
32391
+ const srPolygonElementsNum = hasOnePoint ? strings.srPolygonElementsOne : strings.srPolygonElementsNum({
32392
+ num: coords.length
32393
+ });
32394
+ const srPolygonInteractiveElements = coords.length > 0 ? strings.srInteractiveElements({
32395
+ elements: [srPolygonElementsNum, srPolygonGraphPoints].join(" ")
32396
+ }) : null;
32397
+ return {
32398
+ srPolygonGraph,
32399
+ srPolygonGraphPointsNum,
32400
+ srPolygonGraphPoints,
32401
+ srPolygonElementsNum,
32402
+ srPolygonInteractiveElements
32403
+ };
32404
+ }
32182
32405
 
32183
32406
  function renderQuadraticGraph(state, dispatch, i18n) {
32184
32407
  return {
@@ -32337,15 +32560,13 @@ function describeQuadraticGraph(state, i18n) {
32337
32560
  };
32338
32561
  }
32339
32562
 
32340
- function renderRayGraph(state, dispatch) {
32563
+ function renderRayGraph(state, dispatch, i18n) {
32341
32564
  return {
32342
32565
  graph: /*#__PURE__*/React.createElement(RayGraph, {
32343
32566
  graphState: state,
32344
32567
  dispatch: dispatch
32345
32568
  }),
32346
- interactiveElementsDescription: /*#__PURE__*/React.createElement(RayGraphDescription, {
32347
- state: state
32348
- })
32569
+ interactiveElementsDescription: getRayGraphDescription(state, i18n)
32349
32570
  };
32350
32571
  }
32351
32572
  const RayGraph = props => {
@@ -32399,13 +32620,7 @@ const RayGraph = props => {
32399
32620
  style: a11y.srOnly
32400
32621
  }, srRayPoints));
32401
32622
  };
32402
- function RayGraphDescription({
32403
- state
32404
- }) {
32405
- // The reason that RayGraphDescription is a component (rather than a
32406
- // function that returns a string) is because it needs to use a
32407
- // hook: `usePerseusI18n`.
32408
- const i18n = usePerseusI18n();
32623
+ function getRayGraphDescription(state, i18n) {
32409
32624
  const strings = describeRayGraph(state, i18n);
32410
32625
  return strings.srRayInteractiveElement;
32411
32626
  }
@@ -32455,15 +32670,13 @@ function describeRayGraph(state, i18n) {
32455
32670
  };
32456
32671
  }
32457
32672
 
32458
- function renderSegmentGraph(state, dispatch) {
32673
+ function renderSegmentGraph(state, dispatch, i18n) {
32459
32674
  return {
32460
32675
  graph: /*#__PURE__*/React.createElement(SegmentGraph, {
32461
32676
  graphState: state,
32462
32677
  dispatch: dispatch
32463
32678
  }),
32464
- interactiveElementsDescription: /*#__PURE__*/React.createElement(SegmentGraphDescription, {
32465
- state: state
32466
- })
32679
+ interactiveElementsDescription: getSegmentGraphDescription(state, i18n)
32467
32680
  };
32468
32681
  }
32469
32682
  const SegmentGraph = ({
@@ -32555,18 +32768,9 @@ const SegmentGraph = ({
32555
32768
  function getLengthOfSegment(segment) {
32556
32769
  return point.distanceToPoint(...segment);
32557
32770
  }
32558
- function SegmentGraphDescription({
32559
- state
32560
- }) {
32561
- // The reason that SegmentGraphDescription is a component (rather than a
32562
- // function that returns a string) is because it needs to use a
32563
- // hook: `usePerseusI18n`.
32564
- const i18n = usePerseusI18n();
32565
- return describeSegmentGraph(state, i18n);
32566
- }
32567
32771
 
32568
32772
  // Exported for testing
32569
- function describeSegmentGraph(state, i18n) {
32773
+ function getSegmentGraphDescription(state, i18n) {
32570
32774
  const {
32571
32775
  strings,
32572
32776
  locale
@@ -32583,13 +32787,13 @@ function describeSegmentGraph(state, i18n) {
32583
32787
  });
32584
32788
  }
32585
32789
 
32586
- function renderSinusoidGraph(state, dispatch) {
32790
+ function renderSinusoidGraph(state, dispatch, i18n) {
32587
32791
  return {
32588
32792
  graph: /*#__PURE__*/React.createElement(SinusoidGraph, {
32589
32793
  graphState: state,
32590
32794
  dispatch: dispatch
32591
32795
  }),
32592
- interactiveElementsDescription: null
32796
+ interactiveElementsDescription: getSinusoidDescription(state, i18n)
32593
32797
  };
32594
32798
  }
32595
32799
  function SinusoidGraph(props) {
@@ -32597,6 +32801,9 @@ function SinusoidGraph(props) {
32597
32801
  dispatch,
32598
32802
  graphState
32599
32803
  } = props;
32804
+ const i18n = usePerseusI18n();
32805
+ const id = React.useId();
32806
+ const descriptionId = id + "-description";
32600
32807
 
32601
32808
  // Destructure the coordinates from the graph state
32602
32809
  // Note: The order of the coordinates is important:
@@ -32621,15 +32828,30 @@ function SinusoidGraph(props) {
32621
32828
  if (coeffs !== undefined) {
32622
32829
  coeffRef.current = coeffs;
32623
32830
  }
32624
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Plot$2.OfX, {
32831
+
32832
+ // Aria strings
32833
+ const {
32834
+ srSinusoidGraph,
32835
+ srSinusoidDescription,
32836
+ srSinusoidRootPoint,
32837
+ srSinusoidPeakPoint
32838
+ } = describeSinusoidGraph(graphState, i18n);
32839
+ return /*#__PURE__*/React.createElement("g", {
32840
+ // Outer graph minimal description
32841
+ "aria-label": srSinusoidGraph,
32842
+ "aria-describedby": descriptionId
32843
+ }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
32625
32844
  y: x => computeSine(x, coeffRef.current),
32626
32845
  color: color.blue
32627
32846
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
32847
+ ariaLabel: i === 0 ? srSinusoidRootPoint : srSinusoidPeakPoint,
32628
32848
  key: "point-" + i,
32629
32849
  point: coord,
32630
32850
  sequenceNumber: i + 1,
32631
32851
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
32632
- })));
32852
+ })), /*#__PURE__*/React.createElement("g", {
32853
+ id: descriptionId
32854
+ }, srSinusoidDescription));
32633
32855
  }
32634
32856
 
32635
32857
  // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
@@ -32667,6 +32889,52 @@ const getSinusoidCoefficients$1 = coords => {
32667
32889
  verticalOffset
32668
32890
  };
32669
32891
  };
32892
+ function getSinusoidDescription(state, i18n) {
32893
+ const strings = describeSinusoidGraph(state, i18n);
32894
+ return strings.srSinusoidInteractiveElements;
32895
+ }
32896
+ function describeSinusoidGraph(state, i18n) {
32897
+ const {
32898
+ strings,
32899
+ locale
32900
+ } = i18n;
32901
+ const {
32902
+ coords
32903
+ } = state;
32904
+ const [root, peak] = coords;
32905
+ const diffX = Math.abs(peak[X] - root[X]);
32906
+ const diffY = Math.abs(peak[Y] - root[Y]);
32907
+ const srSinusoidGraph = strings.srSinusoidGraph;
32908
+ const srSinusoidDescription = strings.srSinusoidDescription({
32909
+ minValue: srFormatNumber(root[Y] - diffY, locale),
32910
+ maxValue: srFormatNumber(root[Y] + diffY, locale),
32911
+ cycleStart: srFormatNumber(root[X] - 2 * diffX, locale),
32912
+ cycleEnd: srFormatNumber(root[X] + 2 * diffX, locale)
32913
+ });
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
+ });
32922
+ const srSinusoidInteractiveElements = strings.srInteractiveElements({
32923
+ elements: strings.srSinusoidInteractiveElements({
32924
+ point1X: srFormatNumber(root[X], locale),
32925
+ point1Y: srFormatNumber(root[Y], locale),
32926
+ point2X: srFormatNumber(peak[X], locale),
32927
+ point2Y: srFormatNumber(peak[Y], locale)
32928
+ })
32929
+ });
32930
+ return {
32931
+ srSinusoidGraph,
32932
+ srSinusoidDescription,
32933
+ srSinusoidRootPoint,
32934
+ srSinusoidPeakPoint,
32935
+ srSinusoidInteractiveElements
32936
+ };
32937
+ }
32670
32938
 
32671
32939
  const {
32672
32940
  calculateAngleInDegrees,
@@ -32837,6 +33105,7 @@ const MafsGraph = props => {
32837
33105
  const descriptionId = `interactive-graph-description-${uniqueId}`;
32838
33106
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
32839
33107
  const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
33108
+ const instructionsId = `instructions-${uniqueId}`;
32840
33109
  const graphRef = React.useRef(null);
32841
33110
  const {
32842
33111
  analytics
@@ -32864,6 +33133,7 @@ const MafsGraph = props => {
32864
33133
  const interactionPrompt = isUnlimitedGraphState(state) && state.showKeyboardInteractionInvitation;
32865
33134
  useOnMountEffect(() => {
32866
33135
  analytics.onAnalyticsEvent({
33136
+ // TODO(LEMS-2827): Remove analytics event in LEMS-2827 in favor of ti below.
32867
33137
  type: "perseus:interactive-graph-widget:rendered",
32868
33138
  payload: {
32869
33139
  type,
@@ -32871,6 +33141,14 @@ const MafsGraph = props => {
32871
33141
  widgetId: "interactive-graph"
32872
33142
  }
32873
33143
  });
33144
+ analytics.onAnalyticsEvent({
33145
+ type: "perseus:widget:rendered:ti",
33146
+ payload: {
33147
+ widgetSubType: type,
33148
+ widgetType: "INTERACTIVE_GRAPH",
33149
+ widgetId: "interactive-graph"
33150
+ }
33151
+ });
32874
33152
  });
32875
33153
  const {
32876
33154
  graph,
@@ -32878,7 +33156,8 @@ const MafsGraph = props => {
32878
33156
  } = renderGraphElements({
32879
33157
  state,
32880
33158
  dispatch,
32881
- i18n
33159
+ i18n,
33160
+ markings: props.markings
32882
33161
  });
32883
33162
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
32884
33163
  value: {
@@ -32913,7 +33192,7 @@ const MafsGraph = props => {
32913
33192
  handleKeyboardEvent(event, state, dispatch);
32914
33193
  },
32915
33194
  "aria-label": fullGraphAriaLabel,
32916
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
33195
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && unlimitedGraphKeyboardPromptId, state.type !== "none" && instructionsId),
32917
33196
  ref: graphRef,
32918
33197
  tabIndex: 0,
32919
33198
  onFocus: event => {
@@ -32930,7 +33209,11 @@ const MafsGraph = props => {
32930
33209
  id: interactiveElementsDescriptionId,
32931
33210
  tabIndex: -1,
32932
33211
  className: "mafs-sr-only"
32933
- }, interactiveElementsDescription), /*#__PURE__*/React.createElement(LegacyGrid, {
33212
+ }, interactiveElementsDescription), state.type !== "none" && /*#__PURE__*/React.createElement(View, {
33213
+ id: instructionsId,
33214
+ tabIndex: -1,
33215
+ className: "mafs-sr-only"
33216
+ }, isUnlimitedGraphState(state) ? strings.srUnlimitedGraphInstructions : strings.srGraphInstructions), /*#__PURE__*/React.createElement(LegacyGrid, {
32934
33217
  box: props.box,
32935
33218
  backgroundImage: props.backgroundImage
32936
33219
  }), /*#__PURE__*/React.createElement(View, {
@@ -32939,7 +33222,21 @@ const MafsGraph = props => {
32939
33222
  bottom: 0,
32940
33223
  left: 0
32941
33224
  }
32942
- }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, null)), /*#__PURE__*/React.createElement(Mafs, {
33225
+ }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisLabels, {
33226
+ i18n: i18n
33227
+ })), /*#__PURE__*/React.createElement(View
33228
+ // If we have locked figures, we set aria-hidden
33229
+ // to false so that screen readers can read the
33230
+ // locked figures. If we don't have any locked
33231
+ // figures, we need to set aria-hidden to true so
33232
+ // that screen readers don't read this Mafs element
33233
+ // as an empty image.
33234
+ // Note: Adding this in a wrapping View element
33235
+ // so that `aria-hidden` is a valid property.
33236
+ // It does not work on `Mafs` or svg elements.
33237
+ , {
33238
+ "aria-hidden": props.lockedFigures && props.lockedFigures.length > 0 ? false : true
33239
+ }, /*#__PURE__*/React.createElement(Mafs, {
32943
33240
  preserveAspectRatio: false,
32944
33241
  viewBox: {
32945
33242
  x: state.range[X],
@@ -32959,10 +33256,10 @@ const MafsGraph = props => {
32959
33256
  height: height
32960
33257
  })),
32961
33258
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
32962
- (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLayer, {
33259
+ (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(AxisTicks, null), /*#__PURE__*/React.createElement(AxisArrows, null)), props.lockedFigures && props.lockedFigures.length > 0 && /*#__PURE__*/React.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React.createElement(GraphLockedLayer, {
32963
33260
  lockedFigures: props.lockedFigures,
32964
33261
  range: state.range
32965
- }))), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
33262
+ })))), props.lockedFigures && /*#__PURE__*/React.createElement(GraphLockedLabelsLayer, {
32966
33263
  lockedFigures: props.lockedFigures
32967
33264
  }), /*#__PURE__*/React.createElement(View, {
32968
33265
  style: {
@@ -33257,32 +33554,33 @@ const renderGraphElements = props => {
33257
33554
  const {
33258
33555
  state,
33259
33556
  dispatch,
33260
- i18n
33557
+ i18n,
33558
+ markings
33261
33559
  } = props;
33262
33560
  const {
33263
33561
  type
33264
33562
  } = state;
33265
33563
  switch (type) {
33266
33564
  case "angle":
33267
- return renderAngleGraph(state, dispatch);
33565
+ return renderAngleGraph(state, dispatch, i18n);
33268
33566
  case "segment":
33269
- return renderSegmentGraph(state, dispatch);
33567
+ return renderSegmentGraph(state, dispatch, i18n);
33270
33568
  case "linear-system":
33271
- return renderLinearSystemGraph(state, dispatch);
33569
+ return renderLinearSystemGraph(state, dispatch, i18n);
33272
33570
  case "linear":
33273
- return renderLinearGraph(state, dispatch);
33571
+ return renderLinearGraph(state, dispatch, i18n);
33274
33572
  case "ray":
33275
- return renderRayGraph(state, dispatch);
33573
+ return renderRayGraph(state, dispatch, i18n);
33276
33574
  case "polygon":
33277
- return renderPolygonGraph(state, dispatch);
33575
+ return renderPolygonGraph(state, dispatch, i18n, markings);
33278
33576
  case "point":
33279
- return renderPointGraph(state, dispatch);
33577
+ return renderPointGraph(state, dispatch, i18n);
33280
33578
  case "circle":
33281
- return renderCircleGraph(state, dispatch);
33579
+ return renderCircleGraph(state, dispatch, i18n);
33282
33580
  case "quadratic":
33283
33581
  return renderQuadraticGraph(state, dispatch, i18n);
33284
33582
  case "sinusoid":
33285
- return renderSinusoidGraph(state, dispatch);
33583
+ return renderSinusoidGraph(state, dispatch, i18n);
33286
33584
  case "none":
33287
33585
  return {
33288
33586
  graph: null,
@@ -33679,13 +33977,13 @@ const {
33679
33977
  polar
33680
33978
  } = angles;
33681
33979
  const {
33682
- angleMeasures: angleMeasures$1,
33683
- ccw: ccw$1,
33684
- lawOfCosines: lawOfCosines$1,
33980
+ angleMeasures,
33981
+ ccw,
33982
+ lawOfCosines,
33685
33983
  magnitude: magnitude$1,
33686
33984
  polygonSidesIntersect,
33687
33985
  reverseVector,
33688
- sign: sign$1,
33986
+ sign,
33689
33987
  vector: vector$1
33690
33988
  } = geometry;
33691
33989
  const {
@@ -34169,7 +34467,7 @@ function doAddPoint(state, action) {
34169
34467
  return _extends({}, state, {
34170
34468
  hasBeenInteractedWith: true,
34171
34469
  coords: newCoords,
34172
- showRemovePointButton: false,
34470
+ showRemovePointButton: true,
34173
34471
  focusedPointIndex: newCoords.length - 1
34174
34472
  });
34175
34473
  }
@@ -34177,16 +34475,11 @@ function doRemovePoint(state, action) {
34177
34475
  if (!isUnlimitedGraphState(state)) {
34178
34476
  return state;
34179
34477
  }
34180
- let nextFocusedPointIndex;
34181
- if (state.interactionMode === "mouse") {
34182
- nextFocusedPointIndex = null;
34183
- } else {
34184
- nextFocusedPointIndex = state.coords.length > 1 ? state.coords.length - 2 : null;
34185
- }
34478
+ const nextFocusedPointIndex = state.coords.length > 1 ? state.coords.length - 2 : null;
34186
34479
  return _extends({}, state, {
34187
34480
  coords: state.coords.filter((_, i) => i !== action.index),
34188
34481
  focusedPointIndex: nextFocusedPointIndex,
34189
- showRemovePointButton: false
34482
+ showRemovePointButton: nextFocusedPointIndex !== null ? true : false
34190
34483
  });
34191
34484
  }
34192
34485
  const getDeltaVertex = (maxMoves, minMoves, delta) => {
@@ -34210,7 +34503,7 @@ const getChange = (coords, delta, constraintOpts) => {
34210
34503
  return [dx, dy];
34211
34504
  };
34212
34505
  // Less than or approximately equal
34213
- function leq$1(a, b) {
34506
+ function leq(a, b) {
34214
34507
  return a < b || approximateEqual(a, b);
34215
34508
  }
34216
34509
  function boundAndSnapToGrid(point, {
@@ -34355,7 +34648,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
34355
34648
  });
34356
34649
 
34357
34650
  // Gets the radian angles between the coords and maps them to degrees
34358
- const angles = angleMeasures$1(coordsCopy).map(angle => angle * 180 / Math.PI);
34651
+ const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
34359
34652
 
34360
34653
  // Gets the relative index of a point
34361
34654
  const rel = j => {
@@ -34375,7 +34668,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
34375
34668
 
34376
34669
  // Avoid degenerate triangles
34377
34670
  if (innerAngles.some(function (angle) {
34378
- return leq$1(angle, 1);
34671
+ return leq(angle, 1);
34379
34672
  })) {
34380
34673
  return startingPoint;
34381
34674
  }
@@ -34389,7 +34682,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
34389
34682
  // we want to add the inner angle to the outer angle, and if it's on the right,
34390
34683
  // we want to subtract the inner angle from the outer angle. The angle solved
34391
34684
  // for is then used in the polar function to determine the new point.
34392
- const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34685
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34393
34686
 
34394
34687
  // Solve for side by using the law of sines
34395
34688
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
@@ -34436,13 +34729,13 @@ function boundAndSnapToSides(destinationPoint, {
34436
34729
  });
34437
34730
 
34438
34731
  // Avoid degenerate triangles
34439
- if (leq$1(sides[1] + sides[2], sides[0]) || leq$1(sides[0] + sides[2], sides[1]) || leq$1(sides[0] + sides[1], sides[2])) {
34732
+ if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
34440
34733
  return startingPoint;
34441
34734
  }
34442
34735
 
34443
34736
  // Solve for angle by using the law of cosines
34444
34737
  // Angle at the first vertex of the polygon
34445
- const innerAngle = lawOfCosines$1(sides[0], sides[2], sides[1]);
34738
+ const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
34446
34739
 
34447
34740
  // Angle at the second vertex of the polygon
34448
34741
  const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
@@ -34453,7 +34746,7 @@ function boundAndSnapToSides(destinationPoint, {
34453
34746
  // we want to add the inner angle to the outer angle, and if it's on the right,
34454
34747
  // we want to subtract the inner angle from the outer angle. The angle solved
34455
34748
  // for is then used in the polar function to determine the new point.
34456
- const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34749
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34457
34750
 
34458
34751
  // Uses the length of the first side of the polygon (radial coordinate)
34459
34752
  // and the angle between the first and second sides of the
@@ -34588,19 +34881,11 @@ const {
34588
34881
  getClockwiseAngle
34589
34882
  } = angles;
34590
34883
  const {
34591
- angleMeasures,
34592
- ccw,
34593
- collinear,
34594
34884
  getLineEquation,
34595
34885
  getLineIntersection,
34596
- intersects,
34597
- lawOfCosines,
34598
34886
  magnitude,
34599
- rotate,
34600
- sign,
34601
34887
  vector
34602
34888
  } = geometry;
34603
- const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
34604
34889
  const defaultBackgroundImage = {
34605
34890
  url: null
34606
34891
  };
@@ -34609,31 +34894,9 @@ const UNLIMITED = "unlimited";
34609
34894
  // Sample background image:
34610
34895
  // https://ka-perseus-graphie.s3.amazonaws.com/29c1b0fcd17fe63df0f148fe357044d5d5c7d0bb.png
34611
34896
 
34612
- // default to defaultValue if actual is null or undefined
34613
- function defaultVal(actual, defaultValue) {
34614
- return actual == null ? defaultValue : actual;
34615
- }
34616
-
34617
- // Less than or approximately equal
34618
- function leq(a, b) {
34619
- return a < b || approximateEqual(a, b);
34620
- }
34621
- function capitalize(str) {
34622
- return str.replace(/(?:^|-)(.)/g, function (match, letter) {
34623
- return letter.toUpperCase();
34624
- });
34625
- }
34626
34897
  function numSteps(range, step) {
34627
34898
  return Math.floor((range[1] - range[0]) / step);
34628
34899
  }
34629
- const _getShouldShowInstructions = props => {
34630
- return _isClickToAddPoints(props) && (
34631
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34632
- props.graph.coords == null || props.graph.coords.length === 0);
34633
- };
34634
- const _isClickToAddPoints = props => {
34635
- return props.graph.type === "point" && props.graph.numPoints === UNLIMITED || props.graph.type === "polygon" && props.graph.numSides === UNLIMITED;
34636
- };
34637
34900
  const makeInvalidTypeError = (functionName, graphType) => {
34638
34901
  return new PerseusError(`${functionName} called but current graph type is not a '${graphType}'`, Errors.NotAllowed, {
34639
34902
  metadata: {
@@ -34674,1236 +34937,16 @@ function getQuadraticCoefficients(coords) {
34674
34937
  const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
34675
34938
  return [a, b, c];
34676
34939
  }
34677
-
34678
- // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
34679
- // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
34680
- // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
34681
- // Once the legacy graph is removed, we should move this logic directly into the validate function below.
34682
- class LegacyInteractiveGraph extends React.Component {
34683
- constructor(...args) {
34684
- super(...args);
34685
- this.angle = void 0;
34686
- this.circle = void 0;
34687
- this.graphie = void 0;
34688
- this.horizHairline = void 0;
34689
- this.line = void 0;
34690
- this.lines = void 0;
34691
- this.parabola = void 0;
34692
- this.pointA = void 0;
34693
- this.pointB = void 0;
34694
- this.pointC = void 0;
34695
- // @ts-expect-error - TS2564 - Property 'points' has no initializer and is not definitely assigned in the constructor.
34696
- this.points = void 0;
34697
- this.polygon = void 0;
34698
- // @ts-expect-error - TS2564 - Property 'shouldResetGraphie' has no initializer and is not definitely assigned in the constructor.
34699
- this.shouldResetGraphie = void 0;
34700
- this.sinusoid = void 0;
34701
- this.trashCan = void 0;
34702
- this.vertHairline = void 0;
34703
- this.state = {
34704
- shouldShowInstructions: _getShouldShowInstructions(this.props)
34705
- };
34706
- this._getShouldShowInstructions = props => {
34707
- props = props || this.props;
34708
- return this.isClickToAddPoints(props) && (
34709
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
34710
- props.graph.coords == null || props.graph.coords.length === 0);
34711
- };
34712
- this.setGraphie = newGraphie => {
34713
- this.graphie = newGraphie;
34714
- this.setupGraphie();
34715
- };
34716
- this.handleAddPointsMouseDown = coord => {
34717
- // This function should only be called when this.isClickToAddPoints()
34718
- // is true
34719
- if (!this.isClickToAddPoints()) {
34720
- throw new PerseusError("handleAddPointsClick should not be registered" + "when isClickToAddPoints() is false", Errors.NotAllowed);
34721
- }
34722
- if (!this.isCoordInTrash(coord)) {
34723
- let point;
34724
- if (this.props.graph.type === "point") {
34725
- point = this.createPointForPointsType(coord, this.points.length);
34726
- if (!point.constrain()) {
34727
- point.remove();
34728
- return;
34729
- }
34730
- this.points.push(point);
34731
-
34732
- // interactive2 allows us to grab the point
34733
- const idx = this.points.length - 1;
34734
- this.points[idx].grab(coord);
34735
- this.updateCoordsFromPoints();
34736
- } else if (this.props.graph.type === "polygon") {
34737
- var _this$polygon;
34738
- if ((_this$polygon = this.polygon) != null && _this$polygon.closed()) {
34739
- return;
34740
- }
34741
- point = this.createPointForPolygonType(coord, this.points.length);
34742
- this.points.push(point);
34743
- const idx = this.points.length - 1;
34744
- this.points[idx].grab(coord);
34745
-
34746
- // We don't call updateCoordsFromPoints for
34747
- // polygons, since the polygon won't be
34748
- // closed yet.
34749
- this.updatePolygon();
34750
- }
34751
- this.setState({
34752
- shouldShowInstructions: false
34753
- });
34754
- }
34755
- };
34756
- this.resetGraphie = () => {
34757
- var _this$refs$graph;
34758
- this.shouldResetGraphie = false;
34759
- this.parabola = null;
34760
- this.sinusoid = null;
34761
- // eslint-disable-next-line react/no-string-refs
34762
- // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
34763
- (_this$refs$graph = this.refs.graph) == null || _this$refs$graph.reset();
34764
- };
34765
- this.setupGraphie = () => {
34766
- this.setTrashCanVisibility(0);
34767
- if (this.isClickToAddPoints()) {
34768
- this.setTrashCanVisibility(0.5);
34769
- }
34770
- if (this.props.apiOptions.isMobile) {
34771
- var _this$horizHairline, _this$vertHairline;
34772
- this.horizHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
34773
- normalStyle: {
34774
- strokeWidth: 1
34775
- }
34776
- });
34777
- this.horizHairline.attr({
34778
- stroke: KhanColors.INTERACTIVE
34779
- });
34780
- (_this$horizHairline = this.horizHairline) == null || _this$horizHairline.hide();
34781
- this.vertHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
34782
- normalStyle: {
34783
- strokeWidth: 1
34784
- }
34785
- });
34786
- this.vertHairline.attr({
34787
- stroke: KhanColors.INTERACTIVE
34788
- });
34789
- (_this$vertHairline = this.vertHairline) == null || _this$vertHairline.hide();
34790
- }
34791
- const type = this.props.graph.type;
34792
- this["add" + capitalize(type) + "Controls"]();
34793
- };
34794
- this.showHairlines = point => {
34795
- if (this.props.apiOptions.isMobile && this.props.markings !== "none") {
34796
- var _this$horizHairline2, _this$horizHairline3, _this$vertHairline2, _this$vertHairline3;
34797
- // Hairlines are already initialized when the graph is loaded, so
34798
- // here we just move them to the updated location and make them
34799
- // visible.
34800
- (_this$horizHairline2 = this.horizHairline) == null || _this$horizHairline2.moveTo([this.props.range[0][0], point[1]], [this.props.range[0][1], point[1]]);
34801
- (_this$horizHairline3 = this.horizHairline) == null || _this$horizHairline3.show();
34802
- (_this$vertHairline2 = this.vertHairline) == null || _this$vertHairline2.moveTo([point[0], this.props.range[1][0]], [point[0], this.props.range[1][1]]);
34803
- (_this$vertHairline3 = this.vertHairline) == null || _this$vertHairline3.show();
34804
- }
34805
- };
34806
- this.hideHairlines = () => {
34807
- if (this.props.apiOptions.isMobile) {
34808
- var _this$horizHairline4, _this$vertHairline4;
34809
- (_this$horizHairline4 = this.horizHairline) == null || _this$horizHairline4.hide();
34810
- (_this$vertHairline4 = this.vertHairline) == null || _this$vertHairline4.hide();
34811
- }
34812
- };
34813
- this.setTrashCanVisibility = opacity => {
34814
- const graphie = this.graphie;
34815
- if (number.equal(opacity, 0)) {
34816
- if (this.trashCan) {
34817
- this.trashCan.remove();
34818
- this.trashCan = null;
34819
- }
34820
- } else if (!this.props.apiOptions.isMobile) {
34821
- var _this$trashCan;
34822
- // Only if trash tooltips are not being used, we initialize the old
34823
- // trash can area.
34824
- if (!this.trashCan) {
34825
- var _graphie$raphael;
34826
- this.trashCan = graphie == null || (_graphie$raphael = graphie.raphael) == null ? void 0 : _graphie$raphael.image(TRASH_ICON_URI, graphie.xpixels - 40, graphie.ypixels - 40, 40, 40);
34827
- }
34828
- (_this$trashCan = this.trashCan) == null || _this$trashCan.attr({
34829
- opacity: opacity
34830
- });
34831
- }
34832
- };
34833
- this.isClickToAddPoints = props => {
34834
- props = props || this.props;
34835
- return _isClickToAddPoints(props);
34836
- };
34837
- this._lineStroke = () => {
34838
- // This should probably use: this.props.apiOptions.isMobile
34839
- // @ts-expect-error - TS2339 - Property 'isMobile' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
34840
- return this.props.isMobile ? {
34841
- "stroke-width": 3
34842
- } : {};
34843
- };
34844
- this.addLine = type => {
34845
- const self = this;
34846
- const graphie = self.graphie;
34847
- const coords = InteractiveGraph.getLineCoords(self.props.graph, self.props);
34848
- const points = self.points = _.map(coords, coord => {
34849
- return Interactive2.addMaybeMobileMovablePoint(this, {
34850
- coord: coord,
34851
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap()],
34852
- onMove: () => {
34853
- const graph = _.extend({}, self.props.graph, {
34854
- coords: _.invoke(points, "coord")
34855
- });
34856
- self.onChange({
34857
- graph: graph
34858
- });
34859
- }
34860
- });
34861
- });
34862
- const lineConfig = {
34863
- points: points,
34864
- static: true,
34865
- normalStyle: _extends({
34866
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE
34867
- }, this._lineStroke())
34868
- };
34869
- if (type === "line") {
34870
- // @ts-expect-error - TS2339 - Property 'extendLine' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
34871
- lineConfig.extendLine = true;
34872
- } else if (type === "ray") {
34873
- // @ts-expect-error - TS2339 - Property 'extendRay' does not exist on type '{ readonly points: any[]; readonly static: true; readonly normalStyle: { readonly "stroke-width"?: number | undefined; readonly stroke: "#71B307" | "#63D9EA"; }; }'.
34874
- lineConfig.extendRay = true;
34875
- }
34876
- self.line = Interactive2.addMovableLine(graphie, lineConfig);
34877
-
34878
- // A and B can't be in the same place
34879
- points[0].listen("constraints", "isLine", coord => {
34880
- return !point.equal(coord, points[1].coord());
34881
- });
34882
- points[1].listen("constraints", "isLine", coord => {
34883
- return !point.equal(coord, points[0].coord());
34884
- });
34885
- };
34886
- this.removeLine = () => {
34887
- var _this$line;
34888
- _.invoke(this.points, "remove");
34889
- (_this$line = this.line) == null || _this$line.remove();
34890
- };
34891
- this.addNoneControls = () => {};
34892
- this.removeNoneControls = () => {};
34893
- this.addLinearControls = () => {
34894
- this.addLine("line");
34895
- };
34896
- this.removeLinearControls = () => {
34897
- this.removeLine();
34898
- };
34899
- this.addQuadraticControls = () => {
34900
- if (this.props.graph.type !== "quadratic") {
34901
- throw makeInvalidTypeError("addQuadraticControls", "quadratic");
34902
- }
34903
- let coords = this.props.graph.coords;
34904
- if (!coords) {
34905
- coords = InteractiveGraph.defaultQuadraticCoords(this.props);
34906
- }
34907
- const onMoveHandler = () => {
34908
- const graph = _.extend({}, this.props.graph, {
34909
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'.
34910
- coords: [pointA.coord(), pointB.coord(), pointC.coord()]
34911
- });
34912
- this.onChange({
34913
- graph
34914
- });
34915
- this.updateQuadratic();
34916
- };
34917
-
34918
- // NOTE(jeresig): This code attempts to access the variable while it's
34919
- // still being defined, it should be refactored! The callbacks appear
34920
- // to be executed synchronously, which causes this issue.
34921
- let pointA = null;
34922
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
34923
- coord: coords[0],
34924
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
34925
- return !pointA ||
34926
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34927
- coord[0] !== pointB.coord()[0] &&
34928
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34929
- coord[0] !== pointC.coord()[0];
34930
- }],
34931
- onMove: onMoveHandler
34932
- });
34933
-
34934
- // NOTE(jeresig): This code attempts to access the variable while it's
34935
- // still being defined, it should be refactored! The callbacks appear
34936
- // to be executed synchronously, which causes this issue.
34937
- let pointB = null;
34938
- pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
34939
- coord: coords[1],
34940
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
34941
- return !pointB ||
34942
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34943
- coord[0] !== pointA.coord()[0] &&
34944
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34945
- coord[0] !== pointC.coord()[0];
34946
- }],
34947
- onMove: onMoveHandler
34948
- });
34949
-
34950
- // NOTE(jeresig): This code attempts to access the variable while it's
34951
- // still being defined, it should be refactored! The callbacks appear
34952
- // to be executed synchronously, which causes this issue.
34953
- let pointC = null;
34954
- pointC = this.pointC = Interactive2.addMaybeMobileMovablePoint(this, {
34955
- coord: coords[2],
34956
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
34957
- return !pointC ||
34958
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34959
- coord[0] !== pointA.coord()[0] &&
34960
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
34961
- coord[0] !== pointB.coord()[0];
34962
- }],
34963
- onMove: onMoveHandler
34964
- });
34965
- this.updateQuadratic();
34966
- };
34967
- this.updateQuadratic = () => {
34968
- const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(this.props);
34969
- if (!coeffs) {
34970
- return;
34971
- }
34972
-
34973
- // Extract coefficients the parabola
34974
- const a = coeffs[0];
34975
- const b = coeffs[1];
34976
- const c = coeffs[2];
34977
-
34978
- // Plot and style
34979
- if (this.parabola) {
34980
- var _this$graphie, _this$parabola;
34981
- const path = (_this$graphie = this.graphie) == null ? void 0 : _this$graphie.svgParabolaPath(a, b, c);
34982
- (_this$parabola = this.parabola) == null || _this$parabola.attr({
34983
- path: path
34984
- });
34985
- } else {
34986
- var _this$graphie2, _this$parabola2, _this$parabola3;
34987
- this.parabola = (_this$graphie2 = this.graphie) == null ? void 0 : _this$graphie2.parabola(a, b, c);
34988
- (_this$parabola2 = this.parabola) == null || _this$parabola2.attr(_extends({
34989
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC
34990
- }, this._lineStroke()));
34991
- (_this$parabola3 = this.parabola) == null || _this$parabola3.toBack();
34992
- }
34993
- };
34994
- this.removeQuadraticControls = () => {
34995
- var _this$pointA, _this$pointB, _this$pointC;
34996
- (_this$pointA = this.pointA) == null || _this$pointA.remove();
34997
- (_this$pointB = this.pointB) == null || _this$pointB.remove();
34998
- (_this$pointC = this.pointC) == null || _this$pointC.remove();
34999
- if (this.parabola) {
35000
- this.parabola.remove();
35001
- this.parabola = null;
35002
- }
35003
- };
35004
- this.addSinusoidControls = () => {
35005
- if (this.props.graph.type !== "sinusoid") {
35006
- throw makeInvalidTypeError("addSinusoidControls", "sinusoid");
35007
- }
35008
- let coords = this.props.graph.coords;
35009
- if (!coords) {
35010
- coords = InteractiveGraph.defaultSinusoidCoords(this.props);
35011
- }
35012
- const onMoveHandler = () => {
35013
- const graph = _.extend({}, this.props.graph, {
35014
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35015
- coords: [pointA.coord(), pointB.coord()]
35016
- });
35017
- this.onChange({
35018
- graph: graph
35019
- });
35020
- this.updateSinusoid();
35021
- };
35022
-
35023
- // NOTE(jeresig): This code attempts to access the variable while it's
35024
- // still being defined, it should be refactored! The callbacks appear
35025
- // to be executed synchronously, which causes this issue.
35026
- let pointA = null;
35027
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35028
- coord: coords[0],
35029
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35030
- return !pointA || coord[0] !== pointB.coord()[0];
35031
- }],
35032
- onMove: onMoveHandler
35033
- });
35034
- const pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35035
- coord: coords[1],
35036
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35037
- // @ts-expect-error - TS2339 - Property 'coord' does not exist on type 'never'.
35038
- return !pointA || coord[0] !== pointA.coord()[0];
35039
- }],
35040
- onMove: onMoveHandler
35041
- });
35042
- this.updateSinusoid();
35043
- };
35044
- this.updateSinusoid = () => {
35045
- const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(this.props);
35046
- if (!coeffs) {
35047
- return;
35048
- }
35049
- const a = coeffs[0];
35050
- const b = coeffs[1];
35051
- const c = coeffs[2];
35052
- const d = coeffs[3];
35053
-
35054
- // Plot and style
35055
- if (this.sinusoid) {
35056
- var _this$graphie3, _this$sinusoid;
35057
- const path = (_this$graphie3 = this.graphie) == null ? void 0 : _this$graphie3.svgSinusoidPath(a, b, c, d);
35058
- (_this$sinusoid = this.sinusoid) == null || _this$sinusoid.attr({
35059
- path: path
35060
- });
35061
- } else {
35062
- var _this$graphie4, _this$sinusoid2, _this$sinusoid3;
35063
- this.sinusoid = (_this$graphie4 = this.graphie) == null ? void 0 : _this$graphie4.sinusoid(a, b, c, d);
35064
- (_this$sinusoid2 = this.sinusoid) == null || _this$sinusoid2.attr(_extends({
35065
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC
35066
- }, this._lineStroke()));
35067
- (_this$sinusoid3 = this.sinusoid) == null || _this$sinusoid3.toBack();
35068
- }
35069
- };
35070
- this.removeSinusoidControls = () => {
35071
- var _this$pointA2, _this$pointB2;
35072
- (_this$pointA2 = this.pointA) == null || _this$pointA2.remove();
35073
- (_this$pointB2 = this.pointB) == null || _this$pointB2.remove();
35074
- if (this.sinusoid) {
35075
- this.sinusoid.remove();
35076
- this.sinusoid = null;
35077
- }
35078
- };
35079
- this.addCircleControls = () => {
35080
- const graphie = this.graphie;
35081
- const minSnap = _.min(graphie.snap);
35082
- const circle = this.circle = graphie == null ? void 0 : graphie.addCircleGraph({
35083
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'.
35084
- center: this.props.graph.center || [0, 0],
35085
- // @ts-expect-error - TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35086
- radius: this.props.graph.radius || _.min(this.props.step),
35087
- snapX: graphie.snap[0],
35088
- snapY: graphie.snap[1],
35089
- minRadius: minSnap,
35090
- snapRadius: minSnap
35091
- });
35092
- $(circle).on("move", () => {
35093
- const graph = _.extend({}, this.props.graph, {
35094
- center: circle.center,
35095
- radius: circle.radius
35096
- });
35097
- this.onChange({
35098
- graph: graph
35099
- });
35100
- });
35101
- };
35102
- this.removeCircleControls = () => {
35103
- var _this$circle;
35104
- (_this$circle = this.circle) == null || _this$circle.remove();
35105
- };
35106
- this.addLinearSystemControls = () => {
35107
- const graphie = this.graphie;
35108
- const coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, this.props);
35109
- const segmentColors = [KhanColors.INTERACTIVE, KhanColors.GREEN];
35110
- const points = this.points = _.map(coords, (segmentCoords, segmentIndex) => {
35111
- // NOTE(jeresig): This code attempts to access the variable
35112
- // while it's still being defined, it should be refactored!
35113
- // The callbacks appear to be executed synchronously, which
35114
- // causes this issue.
35115
- const segmentPoints = [];
35116
- for (let i = 0; i < segmentCoords.length; i += 1) {
35117
- const coord = segmentCoords[i];
35118
- segmentPoints.push(Interactive2.addMaybeMobileMovablePoint(this, {
35119
- coord: coord,
35120
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35121
- const otherSegment = segmentPoints[1 - i];
35122
- if (!otherSegment) {
35123
- // segment hasn't been defined yet
35124
- // because we're still creating them
35125
- return;
35126
- }
35127
- return !point.equal(coord, otherSegment.coord());
35128
- }],
35129
- onMove: () => {
35130
- const graph = _.extend({}, this.props.graph, {
35131
- coords: _.map(this.points, segment => _.invoke(segment, "coord"))
35132
- });
35133
- this.onChange({
35134
- graph: graph
35135
- });
35136
- },
35137
- normalStyle: {
35138
- fill: segmentColors[segmentIndex]
35139
- },
35140
- highlightStyle: {
35141
- fill: segmentColors[segmentIndex]
35142
- }
35143
- }));
35144
- }
35145
- return segmentPoints;
35146
- });
35147
- this.lines = _.map(points, (segmentPoints, segmentIndex) => {
35148
- return Interactive2.addMovableLine(graphie, {
35149
- points: segmentPoints,
35150
- static: true,
35151
- extendLine: true,
35152
- normalStyle: {
35153
- stroke: segmentColors[segmentIndex]
35154
- }
35155
- });
35156
- });
35157
- };
35158
- this.removeLinearSystemControls = () => {
35159
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
35160
- _.invoke(this.lines, "remove");
35161
- _.map(this.points, segment => _.invoke(segment, "remove"));
35162
- };
35163
- this.isCoordInTrash = coord => {
35164
- if (this.props.apiOptions.isMobile) {
35165
- return false;
35166
- }
35167
- const graphie = this.graphie;
35168
- const screenPoint = graphie == null ? void 0 : graphie.scalePoint(coord);
35169
- return screenPoint[0] >= graphie.xpixels - 40 && screenPoint[1] >= graphie.ypixels - 40;
35170
- };
35171
- this.createPointForPointsType = (coord, i) => {
35172
- const self = this;
35173
- const remove = () => {
35174
- self.points = _.filter(self.points, function (pt) {
35175
- return pt !== point$1;
35176
- });
35177
- // update the correct answer box
35178
- self.updateCoordsFromPoints();
35179
-
35180
- // remove this movablePoint from graphie.
35181
- // we wait to do this until we're not inside of
35182
- // said point's onMoveEnd method so its state is
35183
- // consistent throughout this method call
35184
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35185
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35186
- setTimeout(point$1.remove.bind(point$1), 0); // eslint-disable-line no-restricted-syntax
35187
- };
35188
-
35189
- // NOTE(jeresig): This code attempts to access the variable while it's
35190
- // still being defined, it should be refactored! The callbacks appear
35191
- // to be executed synchronously, which causes this issue.
35192
- let point$1 = null;
35193
- point$1 = Interactive2.addMaybeMobileMovablePoint(this, _extends({
35194
- coord: coord,
35195
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), function (coord) {
35196
- // TODO(jack): There ought to be a
35197
- // MovablePoint.constraints.avoid
35198
- // default that lets you do things like this
35199
- return _.all(self.points, function (pt) {
35200
- return point$1 === pt || !point.equal(coord, pt.coord());
35201
- });
35202
- }],
35203
- onMoveStart: function () {
35204
- if (self.isClickToAddPoints()) {
35205
- self.setTrashCanVisibility(1);
35206
- }
35207
- },
35208
- onMove: self.updateCoordsFromPoints,
35209
- onMoveEnd: function (coord) {
35210
- if (self.isClickToAddPoints()) {
35211
- if (self.isCoordInTrash(coord)) {
35212
- remove();
35213
- }
35214
- // In case we mouseup'd off the graphie and that
35215
- // stopped the move (in which case, we might not
35216
- // be in isCoordInTrash()
35217
- self.setTrashCanVisibility(0.5);
35218
- }
35219
- }
35220
- }, this.props.apiOptions.isMobile && self.isClickToAddPoints() ? {
35221
- onRemove: remove
35222
- } : {}));
35223
- return point$1;
35224
- };
35225
- this.removePoint = point => {
35226
- let index = null;
35227
- this.points = _.filter(this.points, function (pt, i) {
35228
- if (pt === point) {
35229
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
35230
- index = i;
35231
- return false;
35232
- }
35233
- return true;
35234
- });
35235
- return index;
35236
- };
35237
- this.createPointForPolygonType = (coord, i) => {
35238
- if (this.props.graph.type !== "polygon") {
35239
- throw makeInvalidTypeError("createPointForPolygonType", "polygon");
35240
- }
35241
-
35242
- // TODO(alex): check against "grid" instead, use constants
35243
- const snapToGrid = !_.contains(["angles", "sides"], this.props.graph.snapTo);
35244
-
35245
- // Index relative to current point -> absolute index
35246
- // NOTE: This does not work when isClickToAddPoints() == true,
35247
- // as `i` can be changed by dragging a point to the trash
35248
- // Currently this function is only called when !isClickToAddPoints()
35249
- const rel = j => {
35250
- return (i + j + this.points.length) % this.points.length;
35251
- };
35252
- const remove = () => {
35253
- var _this$polygon2;
35254
- // remove this point from points
35255
- const index = this.removePoint(point$1);
35256
- if ((_this$polygon2 = this.polygon) != null && _this$polygon2.closed()) {
35257
- var _this$polygon3;
35258
- // We should be checking if this.points is defined before rotating them.
35259
- // @ts-expect-error - TS4104 - The type 'readonly any[]' is 'readonly' and cannot be assigned to the mutable type 'any[]'. | TS2345 - Argument of type 'number | null | undefined' is not assignable to parameter of type 'number | undefined'.
35260
- this.points = rotate(this.points, index);
35261
- (_this$polygon3 = this.polygon) == null || _this$polygon3.update({
35262
- closed: false
35263
- });
35264
- }
35265
- this.updatePolygon();
35266
- // the polygon is now unclosed, so we need to
35267
- // remove any points props
35268
- this.clearCoords();
35269
-
35270
- // remove this movablePoint from graphie.
35271
- // wait to do this until we're not inside of
35272
- // said point's onMoveEnd method so state is
35273
- // consistent throughout the method call
35274
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35275
- // eslint-disable-next-line no-restricted-syntax
35276
- setTimeout(point$1.remove.bind(point$1), 0);
35277
- };
35278
- const setInitialMove = debounce$2((point, bool) => {
35279
- point.state.isInitialMove = bool;
35280
- }, 1000);
35281
- const onMoveEndHandler = coord => {
35282
- // If the user clicked and dragged a point over endpoint,
35283
- // join the them
35284
- const overlappedPoints = this.points.length > 1 && (point$1 === this.points[0] && point.equal(coord, _.last(this.points).coord()) || point$1 === _.last(this.points) && point.equal(coord, this.points[0].coord()));
35285
- if (this.isClickToAddPoints()) {
35286
- if (this.isCoordInTrash(coord)) {
35287
- remove();
35288
- } else if (overlappedPoints) {
35289
- const pointToRemove = this.points.pop();
35290
- if (this.points.length > 2) {
35291
- var _this$polygon4;
35292
- (_this$polygon4 = this.polygon) == null || _this$polygon4.update({
35293
- closed: true
35294
- });
35295
- this.updateCoordsFromPoints();
35296
- } else {
35297
- var _this$polygon5;
35298
- (_this$polygon5 = this.polygon) == null || _this$polygon5.update({
35299
- closed: false
35300
- });
35301
- this.clearCoords();
35302
- }
35303
- this.updatePolygon();
35304
- // remove this movablePoint from graphie.
35305
- // wait to do this until we're not inside of
35306
- // said point's onMoveEnd method so state is
35307
- // consistent throughout the method call
35308
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35309
- // eslint-disable-next-line no-restricted-syntax
35310
- setTimeout(pointToRemove.remove.bind(pointToRemove), 0);
35311
- } else {
35312
- // If the user clicked and dragged a point over any other
35313
- // existing point, fix shape
35314
- const shouldRemove = _.any(this.points, function (pt) {
35315
- return pt !== point$1 && point.equal(pt.coord(), coord);
35316
- });
35317
- if (shouldRemove) {
35318
- var _this$polygon7;
35319
- this.removePoint(point$1);
35320
- if (this.points.length < 3) {
35321
- var _this$polygon6;
35322
- (_this$polygon6 = this.polygon) == null || _this$polygon6.update({
35323
- closed: false
35324
- });
35325
- this.clearCoords();
35326
- } else if ((_this$polygon7 = this.polygon) != null && _this$polygon7.closed()) {
35327
- this.updateCoordsFromPoints();
35328
- }
35329
- this.updatePolygon();
35330
- // remove this movablePoint from graphie.
35331
- // wait to do this until we're not inside
35332
- // said point's onMoveEnd method so state
35333
- // is consistent throughout the method call
35334
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35335
- // eslint-disable-next-line no-restricted-syntax
35336
- setTimeout(point$1.remove.bind(point$1), 0);
35337
- } else {
35338
- var _this$polygon8;
35339
- // If this was
35340
- // * a click on the first or last point
35341
- // * and not a deletion
35342
- // * and not a drag,
35343
- // * and not a creation of a new point
35344
- // (see !point.state.isInitialMove, below),
35345
- // * and our polygon is not closed,
35346
- // * and we can close it (we need at least the same
35347
- // number of points as coordinates asked for in the
35348
- // question),
35349
- // then close it
35350
- const isFirstOrLastPoint = point$1 === _.head(this.points) || point$1 === _.last(this.points);
35351
- const pointNotMoved = !point$1.hasMoved();
35352
- // NOTE (jsatk): Currently different on mobile versus
35353
- // destkop
35354
- const notANewPoint = !point$1.state.isInitialMove;
35355
- const polygonNotClosed = !((_this$polygon8 = this.polygon) != null && _this$polygon8.closed());
35356
- const allPointsPlotted = this.points.length > 2;
35357
- const canCloseUnclosedPolygon = isFirstOrLastPoint && pointNotMoved && notANewPoint && polygonNotClosed && allPointsPlotted;
35358
- if (canCloseUnclosedPolygon) {
35359
- var _this$polygon9;
35360
- (_this$polygon9 = this.polygon) == null || _this$polygon9.update({
35361
- closed: true
35362
- });
35363
- this.updatePolygon();
35364
-
35365
- // We finally have a closed polygon, so save our
35366
- // points to props
35367
- this.updateCoordsFromPoints();
35368
- }
35369
- }
35370
- }
35371
-
35372
- // In case we mouseup'd off the graphie and that
35373
- // stopped the move
35374
- this.setTrashCanVisibility(0.5);
35375
- }
35376
- setInitialMove(point$1, false);
35377
- };
35378
- const graphConstraint = coord => {
35379
- // These constraints are all relative to the other points, so if
35380
- // we're creating the initial points and haven't added any others
35381
- // to the graph, we can't enforce them.
35382
- if (this.points == null || this.points.length === 0) {
35383
- return true;
35384
- }
35385
- const coords = _.invoke(this.points, "coord");
35386
- coords[i] = coord;
35387
-
35388
- // Check for invalid positioning, but only if we aren't adding
35389
- // points one click at a time, since those added points could
35390
- // have already violated these constraints
35391
- if (!this.isClickToAddPoints()) {
35392
- // Polygons can't have consecutive collinear points
35393
- if (collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) || collinear(coords[rel(-1)], coords[i], coords[rel(1)]) || collinear(coords[i], coords[rel(1)], coords[rel(2)])) {
35394
- return false;
35395
- }
35396
- const segments = _.zip(coords, rotate(coords));
35397
- if (this.points.length > 3) {
35398
- // Constrain to simple (non self-intersecting) polygon by
35399
- // testing whether adjacent segments intersect any others
35400
- for (let j = -1; j <= 0; j++) {
35401
- const segment = segments[rel(j)];
35402
- const others = _.without(segments, segment, segments[rel(j - 1)], segments[rel(j + 1)]);
35403
- for (let k = 0; k < others.length; k++) {
35404
- const other = others[k];
35405
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Line'.
35406
- if (intersects(segment, other)) {
35407
- return false;
35408
- }
35409
- }
35410
- }
35411
- }
35412
- }
35413
- if (
35414
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35415
- this.props.graph.snapTo === "angles" && this.points.length > 2) {
35416
- var _this$graphie5;
35417
- // Snap to whole degree interior angles
35418
-
35419
- const angles = _.map(angleMeasures(coords), function (rad) {
35420
- return rad * 180 / Math.PI;
35421
- });
35422
- _.each([-1, 1], function (j) {
35423
- angles[rel(j)] = Math.round(angles[rel(j)]);
35424
- });
35425
- const getAngle = function getAngle(a, vertex, b) {
35426
- const angle = GraphUtils.findAngleDeprecated(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
35427
- return (angle + 360) % 360;
35428
- };
35429
- const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
35430
- innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
35431
-
35432
- // Avoid degenerate triangles
35433
- if (_.any(innerAngles, function (angle) {
35434
- return leq(angle, 1);
35435
- })) {
35436
- return false;
35437
- }
35438
- const knownSide = magnitude(
35439
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
35440
- vector(coords[rel(-1)], coords[rel(1)]));
35441
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
35442
-
35443
- // Solve for side by using the law of sines
35444
- const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
35445
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
35446
- const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
35447
- return (_this$graphie5 = this.graphie) == null ? void 0 : _this$graphie5.addPoints(coords[rel(-1)], offset);
35448
- }
35449
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35450
- if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
35451
- var _this$graphie6;
35452
- // Snap to whole unit side measures
35453
-
35454
- const sides = _.map([[coords[rel(-1)], coords[i]], [coords[i], coords[rel(1)]], [coords[rel(-1)], coords[rel(1)]]], function (coords) {
35455
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
35456
- return magnitude(vector(...coords));
35457
- });
35458
- _.each([0, 1], function (j) {
35459
- sides[j] = Math.round(sides[j]);
35460
- });
35461
-
35462
- // Avoid degenerate triangles
35463
- if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
35464
- return false;
35465
- }
35466
-
35467
- // Solve for angle by using the law of cosines
35468
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
35469
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
35470
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
35471
- const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
35472
- return (_this$graphie6 = this.graphie) == null ? void 0 : _this$graphie6.addPoints(coords[rel(-1)], offset);
35473
- }
35474
- // Snap to grid (already done)
35475
- return true;
35476
- };
35477
- const point$1 = Interactive2.addMaybeMobileMovablePoint(this, _extends({
35478
- coord: coord,
35479
- constraints: [Interactive2.MovablePoint.constraints.bound(), snapToGrid ? Interactive2.MovablePoint.constraints.snap() : null, graphConstraint],
35480
- onMoveStart: () => {
35481
- if (this.isClickToAddPoints()) {
35482
- this.setTrashCanVisibility(1);
35483
- }
35484
- },
35485
- onMove: () => {
35486
- var _this$polygon10;
35487
- if ((_this$polygon10 = this.polygon) != null && _this$polygon10.closed()) {
35488
- this.updateCoordsFromPoints();
35489
- }
35490
- },
35491
- onMoveEnd: onMoveEndHandler
35492
- }, this.props.apiOptions.isMobile && this.isClickToAddPoints() ? {
35493
- onRemove: remove
35494
- } : {}));
35495
- point$1.state.isInitialMove = true;
35496
- return point$1;
35497
- };
35498
- this.updateCoordsFromPoints = () => {
35499
- const graph = _.extend({}, this.props.graph, {
35500
- // Handle old movable points with .coord, or
35501
- // Interactive2.MovablePoint's with .coord()
35502
- coords: _.map(this.points, function (point) {
35503
- return _.result(point, "coord");
35504
- })
35505
- });
35506
- this.onChange({
35507
- graph: graph
35508
- });
35509
- };
35510
- this.clearCoords = () => {
35511
- const graph = _.extend({}, this.props.graph, {
35512
- coords: null
35513
- });
35514
- this.onChange({
35515
- graph: graph
35516
- });
35517
- };
35518
- this.onChange = data => {
35519
- this.props.onChange(data);
35520
- this.props.trackInteraction();
35521
- };
35522
- this.addPointControls = () => {
35523
- if (this.props.graph.type !== "point") {
35524
- throw makeInvalidTypeError("addPointControls", "point");
35525
- }
35526
- const coords = InteractiveGraph.getPointCoords(this.props.graph, this.props);
35527
- // Clear out our old points so that newly added points don't
35528
- // "collide" with them and reposition when being added
35529
- // Without this, when added, each point checks whether it is on top
35530
- // of a point in this.points, which (a) shouldn't matter since
35531
- // we're clearing out this.points anyways, and (b) can cause problems
35532
- // if each of this.points is a MovablePoint instead of an
35533
- // Interactive2.MovablePoint, since one has a .coord and the other
35534
- // has .coord()
35535
- // TODO(jack): Figure out a better way to do this
35536
- this.points = [];
35537
- this.points = _.map(coords, this.createPointForPointsType, this);
35538
- };
35539
- this.removePointControls = () => {
35540
- _.invoke(this.points, "remove");
35541
- };
35542
- this.addSegmentControls = () => {
35543
- if (this.props.graph.type !== "segment") {
35544
- throw makeInvalidTypeError("addSegmentControls", "segment");
35545
- }
35546
- const self = this;
35547
- const graphie = this.graphie;
35548
- const coords = InteractiveGraph.getSegmentCoords(this.props.graph, this.props);
35549
- const createPoint = options => Interactive2.addMaybeMobileMovablePoint(this, options);
35550
- this.points = [];
35551
- this.lines = _.map(coords, function (segment, i) {
35552
- const updateCoordProps = function updateCoordProps() {
35553
- const graph = _.extend({}, self.props.graph, {
35554
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
35555
- coords: _.invoke(self.lines, "coords")
35556
- });
35557
- self.onChange({
35558
- graph: graph
35559
- });
35560
- };
35561
-
35562
- // NOTE(jeresig): This code attempts to access the variable
35563
- // while it's still being defined, it should be refactored!
35564
- // The callbacks appear to be executed synchronously, which
35565
- // causes this issue.
35566
- const points = [];
35567
- for (let _i = 0; _i < segment.length; _i += 1) {
35568
- const coord = segment[_i];
35569
- points.push(createPoint({
35570
- coord: coord,
35571
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35572
- const otherPoint = points[1 - _i];
35573
- if (!otherPoint) {
35574
- // point hasn't been defined yet
35575
- // because we're still creating them
35576
- // We return false as the points aren't
35577
- // going to be in the same position at
35578
- // this point.
35579
- return false;
35580
- }
35581
- return !point.equal(coord, otherPoint.coord());
35582
- }],
35583
- onMove: updateCoordProps
35584
- }));
35585
- }
35586
- self.points = self.points.concat(points);
35587
- const line = Interactive2.addMovableLine(graphie, {
35588
- points: points,
35589
- static: false,
35590
- constraints: [
35591
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
35592
- Interactive2.MovableLine.constraints.bound(),
35593
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
35594
- Interactive2.MovableLine.constraints.snap()],
35595
- onMove: [
35596
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => void'.
35597
- Interactive2.MovableLine.onMove.updatePoints, updateCoordProps],
35598
- normalStyle: _extends({
35599
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35600
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE
35601
- }, this._lineStroke()),
35602
- highlightStyle: _extends({
35603
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35604
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTING
35605
- }, this._lineStroke())
35606
- });
35607
- _.invoke(points, "toFront");
35608
- return line;
35609
- }, this);
35610
- };
35611
- this.removeSegmentControls = () => {
35612
- _.invoke(this.points, "remove");
35613
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
35614
- _.invoke(this.lines, "remove");
35615
- };
35616
- this.addRayControls = () => {
35617
- this.addLine("ray");
35618
- };
35619
- this.removeRayControls = () => {
35620
- this.removeLine();
35621
- };
35622
- this.addPolygonControls = () => {
35623
- this.polygon = null;
35624
- const coords = InteractiveGraph.getPolygonCoords(this.props.graph, this.props);
35625
- // Clear out our old points so that newly added points don't
35626
- // "collide", as in `addPointControls`
35627
- this.points = [];
35628
- this.points = _.map(coords, this.createPointForPolygonType, this);
35629
- this.updatePolygon();
35630
- };
35631
- this.updatePolygon = () => {
35632
- if (this.props.graph.type !== "polygon") {
35633
- throw makeInvalidTypeError("updatePolygon", "polygon");
35634
- }
35635
- let closed;
35636
- if (this.polygon) {
35637
- closed = this.polygon.closed();
35638
- } else if (this.points.length >= 3) {
35639
- closed = true;
35640
- } else {
35641
- // There will only be fewer than 3 points in click-to-add-vertices
35642
- // mode, so we don't need to explicitly check for that here.
35643
- closed = false;
35644
- }
35645
- const graphie = this.graphie;
35646
- const n = this.points.length;
35647
-
35648
- // TODO(alex): check against "grid" instead, use constants
35649
- const snapToGrid = !_.contains(["angles", "sides"], this.props.graph.snapTo);
35650
- const angleLabels = _.times(n, function (i) {
35651
- if (
35652
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35653
- !this.props.graph.showAngles || !closed && (i === 0 || i === n - 1)) {
35654
- return "";
35655
- }
35656
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35657
- if (this.props.graph.snapTo === "angles") {
35658
- return "$deg0";
35659
- }
35660
- return "$deg1";
35661
- }, this);
35662
- const showRightAngleMarkers = _.times(n, function (i) {
35663
- return closed || i !== 0 && i !== n - 1;
35664
- }, this);
35665
- const numArcs = _.times(n, function (i) {
35666
- if (
35667
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35668
- this.props.graph.showAngles && (closed || i !== 0 && i !== n - 1)) {
35669
- return 1;
35670
- }
35671
- return 0;
35672
- }, this);
35673
- const sideLabels = _.times(n, function (i) {
35674
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35675
- if (!this.props.graph.showSides || !closed && i === n - 1) {
35676
- return "";
35677
- }
35678
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35679
- if (this.props.graph.snapTo === "sides") {
35680
- return "$len0";
35681
- }
35682
- return "$len1";
35683
- }, this);
35684
- if (this.polygon == null) {
35685
- const self = this;
35686
- self.polygon = Interactive2.addMovablePolygon(graphie, {
35687
- constraints: [
35688
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
35689
- Interactive2.MovablePolygon.constraints.bound(), snapToGrid ?
35690
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
35691
- Interactive2.MovablePolygon.constraints.snap() : null],
35692
- closed: closed,
35693
- points: self.points,
35694
- angleLabels: angleLabels,
35695
- showRightAngleMarkers: showRightAngleMarkers,
35696
- numArcs: numArcs,
35697
- sideLabels: sideLabels,
35698
- onMove: [
35699
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
35700
- Interactive2.MovablePolygon.onMove.updatePoints, function () {
35701
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
35702
- if (this.closed()) {
35703
- self.updateCoordsFromPoints();
35704
- }
35705
- }],
35706
- normalStyle: _extends({
35707
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE
35708
- }, this._lineStroke())
35709
- });
35710
- } else {
35711
- // We only need to pass in the properties that might've changed
35712
- this.polygon.update({
35713
- closed: closed,
35714
- points: this.points,
35715
- angleLabels: angleLabels,
35716
- showRightAngleMarkers: showRightAngleMarkers,
35717
- numArcs: numArcs,
35718
- sideLabels: sideLabels
35719
- });
35720
- }
35721
- };
35722
- this.removePolygonControls = () => {
35723
- var _this$polygon11;
35724
- _.invoke(this.points, "remove");
35725
- (_this$polygon11 = this.polygon) == null || _this$polygon11.remove();
35726
- };
35727
- this.addAngleControls = () => {
35728
- if (this.props.graph.type !== "angle") {
35729
- throw makeInvalidTypeError("addAngleControls", "angle");
35730
- }
35731
- const graph = this.props.graph;
35732
- const graphie = this.graphie;
35733
- const coords = InteractiveGraph.getAngleCoords(this.props.graph, this.props);
35734
-
35735
- // The vertex snaps to the grid, but the rays don't...
35736
- this.points = coords.map(function (coord, i) {
35737
- return graphie == null ? void 0 : graphie.addMovablePoint(_.extend({
35738
- coord: coord,
35739
- normalStyle: {
35740
- stroke: KhanColors.INTERACTIVE,
35741
- fill: KhanColors.INTERACTIVE
35742
- }
35743
- }, i === 1 ? {
35744
- snapX: graphie.snap[0],
35745
- snapY: graphie.snap[1]
35746
- } : {}));
35747
- });
35748
-
35749
- // ...they snap to whole-degree angles from the vertex.
35750
- this.angle = graphie == null ? void 0 : graphie.addMovableAngle({
35751
- points: this.points,
35752
- snapDegrees: graph.snapDegrees || 1,
35753
- snapOffsetDeg: graph.angleOffsetDeg || 0,
35754
- angleLabel: graph.showAngles ? "$deg0" : "",
35755
- pushOut: 2,
35756
- allowReflex: defaultVal(graph.allowReflexAngles, true)
35757
- });
35758
- $(this.angle).on("move", () => {
35759
- var _this$angle;
35760
- this.onChange({
35761
- graph: _extends({}, graph, {
35762
- coords: (_this$angle = this.angle) == null ? void 0 : _this$angle.getClockwiseCoords()
35763
- })
35764
- });
35765
- });
35766
- };
35767
- this.removeAngleControls = () => {
35768
- var _this$angle2;
35769
- _.invoke(this.points, "remove");
35770
- (_this$angle2 = this.angle) == null || _this$angle2.remove();
35771
- };
35772
- this.toggleShowSides = () => {
35773
- if (this.props.graph.type !== "polygon") {
35774
- throw makeInvalidTypeError("toggleShowSides", "polygon");
35775
- }
35776
- const graph = _.extend({}, this.props.graph, {
35777
- showSides: !this.props.graph.showSides
35778
- });
35779
- this.onChange({
35780
- graph: graph
35781
- });
35782
- };
35783
- this.focus = $.noop;
35784
- }
35785
- componentDidMount() {
35786
- if (this.refs.graph) {
35787
- // eslint-disable-next-line react/no-string-refs
35788
- // @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
35789
- this.setGraphie(this.refs.graph.graphie());
35790
- }
35791
- }
35792
- UNSAFE_componentWillReceiveProps(nextProps) {
35793
- var _this$props$backgroun, _nextProps$background;
35794
- if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) {
35795
- this.shouldResetGraphie = true;
35796
- this.setState({
35797
- shouldShowInstructions: _getShouldShowInstructions(nextProps)
35798
- });
35799
- }
35800
- if (((_this$props$backgroun = this.props.backgroundImage) == null ? void 0 : _this$props$backgroun.url) !== ((_nextProps$background = nextProps.backgroundImage) == null ? void 0 : _nextProps$background.url) || this.props.backgroundImage !== nextProps.backgroundImage || this.props.containerSizeClass !== nextProps.containerSizeClass) {
35801
- this.shouldResetGraphie = true;
35802
- }
35803
- }
35804
- componentDidUpdate(prevProps, prevState) {
35805
- // TODO(jeremy): This feels underspecified. For example, if the props
35806
- // change the coords of a segment graph, but not the numSegments, we'd
35807
- // not count that as an update.
35808
- const oldType = prevProps.graph.type;
35809
- const newType = this.props.graph.type;
35810
- if (oldType !== newType || prevProps.graph.type === "angle" && this.props.graph.type === "angle" && this.areAngleGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "point" && this.props.graph.type === "point" && this.arePointGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "polygon" && this.props.graph.type === "polygon" && this.arePolygonGraphsEqual(prevProps.graph, this.props.graph) || prevProps.graph.type === "segment" && this.props.graph.type === "segment" && this.areSegmentGraphsEqual(prevProps.graph, this.props.graph)) {
35811
- this["remove" + capitalize(oldType) + "Controls"]();
35812
- this["add" + capitalize(newType) + "Controls"]();
35813
- }
35814
- if (this.shouldResetGraphie) {
35815
- this.resetGraphie();
35816
- }
35817
- }
35818
- areAngleGraphsEqual(prevGraph, currentGraph) {
35819
- return prevGraph.allowReflexAngles !== currentGraph.allowReflexAngles || prevGraph.angleOffsetDeg !== currentGraph.angleOffsetDeg || prevGraph.snapDegrees !== currentGraph.snapDegrees;
35820
- }
35821
- arePointGraphsEqual(prevGraph, currentGraph) {
35822
- return prevGraph.numPoints !== currentGraph.numPoints;
35823
- }
35824
- arePolygonGraphsEqual(prevGraph, currentGraph) {
35825
- return prevGraph.numSides !== currentGraph.numSides || prevGraph.showAngles !== currentGraph.showAngles || prevGraph.showSides !== currentGraph.showSides || prevGraph.snapTo !== currentGraph.snapTo;
35826
- }
35827
- areSegmentGraphsEqual(prevGraph, currentGraph) {
35828
- return prevGraph.numSegments !== currentGraph.numSegments;
35829
- }
35830
- getUserInput() {
35831
- return InteractiveGraph.getUserInputFromProps(this.props);
35832
- }
35833
- render() {
35834
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
35835
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
35836
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
35837
- let instructions;
35838
- // isClickToAddPoints() only applies to points and polygons
35839
- if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
35840
- if (this.props.graph.type === "point") {
35841
- instructions = this.context.strings.addPoints;
35842
- } else if (this.props.graph.type === "polygon") {
35843
- instructions = this.context.strings.addVertices;
35844
- }
35845
- } else {
35846
- instructions = undefined;
35847
- }
35848
- let onMouseDown;
35849
- if (this.isClickToAddPoints()) {
35850
- onMouseDown = this.handleAddPointsMouseDown;
35851
- }
35852
- const isMobile = this.props.apiOptions.isMobile;
35853
- return /*#__PURE__*/React.createElement("div", {
35854
- className: "perseus-widget " + "perseus-widget-interactive-graph",
35855
- style: {
35856
- width: box[0],
35857
- height: box[1]
35858
- }
35859
- }, /*#__PURE__*/React.createElement(Graph, {
35860
- instructions: instructions
35861
- // eslint-disable-next-line react/no-string-refs
35862
- ,
35863
- ref: "graph",
35864
- box: box,
35865
- labels: this.props.labels,
35866
- range: this.props.range,
35867
- step: isMobile ? Util.constrainedTickStepsFromTickSteps(this.props.step, this.props.range) : this.props.step,
35868
- gridStep: gridStep,
35869
- snapStep: snapStep,
35870
- markings: this.props.markings,
35871
- backgroundImage: this.props.backgroundImage,
35872
- showProtractor: this.props.showProtractor,
35873
- onMouseDown: onMouseDown,
35874
- onGraphieUpdated: this.setGraphie,
35875
- setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
35876
- isMobile: isMobile
35877
- }));
35878
- }
35879
- }
35880
- LegacyInteractiveGraph.contextType = PerseusI18nContext;
35881
- LegacyInteractiveGraph.defaultProps = {
35882
- labels: ["x", "y"],
35883
- range: [[-10, 10], [-10, 10]],
35884
- step: [1, 1],
35885
- backgroundImage: defaultBackgroundImage,
35886
- markings: "graph",
35887
- showTooltips: false,
35888
- showProtractor: false,
35889
- graph: {
35890
- type: "linear"
35891
- }
35892
- };
35893
34940
  class InteractiveGraph extends React.Component {
35894
34941
  constructor(...args) {
35895
34942
  super(...args);
35896
- this.legacyGraphRef = /*#__PURE__*/React.createRef();
35897
34943
  this.mafsRef = /*#__PURE__*/React.createRef();
35898
34944
  }
35899
34945
  getUserInput() {
35900
- var _this$mafsRef$current, _this$legacyGraphRef$;
34946
+ var _this$mafsRef$current;
35901
34947
  if ((_this$mafsRef$current = this.mafsRef.current) != null && _this$mafsRef$current.getUserInput) {
35902
34948
  return this.mafsRef.current.getUserInput();
35903
34949
  }
35904
- if ((_this$legacyGraphRef$ = this.legacyGraphRef.current) != null && _this$legacyGraphRef$.getUserInput) {
35905
- return this.legacyGraphRef.current.getUserInput();
35906
- }
35907
34950
  throw new PerseusError("Cannot getUserInput from a graph that has never rendered", Errors.NotAllowed);
35908
34951
  }
35909
34952
  getPromptJSON() {
@@ -35911,26 +34954,17 @@ class InteractiveGraph extends React.Component {
35911
34954
  }
35912
34955
  render() {
35913
34956
  var _this$props$apiOption;
35914
- // Mafs shim
35915
- const mafsFlags = (_this$props$apiOption = this.props.apiOptions) == null || (_this$props$apiOption = _this$props$apiOption.flags) == null ? void 0 : _this$props$apiOption["mafs"];
35916
- if (shouldUseMafs(mafsFlags, this.props.graph)) {
35917
- var _this$props$apiOption2, _this$props$apiOption3;
35918
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
35919
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
35920
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
35921
- return /*#__PURE__*/React.createElement(StatefulMafsGraph, _extends({}, this.props, {
35922
- flags: (_this$props$apiOption2 = this.props.apiOptions) == null ? void 0 : _this$props$apiOption2.flags,
35923
- ref: this.mafsRef,
35924
- gridStep: gridStep,
35925
- snapStep: snapStep,
35926
- box: box,
35927
- showTooltips: !!this.props.showTooltips,
35928
- readOnly: (_this$props$apiOption3 = this.props.apiOptions) == null ? void 0 : _this$props$apiOption3.readOnly
35929
- }));
35930
- }
35931
- return /*#__PURE__*/React.createElement(LegacyInteractiveGraph, _extends({
35932
- ref: this.legacyGraphRef
35933
- }, this.props));
34957
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
34958
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
34959
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
34960
+ return /*#__PURE__*/React.createElement(StatefulMafsGraph, _extends({}, this.props, {
34961
+ ref: this.mafsRef,
34962
+ gridStep: gridStep,
34963
+ snapStep: snapStep,
34964
+ box: box,
34965
+ showTooltips: !!this.props.showTooltips,
34966
+ readOnly: (_this$props$apiOption = this.props.apiOptions) == null ? void 0 : _this$props$apiOption.readOnly
34967
+ }));
35934
34968
  }
35935
34969
 
35936
34970
  /**
@@ -36107,8 +35141,32 @@ class InteractiveGraph extends React.Component {
36107
35141
  }
36108
35142
  static getEquationString(props) {
36109
35143
  const type = props.graph.type;
36110
- const funcName = "get" + capitalize(type) + "EquationString";
36111
- return InteractiveGraph[funcName](props);
35144
+ switch (type) {
35145
+ case "none":
35146
+ return InteractiveGraph.getNoneEquationString();
35147
+ case "linear":
35148
+ return InteractiveGraph.getLinearEquationString(props);
35149
+ case "quadratic":
35150
+ return InteractiveGraph.getQuadraticEquationString(props);
35151
+ case "sinusoid":
35152
+ return InteractiveGraph.getSinusoidEquationString(props);
35153
+ case "circle":
35154
+ return InteractiveGraph.getCircleEquationString(props);
35155
+ case "linear-system":
35156
+ return InteractiveGraph.getLinearSystemEquationString(props);
35157
+ case "point":
35158
+ return InteractiveGraph.getPointEquationString(props);
35159
+ case "segment":
35160
+ return InteractiveGraph.getSegmentEquationString(props);
35161
+ case "ray":
35162
+ return InteractiveGraph.getRayEquationString(props);
35163
+ case "polygon":
35164
+ return InteractiveGraph.getPolygonEquationString(props);
35165
+ case "angle":
35166
+ return InteractiveGraph.getAngleEquationString(props);
35167
+ default:
35168
+ throw new UnreachableCaseError(type);
35169
+ }
36112
35170
  }
36113
35171
  static pointsFromNormalized(props, coordsList, noSnap) {
36114
35172
  // @ts-expect-error - TS2322 - Type 'number[][]' is not assignable to type 'readonly Coord[]'.
@@ -36246,7 +35304,7 @@ class InteractiveGraph extends React.Component {
36246
35304
  }
36247
35305
  }
36248
35306
 
36249
- // exported for testing
35307
+ // We don't need to change any of the original props for static mode
36250
35308
  InteractiveGraph.defaultProps = {
36251
35309
  labels: ["x", "y"],
36252
35310
  range: [[-10, 10], [-10, 10]],
@@ -36259,28 +35317,6 @@ InteractiveGraph.defaultProps = {
36259
35317
  type: "linear"
36260
35318
  }
36261
35319
  };
36262
- function shouldUseMafs(mafsFlags, graph) {
36263
- if (typeof mafsFlags === "boolean" || typeof mafsFlags === "undefined") {
36264
- return false;
36265
- }
36266
- switch (graph.type) {
36267
- case "none":
36268
- return true;
36269
- case "point":
36270
- if (graph.numPoints === UNLIMITED) {
36271
- return Boolean(mafsFlags["unlimited-point"]);
36272
- }
36273
- return Boolean(mafsFlags["point"]);
36274
- case "polygon":
36275
- if (graph.numSides === UNLIMITED) {
36276
- return Boolean(mafsFlags["unlimited-polygon"]);
36277
- }
36278
- return Boolean(mafsFlags["polygon"]);
36279
- default:
36280
- return Boolean(mafsFlags[graph.type]);
36281
- }
36282
- }
36283
- // We don't need to change any of the original props for static mode
36284
35320
  const staticTransform$3 = _.identity;
36285
35321
  var InteractiveGraph$1 = {
36286
35322
  name: "interactive-graph",
@@ -36913,11 +35949,16 @@ class LabelImage extends React.Component {
36913
35949
  });
36914
35950
  }
36915
35951
  activateMarker(index, opened) {
36916
- var _this$props$analytics;
35952
+ var _this$props$analytics, _this$props$analytics2;
35953
+ // TODO(LEMS-2830): Remove analytics event in LEMS-2830 in favor of ti below.
36917
35954
  (_this$props$analytics = this.props.analytics) == null || _this$props$analytics.onAnalyticsEvent({
36918
35955
  type: "perseus:label-image:marker-interacted-with",
36919
35956
  payload: null
36920
35957
  });
35958
+ (_this$props$analytics2 = this.props.analytics) == null || _this$props$analytics2.onAnalyticsEvent({
35959
+ type: "perseus:label-image:marker-interacted-with:ti",
35960
+ payload: null
35961
+ });
36921
35962
  const {
36922
35963
  activeMarkerIndex
36923
35964
  } = this.state;
@@ -37060,11 +36101,16 @@ class LabelImage extends React.Component {
37060
36101
  })),
37061
36102
  multipleSelect: this.props.multipleAnswers,
37062
36103
  onChange: selection => {
37063
- var _this$props$analytics2;
37064
- (_this$props$analytics2 = this.props.analytics) == null || _this$props$analytics2.onAnalyticsEvent({
36104
+ var _this$props$analytics3, _this$props$analytics4;
36105
+ // TODO(LEMS-2829): Remove analytics event in LEMS-2829 in favor of ti below.
36106
+ (_this$props$analytics3 = this.props.analytics) == null || _this$props$analytics3.onAnalyticsEvent({
37065
36107
  type: "perseus:label-image:choiced-interacted-with",
37066
36108
  payload: null
37067
36109
  });
36110
+ (_this$props$analytics4 = this.props.analytics) == null || _this$props$analytics4.onAnalyticsEvent({
36111
+ type: "perseus:label-image:choiced-interacted-with:ti",
36112
+ payload: null
36113
+ });
37068
36114
  this.handleAnswerChoicesChangeForMarker(index, selection);
37069
36115
  },
37070
36116
  onToggle: opened => this.activateMarker(index, opened)
@@ -37160,11 +36206,16 @@ class LabelImage extends React.Component {
37160
36206
  }))), this.renderMarkers()), /*#__PURE__*/React.createElement(HideAnswersToggle, {
37161
36207
  areAnswersHidden: this.state.hideAnswers,
37162
36208
  onChange: hideAnswers => {
37163
- var _this$props$analytics3;
37164
- (_this$props$analytics3 = this.props.analytics) == null || _this$props$analytics3.onAnalyticsEvent({
36209
+ var _this$props$analytics5, _this$props$analytics6;
36210
+ // TODO(LEMS-2831): Remove analytics event in LEMS-2831 in favor of ti below.
36211
+ (_this$props$analytics5 = this.props.analytics) == null || _this$props$analytics5.onAnalyticsEvent({
37165
36212
  type: "perseus:label-image:toggle-answers-hidden",
37166
36213
  payload: null
37167
36214
  });
36215
+ (_this$props$analytics6 = this.props.analytics) == null || _this$props$analytics6.onAnalyticsEvent({
36216
+ type: "perseus:label-image:toggle-answers-hidden:ti",
36217
+ payload: null
36218
+ });
37168
36219
  this.setState({
37169
36220
  hideAnswers
37170
36221
  });
@@ -37227,8 +36278,7 @@ var LabelImage$1 = {
37227
36278
  displayName: "Label Image",
37228
36279
  widget: LabelImageWithDependencies,
37229
36280
  accessible: true,
37230
- isLintable: true,
37231
- getPublicWidgetOptions: getLabelImagePublicWidgetOptions
36281
+ isLintable: true
37232
36282
  };
37233
36283
 
37234
36284
  var ItemState = /*#__PURE__*/function (ItemState) {
@@ -37483,20 +36533,24 @@ class Draggable extends React.Component {
37483
36533
  if (this.props.margin != null) {
37484
36534
  style.margin = this.props.margin;
37485
36535
  }
37486
- return /*#__PURE__*/React.createElement("li", {
37487
- className: className,
37488
- style: style,
37489
- onMouseDown: this.onMouseDown,
37490
- onTouchStart: this.onMouseDown,
37491
- onTouchMove: this.onMouseMove,
37492
- onTouchEnd: this.onMouseUp,
37493
- onTouchCancel: this.onMouseUp
37494
- }, /*#__PURE__*/React.createElement(Renderer, {
37495
- content: this.props.content,
37496
- linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "draggable"),
37497
- onRender: this.props.onRender,
37498
- strings: this.context.strings
37499
- }));
36536
+ return (
36537
+ /*#__PURE__*/
36538
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
36539
+ React.createElement("li", {
36540
+ className: className,
36541
+ style: style,
36542
+ onMouseDown: this.onMouseDown,
36543
+ onTouchStart: this.onMouseDown,
36544
+ onTouchMove: this.onMouseMove,
36545
+ onTouchEnd: this.onMouseUp,
36546
+ onTouchCancel: this.onMouseUp
36547
+ }, /*#__PURE__*/React.createElement(Renderer, {
36548
+ content: this.props.content,
36549
+ linterContext: PerseusLinter.pushContextStack(this.props.linterContext, "draggable"),
36550
+ onRender: this.props.onRender,
36551
+ strings: this.context.strings
36552
+ }))
36553
+ );
37500
36554
  }
37501
36555
  }
37502
36556
  Draggable.contextType = PerseusI18nContext;
@@ -40139,8 +39193,7 @@ var NumberLine$1 = {
40139
39193
  displayName: "Number line",
40140
39194
  widget: NumberLine,
40141
39195
  transform: numberLineTransform,
40142
- staticTransform: staticTransform$1,
40143
- getPublicWidgetOptions: getNumberLinePublicWidgetOptions
39196
+ staticTransform: staticTransform$1
40144
39197
  };
40145
39198
 
40146
39199
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -40308,19 +39361,23 @@ class Card extends React.Component {
40308
39361
  // Pull out the content to get rendered
40309
39362
  const rendererProps = _.pick(this.props, "content");
40310
39363
  const onMouseDown = this.props.animating ? $.noop : this.onMouseDown;
40311
- return /*#__PURE__*/React.createElement("div", {
40312
- className: "card-wrap",
40313
- style: style,
40314
- onMouseDown: onMouseDown,
40315
- onTouchStart: onMouseDown,
40316
- onTouchEnd: this.onMouseUp,
40317
- onTouchCancel: this.onMouseUp
40318
- }, /*#__PURE__*/React.createElement("div", {
40319
- className: className.join(" ")
40320
- }, /*#__PURE__*/React.createElement(Renderer, _extends({}, rendererProps, {
40321
- linterContext: this.props.linterContext,
40322
- strings: this.context.strings
40323
- }))));
39364
+ return (
39365
+ /*#__PURE__*/
39366
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO(LEMS-2871): Address a11y error
39367
+ React.createElement("div", {
39368
+ className: "card-wrap",
39369
+ style: style,
39370
+ onMouseDown: onMouseDown,
39371
+ onTouchStart: onMouseDown,
39372
+ onTouchEnd: this.onMouseUp,
39373
+ onTouchCancel: this.onMouseUp
39374
+ }, /*#__PURE__*/React.createElement("div", {
39375
+ className: className.join(" ")
39376
+ }, /*#__PURE__*/React.createElement(Renderer, _extends({}, rendererProps, {
39377
+ linterContext: this.props.linterContext,
39378
+ strings: this.context.strings
39379
+ }))))
39380
+ );
40324
39381
  }
40325
39382
  }
40326
39383
  Card.contextType = PerseusI18nContext;
@@ -40700,8 +39757,7 @@ var Orderer$1 = {
40700
39757
  displayName: "Orderer",
40701
39758
  hidden: true,
40702
39759
  widget: Orderer,
40703
- isLintable: true,
40704
- getPublicWidgetOptions: getOrdererPublicWidgetOptions
39760
+ isLintable: true
40705
39761
  };
40706
39762
 
40707
39763
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -41923,8 +40979,7 @@ var Sorter$1 = {
41923
40979
  name: "sorter",
41924
40980
  displayName: "Sorter",
41925
40981
  widget: Sorter,
41926
- isLintable: true,
41927
- getPublicWidgetOptions: getSorterPublicWidgetOptions
40982
+ isLintable: true
41928
40983
  };
41929
40984
 
41930
40985
  const {
@@ -42322,7 +41377,7 @@ const init = function init() {
42322
41377
 
42323
41378
  // This file is processed by a Rollup plugin (replace) to inject the production
42324
41379
  const libName = "@khanacademy/perseus";
42325
- const libVersion = "53.0.0";
41380
+ const libVersion = "54.0.0";
42326
41381
  addLibraryVersionToPerseusDebug(libName, libVersion);
42327
41382
 
42328
41383
  const apiVersion = {
@@ -43809,6 +42864,18 @@ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
43809
42864
  const widget = widgetMap[WidgetId];
43810
42865
  return (_widget$type = widget == null ? void 0 : widget.type) != null ? _widget$type : null;
43811
42866
  }
42867
+ function getWidgetSubTypeByWidgetId(widgetId, widgetMap) {
42868
+ var _widget$type2, _graph$type;
42869
+ const widget = widgetMap[widgetId];
42870
+ const widgetType = (_widget$type2 = widget == null ? void 0 : widget.type) != null ? _widget$type2 : null;
42871
+ switch (widgetType) {
42872
+ case "interactive-graph":
42873
+ const graph = widget.options.graph;
42874
+ return (_graph$type = graph == null ? void 0 : graph.type) != null ? _graph$type : null;
42875
+ default:
42876
+ return null;
42877
+ }
42878
+ }
43812
42879
 
43813
42880
  /**
43814
42881
  * Does the content have a specific type of widget?
@@ -44601,5 +43668,5 @@ const ScoringUtil = {
44601
43668
  * Types
44602
43669
  */
44603
43670
 
44604
- export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseDataFromJSONP, preprocessTex, registerAllWidgetsForTesting, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
43671
+ export { ApiOptions, ArticleRenderer, BaseRadio, Categorizer$1 as Categorizer, changeable as Changeable, ClassNames, dependencies as Dependencies, EditorJsonify, Expression, GrapherUtil, Grapher$1 as GrapherWidget, HintRenderer, HintsRenderer, InputNumber$1 as InputNumber, InteractiveGraph$1 as InteractiveGraphWidget, JiptParagraphs, KhanColors, context as LoadingContext, Log, Matrix$1 as MatrixWidget, NumericInput$1 as NumericInput, PerseusI18nContext, PerseusI18nContextProvider, PerseusMarkdown, Plotter$1 as PlotterWidget, Radio, Renderer, ScoringUtil, ref as ServerItemRenderer, Table$1 as TableWidget, Util, denylist as WIDGET_PROP_DENYLIST, widgets$1 as Widgets, apiVersion, bodyXsmallBold, components, containerSizeClass, contentHasWidgetType, convertWidgetNameToEnum, displaySigFigs, generateTestCategorizerWidget, generateTestExpressionWidget, generateTestInteractiveGraphWidget, generateTestNumericInputWidget, generateTestPerseusItem, generateTestRadioWidget, genericPerseusItemData, getAngleCoords, getAnswerFromUserInput, getAnswersFromWidgets, getCircleCoords, getCorrectAnswerForWidgetId, getImagesWithoutAltData, getInteractiveBoxFromSizeClass, getLineCoords, getLinearSystemCoords, getPointCoords, getPolygonCoords, getQuadraticCoords, getSegmentCoords, getSinusoidCoords, getValidWidgetIds, getWidgetFromWidgetMap, getWidgetSubTypeByWidgetId, getWidgetTypeByWidgetId, getWidgetsFromWidgetMap, getWidgetsMapFromItemData, iconChevronDown, iconTrash, init, injectWidgets, interactiveSizes$1 as interactiveSizes, isItemRenderableByVersion, isWidgetIdInContent, isWrongAnswerSupported, itemDataVersion, ItemVersion as itemVersion, libVersion, makeSafeUrl, mathOnlyParser, mediaQueries, parseDataFromJSONP, preprocessTex, registerAllWidgetsForTesting, shouldHaveIndividualAnswer, traverse, usePerseusI18n, violatingWidgets, allWidgets as widgets };
44605
43672
  //# sourceMappingURL=index.js.map