@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/index.js CHANGED
@@ -33,7 +33,6 @@ var Banner = require('@khanacademy/wonder-blocks-banner');
33
33
  var wonderBlocksDropdown = require('@khanacademy/wonder-blocks-dropdown');
34
34
  var caretDown = require('@phosphor-icons/core/regular/caret-down.svg');
35
35
  var caretUp = require('@phosphor-icons/core/regular/caret-up.svg');
36
- var debounce$1 = require('lodash.debounce');
37
36
  var mafs = require('mafs');
38
37
  var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
39
38
  var questionIcon = require('@phosphor-icons/core/regular/question.svg');
@@ -82,7 +81,6 @@ var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
82
81
  var Banner__default = /*#__PURE__*/_interopDefaultLegacy(Banner);
83
82
  var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
84
83
  var caretUp__default = /*#__PURE__*/_interopDefaultLegacy(caretUp);
85
- var debounce__default = /*#__PURE__*/_interopDefaultLegacy(debounce$1);
86
84
  var questionIcon__default = /*#__PURE__*/_interopDefaultLegacy(questionIcon);
87
85
  var Switch__default = /*#__PURE__*/_interopDefaultLegacy(Switch);
88
86
  var Pill__default = /*#__PURE__*/_interopDefaultLegacy(Pill);
@@ -368,6 +366,10 @@ const mockStrings = {
368
366
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
369
367
  selectAnAnswer: "Select an answer",
370
368
  // The following strings are used for interactive graph SR descriptions.
369
+ 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.",
370
+ 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.",
371
+ xAxis: "X-axis",
372
+ yAxis: "Y-axis",
371
373
  graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
372
374
  addPoint: "Add Point",
373
375
  removePoint: "Remove Point",
@@ -379,7 +381,7 @@ const mockStrings = {
379
381
  x,
380
382
  y
381
383
  } = _ref27;
382
- return `Point ${num} at ${x} comma ${y}`;
384
+ return `Point ${num} at ${x} comma ${y}.`;
383
385
  },
384
386
  srInteractiveElements: _ref28 => {
385
387
  let {
@@ -465,25 +467,37 @@ const mockStrings = {
465
467
  } = _ref37;
466
468
  return `Line from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`;
467
469
  },
468
- srAngleSideAtCoordinates: _ref38 => {
470
+ srAngleStartingSide: _ref38 => {
469
471
  let {
470
- point,
471
- side,
472
472
  x,
473
473
  y
474
474
  } = _ref38;
475
- return `Point ${point}, ${side} at ${x} comma ${y}`;
475
+ return `Point 3, starting side at ${x} comma ${y}.`;
476
476
  },
477
- srAngleVertexAtCoordinatesWithAngleMeasure: _ref39 => {
477
+ srAngleEndingSide: _ref39 => {
478
+ let {
479
+ x,
480
+ y
481
+ } = _ref39;
482
+ return `Point 2, ending side at ${x} comma ${y}.`;
483
+ },
484
+ srAngleVertex: _ref40 => {
485
+ let {
486
+ x,
487
+ y
488
+ } = _ref40;
489
+ return `Point 1, vertex at ${x} comma ${y}.`;
490
+ },
491
+ srAngleVertexWithAngleMeasure: _ref41 => {
478
492
  let {
479
493
  x,
480
494
  y,
481
495
  angleMeasure
482
- } = _ref39;
483
- return `Point 2, vertex at ${x} comma ${y}. Angle ${angleMeasure} degrees`;
496
+ } = _ref41;
497
+ return `Point 1, vertex at ${x} comma ${y}. Angle ${angleMeasure} degrees.`;
484
498
  },
485
499
  srAngleGraphAriaLabel: "An angle on a coordinate plane.",
486
- srAngleGraphAriaDescription: _ref40 => {
500
+ srAngleGraphAriaDescription: _ref42 => {
487
501
  let {
488
502
  angleMeasure,
489
503
  vertexX,
@@ -492,118 +506,129 @@ const mockStrings = {
492
506
  startingSideY,
493
507
  endingSideX,
494
508
  endingSideY
495
- } = _ref40;
509
+ } = _ref42;
496
510
  return `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}.`;
497
511
  },
512
+ srAngleInteractiveElements: _ref43 => {
513
+ let {
514
+ vertexX,
515
+ vertexY,
516
+ startingSideX,
517
+ startingSideY,
518
+ endingSideX,
519
+ endingSideY
520
+ } = _ref43;
521
+ return `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}.`;
522
+ },
498
523
  srSingleSegmentGraphAriaLabel: "A line segment on a coordinate plane.",
499
- srMultipleSegmentGraphAriaLabel: _ref41 => {
524
+ srMultipleSegmentGraphAriaLabel: _ref44 => {
500
525
  let {
501
526
  countOfSegments
502
- } = _ref41;
527
+ } = _ref44;
503
528
  return `${countOfSegments} segments on a coordinate plane.`;
504
529
  },
505
- srMultipleSegmentIndividualLabel: _ref42 => {
530
+ srMultipleSegmentIndividualLabel: _ref45 => {
506
531
  let {
507
532
  point1X,
508
533
  point1Y,
509
534
  point2X,
510
535
  point2Y,
511
536
  indexOfSegment
512
- } = _ref42;
537
+ } = _ref45;
513
538
  return `Segment ${indexOfSegment}: Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`;
514
539
  },
515
- srSingleSegmentLabel: _ref43 => {
540
+ srSingleSegmentLabel: _ref46 => {
516
541
  let {
517
542
  point1X,
518
543
  point1Y,
519
544
  point2X,
520
545
  point2Y
521
- } = _ref43;
546
+ } = _ref46;
522
547
  return `Endpoint 1 at ${point1X} comma ${point1Y}. Endpoint 2 at ${point2X} comma ${point2Y}.`;
523
548
  },
524
- srSegmentLength: _ref44 => {
549
+ srSegmentLength: _ref47 => {
525
550
  let {
526
551
  length
527
- } = _ref44;
552
+ } = _ref47;
528
553
  return `Segment length ${length} units.`;
529
554
  },
530
- srSingleSegmentGraphEndpointAriaLabel: _ref45 => {
555
+ srSingleSegmentGraphEndpointAriaLabel: _ref48 => {
531
556
  let {
532
557
  endpointNumber,
533
558
  x,
534
559
  y
535
- } = _ref45;
560
+ } = _ref48;
536
561
  return `Endpoint ${endpointNumber} at ${x} comma ${y}.`;
537
562
  },
538
- srMultipleSegmentGraphEndpointAriaLabel: _ref46 => {
563
+ srMultipleSegmentGraphEndpointAriaLabel: _ref49 => {
539
564
  let {
540
565
  endpointNumber,
541
566
  x,
542
567
  y,
543
568
  indexOfSegment
544
- } = _ref46;
569
+ } = _ref49;
545
570
  return `Endpoint ${endpointNumber} on segment ${indexOfSegment} at ${x} comma ${y}.`;
546
571
  },
547
- srSegmentGrabHandle: _ref47 => {
572
+ srSegmentGrabHandle: _ref50 => {
548
573
  let {
549
574
  point1X,
550
575
  point1Y,
551
576
  point2X,
552
577
  point2Y
553
- } = _ref47;
578
+ } = _ref50;
554
579
  return `Segment from ${point1X} comma ${point1Y} to ${point2X} comma ${point2Y}.`;
555
580
  },
556
581
  srLinearSystemGraph: "Two lines on a coordinate plane.",
557
- srLinearSystemPoints: _ref48 => {
582
+ srLinearSystemPoints: _ref51 => {
558
583
  let {
559
584
  lineNumber,
560
585
  point1X,
561
586
  point1Y,
562
587
  point2X,
563
588
  point2Y
564
- } = _ref48;
589
+ } = _ref51;
565
590
  return `Line ${lineNumber} has two points, point 1 at ${point1X} comma ${point1Y} and point 2 at ${point2X} comma ${point2Y}.`;
566
591
  },
567
- srLinearSystemPoint: _ref49 => {
592
+ srLinearSystemPoint: _ref52 => {
568
593
  let {
569
594
  lineNumber,
570
595
  pointSequence,
571
596
  x,
572
597
  y
573
- } = _ref49;
598
+ } = _ref52;
574
599
  return `Point ${pointSequence} on line ${lineNumber} at ${x} comma ${y}.`;
575
600
  },
576
601
  srRayGraph: "A ray on a coordinate plane.",
577
- srRayPoints: _ref50 => {
602
+ srRayPoints: _ref53 => {
578
603
  let {
579
604
  point1X,
580
605
  point1Y,
581
606
  point2X,
582
607
  point2Y
583
- } = _ref50;
608
+ } = _ref53;
584
609
  return `The endpoint is at ${point1X} comma ${point1Y} and the ray goes through point ${point2X} comma ${point2Y}.`;
585
610
  },
586
- srRayGrabHandle: _ref51 => {
611
+ srRayGrabHandle: _ref54 => {
587
612
  let {
588
613
  point1X,
589
614
  point1Y,
590
615
  point2X,
591
616
  point2Y
592
- } = _ref51;
617
+ } = _ref54;
593
618
  return `Ray with endpoint ${point1X} comma ${point1Y} going through point ${point2X} comma ${point2Y}.`;
594
619
  },
595
- srRayEndpoint: _ref52 => {
620
+ srRayEndpoint: _ref55 => {
596
621
  let {
597
622
  x,
598
623
  y
599
- } = _ref52;
624
+ } = _ref55;
600
625
  return `Endpoint at ${x} comma ${y}.`;
601
626
  },
602
- srRayTerminalPoint: _ref53 => {
627
+ srRayTerminalPoint: _ref56 => {
603
628
  let {
604
629
  x,
605
630
  y
606
- } = _ref53;
631
+ } = _ref56;
607
632
  return `Through point at ${x} comma ${y}.`;
608
633
  },
609
634
  srQuadraticGraph: "A parabola on a 4-quadrant coordinate plane.",
@@ -612,55 +637,55 @@ const mockStrings = {
612
637
  srQuadraticGraphVertexOrigin: "Vertex is at the origin.",
613
638
  srQuadraticGraphVertexXAxis: "Vertex is on the X-axis.",
614
639
  srQuadraticGraphVertexYAxis: "Vertex is on the Y-axis.",
615
- srQuadraticGraphVertexQuadrant: _ref54 => {
640
+ srQuadraticGraphVertexQuadrant: _ref57 => {
616
641
  let {
617
642
  quadrant
618
- } = _ref54;
643
+ } = _ref57;
619
644
  return `Vertex is in quadrant ${quadrant}.`;
620
645
  },
621
- srQuadraticTwoXIntercepts: _ref55 => {
646
+ srQuadraticTwoXIntercepts: _ref58 => {
622
647
  let {
623
648
  intercept1,
624
649
  intercept2
625
- } = _ref55;
650
+ } = _ref58;
626
651
  return `The X-intercepts are at ${intercept1} comma 0 and ${intercept2} comma 0.`;
627
652
  },
628
- srQuadraticOneXIntercept: _ref56 => {
653
+ srQuadraticOneXIntercept: _ref59 => {
629
654
  let {
630
655
  intercept
631
- } = _ref56;
656
+ } = _ref59;
632
657
  return `The X-intercept is at ${intercept} comma 0.`;
633
658
  },
634
- srQuadraticYIntercept: _ref57 => {
659
+ srQuadraticYIntercept: _ref60 => {
635
660
  let {
636
661
  intercept
637
- } = _ref57;
662
+ } = _ref60;
638
663
  return `The Y-intercept is at 0 comma ${intercept}.`;
639
664
  },
640
- srQuadraticPointOrigin: _ref58 => {
665
+ srQuadraticPointOrigin: _ref61 => {
641
666
  let {
642
667
  pointNumber
643
- } = _ref58;
668
+ } = _ref61;
644
669
  return `Point ${pointNumber} on parabola at the origin.`;
645
670
  },
646
- srQuadraticPointAxis: _ref59 => {
671
+ srQuadraticPointAxis: _ref62 => {
647
672
  let {
648
673
  pointNumber,
649
674
  x,
650
675
  y
651
- } = _ref59;
676
+ } = _ref62;
652
677
  return `Point ${pointNumber} on parabola at ${x} comma ${y}.`;
653
678
  },
654
- srQuadraticPointQuadrant: _ref60 => {
679
+ srQuadraticPointQuadrant: _ref63 => {
655
680
  let {
656
681
  pointNumber,
657
682
  x,
658
683
  y,
659
684
  quadrant
660
- } = _ref60;
685
+ } = _ref63;
661
686
  return `Point ${pointNumber} on parabola in quadrant ${quadrant} at ${x} comma ${y}.`;
662
687
  },
663
- srQuadraticInteractiveElements: _ref61 => {
688
+ srQuadraticInteractiveElements: _ref64 => {
664
689
  let {
665
690
  point1X,
666
691
  point1Y,
@@ -668,8 +693,84 @@ const mockStrings = {
668
693
  point2Y,
669
694
  point3X,
670
695
  point3Y
671
- } = _ref61;
696
+ } = _ref64;
672
697
  return `Parabola with points at ${point1X} comma ${point1Y}, ${point2X} comma ${point2Y}, and ${point3X} comma ${point3Y}.`;
698
+ },
699
+ srPolygonGraph: "A polygon.",
700
+ srPolygonGraphCoordinatePlane: "A polygon on a coordinate plane.",
701
+ srPolygonGraphPointsNum: _ref65 => {
702
+ let {
703
+ num
704
+ } = _ref65;
705
+ return `The polygon has ${num} points.`;
706
+ },
707
+ srPolygonGraphPointsOne: "The polygon has 1 point.",
708
+ srPolygonElementsNum: _ref66 => {
709
+ let {
710
+ num
711
+ } = _ref66;
712
+ return `A polygon with ${num} points.`;
713
+ },
714
+ srPolygonElementsOne: "A polygon with 1 point.",
715
+ srPolygonPointAngleApprox: _ref67 => {
716
+ let {
717
+ angle
718
+ } = _ref67;
719
+ return `Angle approximately equal to ${angle} degrees.`;
720
+ },
721
+ srPolygonPointAngle: _ref68 => {
722
+ let {
723
+ angle
724
+ } = _ref68;
725
+ return `Angle equal to ${angle} degrees.`;
726
+ },
727
+ srPolygonSideLength: _ref69 => {
728
+ let {
729
+ pointNum,
730
+ length
731
+ } = _ref69;
732
+ return `A line segment, length equal to ${length} units, connects to point ${pointNum}.`;
733
+ },
734
+ srPolygonSideLengthApprox: _ref70 => {
735
+ let {
736
+ pointNum,
737
+ length
738
+ } = _ref70;
739
+ return `A line segment, length approximately equal to ${length} units, connects to point ${pointNum}.`;
740
+ },
741
+ srUnlimitedPolygonEmpty: "An empty coordinate plane.",
742
+ srSinusoidGraph: "A sinusoid function on a coordinate plane.",
743
+ srSinusoidRootPoint: _ref71 => {
744
+ let {
745
+ x,
746
+ y
747
+ } = _ref71;
748
+ return `Midline intersection at ${x} comma ${y}.`;
749
+ },
750
+ srSinusoidPeakPoint: _ref72 => {
751
+ let {
752
+ x,
753
+ y
754
+ } = _ref72;
755
+ return `Extremum point at ${x} comma ${y}.`;
756
+ },
757
+ srSinusoidDescription: _ref73 => {
758
+ let {
759
+ minValue,
760
+ maxValue,
761
+ cycleStart,
762
+ cycleEnd
763
+ } = _ref73;
764
+ return `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}.`;
765
+ },
766
+ srSinusoidInteractiveElements: _ref74 => {
767
+ let {
768
+ point1X,
769
+ point1Y,
770
+ point2X,
771
+ point2Y
772
+ } = _ref74;
773
+ return `Sinusoid graph with midline intersection point at ${point1X} comma ${point1Y} and extremum point at ${point2X} comma ${point2Y}.`;
673
774
  }
674
775
  // The above strings are used for interactive graph SR descriptions.
675
776
  };
@@ -1592,6 +1693,7 @@ const ApiOptions = {
1592
1693
  },
1593
1694
  baseElements: {
1594
1695
  Link: props => {
1696
+ // eslint-disable-next-line jsx-a11y/anchor-has-content -- TODO(LEMS-2871): Address a11y error
1595
1697
  return /*#__PURE__*/React__namespace.createElement("a", props);
1596
1698
  }
1597
1699
  },
@@ -1992,7 +2094,6 @@ var Expression$1 = {
1992
2094
  propUpgrades: perseusCore.expressionLogic.widgetOptionsUpgrades,
1993
2095
  // For use by the editor
1994
2096
  isLintable: true,
1995
- getPublicWidgetOptions: perseusCore.getExpressionPublicWidgetOptions,
1996
2097
  // TODO(LEMS-2656): remove TS suppression
1997
2098
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusExpressionRubric'.
1998
2099
  getOneCorrectAnswerFromRubric(rubric) {
@@ -10059,7 +10160,7 @@ class WrappedLine extends WrappedDrawing {
10059
10160
 
10060
10161
  /* eslint-disable @babel/no-invalid-this */
10061
10162
  const {
10062
- clockwise: clockwise$3,
10163
+ clockwise: clockwise$2,
10063
10164
  reverseVector: reverseVector$1
10064
10165
  } = kmath.geometry;
10065
10166
  function scaledDistanceFromAngle(angle) {
@@ -11297,7 +11398,7 @@ ___default["default"].extend(GraphUtils.Graphie.prototype, {
11297
11398
  // Temporary objects
11298
11399
  ___default["default"].invoke(polygon.temp, "remove");
11299
11400
  polygon.temp = [];
11300
- const isClockwise = clockwise$3(polygon.coords);
11401
+ const isClockwise = clockwise$2(polygon.coords);
11301
11402
 
11302
11403
  // Update angle labels
11303
11404
  if (polygon.angleLabels.length || polygon.showRightAngleMarkers.length) {
@@ -14730,7 +14831,7 @@ function sum(array) {
14730
14831
  return memo + arg;
14731
14832
  }, 0);
14732
14833
  }
14733
- function clockwise$2(points) {
14834
+ function clockwise$1(points) {
14734
14835
  const segments = ___default["default"].zip(points, points.slice(1).concat(points.slice(0, 1)));
14735
14836
  const areas = ___default["default"].map(segments, function (segment) {
14736
14837
  const p1 = segment[0];
@@ -14796,7 +14897,7 @@ const draw$1 = {
14796
14897
  const graphie = this.graphie;
14797
14898
  const self = this;
14798
14899
  const coords = ___default["default"].invoke(state.points, "coord");
14799
- const isClockwise = clockwise$2(coords);
14900
+ const isClockwise = clockwise$1(coords);
14800
14901
  const n = coords.length;
14801
14902
 
14802
14903
  // graphie.labelAngle and similar methods attempt to re-use the label
@@ -15968,12 +16069,17 @@ class ImageLoader extends React__namespace.Component {
15968
16069
  };
15969
16070
  }
15970
16071
  const staticUrl = getDependencies().staticUrl;
15971
- return /*#__PURE__*/React__namespace.createElement("img", _extends({
15972
- tabIndex: 0,
15973
- src: staticUrl(src),
15974
- onKeyUp: onKeyUp,
15975
- onKeyDown: onKeyDown
15976
- }, imgProps));
16072
+ return (
16073
+ /*#__PURE__*/
16074
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
16075
+ React__namespace.createElement("img", _extends({
16076
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- TODO(LEMS-2871): Address a11y error
16077
+ tabIndex: 0,
16078
+ src: staticUrl(src),
16079
+ onKeyUp: onKeyUp,
16080
+ onKeyDown: onKeyDown
16081
+ }, imgProps))
16082
+ );
15977
16083
  };
15978
16084
  render() {
15979
16085
  switch (this.state.status) {
@@ -16542,9 +16648,28 @@ class Zoomable extends React__namespace.Component {
16542
16648
  if (!this._isMounted) {
16543
16649
  return;
16544
16650
  }
16651
+
16652
+ // When a zoomable item is contained within a collapsed parent (such as an Explanation widget),
16653
+ // the parent node has zero width, which causes the scale to be miscalculated.
16654
+ // Therefore, we check to see if the node has a valid width, and if it doesn't,
16655
+ // then we traverse up the DOM tree and look for the closest "perseus-renderer"
16656
+ // (which should have a width).
16657
+ // To be cautious, we also account for the possibility that we can't find any parents with a valid width
16658
+ // (not that they don't exist, just that we can't find them).
16659
+ let parentNode = this._node;
16660
+ let currentNode = parentNode;
16661
+ while (currentNode && currentNode.offsetWidth === 0) {
16662
+ // When traversing up the DOM tree, we need to start at the "parentElement"
16663
+ // because "closest()" can match the current element,
16664
+ // and if it does so, then we are stuck in an infinite loop.
16665
+ currentNode = currentNode.parentElement?.closest(".perseus-renderer") ?? null;
16666
+ }
16667
+ if (currentNode) {
16668
+ parentNode = currentNode;
16669
+ }
16545
16670
  const parentBounds = {
16546
- width: this._node.offsetWidth,
16547
- height: this._node.offsetHeight
16671
+ width: parentNode.offsetWidth,
16672
+ height: parentNode.offsetHeight
16548
16673
  };
16549
16674
  const childBounds = this.props.computeChildBounds(this._node, parentBounds);
16550
16675
 
@@ -16628,16 +16753,20 @@ class Zoomable extends React__namespace.Component {
16628
16753
  WebkitTapHighlightColor: "transparent",
16629
16754
  ...transitionStyle
16630
16755
  };
16631
- return /*#__PURE__*/React__namespace.createElement("span", {
16632
- onClick: this.handleClick,
16633
- onClickCapture: this.handleClickIfZoomed,
16634
- onTouchCancelCapture: this.stopPropagationIfZoomed,
16635
- onTouchEndCapture: this.stopPropagationIfZoomed,
16636
- onTouchStartCapture: this.stopPropagationIfZoomed
16637
- // @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'.
16638
- ,
16639
- style: style
16640
- }, this.props.children);
16756
+ return (
16757
+ /*#__PURE__*/
16758
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- TODO(LEMS-2871): Address a11y error
16759
+ React__namespace.createElement("span", {
16760
+ onClick: this.handleClick,
16761
+ onClickCapture: this.handleClickIfZoomed,
16762
+ onTouchCancelCapture: this.stopPropagationIfZoomed,
16763
+ onTouchEndCapture: this.stopPropagationIfZoomed,
16764
+ onTouchStartCapture: this.stopPropagationIfZoomed
16765
+ // @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'.
16766
+ ,
16767
+ style: style
16768
+ }, this.props.children)
16769
+ );
16641
16770
  }
16642
16771
  }
16643
16772
 
@@ -17860,9 +17989,6 @@ const getWidget = name => {
17860
17989
  const getWidgetExport = name => {
17861
17990
  return widgets[name] ?? null;
17862
17991
  };
17863
- const getPublicWidgetOptionsFunction = name => {
17864
- return widgets[name]?.getPublicWidgetOptions ?? (i => i);
17865
- };
17866
17992
  const getEditor = name => {
17867
17993
  return ___default["default"].has(editors, name) ? editors[name] : null;
17868
17994
  };
@@ -17999,7 +18125,6 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
17999
18125
  replaceDeprecatedEditors: replaceDeprecatedEditors,
18000
18126
  getWidget: getWidget,
18001
18127
  getWidgetExport: getWidgetExport,
18002
- getPublicWidgetOptionsFunction: getPublicWidgetOptionsFunction,
18003
18128
  getEditor: getEditor,
18004
18129
  getTransform: getTransform,
18005
18130
  getVersion: getVersion,
@@ -18075,6 +18200,7 @@ class WidgetContainer extends React__namespace.Component {
18075
18200
  "perseus-widget__definition": this.props.type === "definition"
18076
18201
  });
18077
18202
  const type = this.props.type;
18203
+ const userAgent = navigator.userAgent;
18078
18204
  const WidgetType = getWidget(type);
18079
18205
  if (WidgetType == null) {
18080
18206
  // This is for the good of all people!!
@@ -18085,6 +18211,11 @@ class WidgetContainer extends React__namespace.Component {
18085
18211
  className: className
18086
18212
  });
18087
18213
  }
18214
+ let subType = "null";
18215
+ if (type === "interactive-graph") {
18216
+ const props = this.state.widgetProps;
18217
+ subType = props.graph?.type ?? "null";
18218
+ }
18088
18219
  let alignment = this.state.widgetProps.alignment;
18089
18220
  if (alignment === "default") {
18090
18221
  alignment = perseusCore.CoreWidgetRegistry.getDefaultAlignment(type);
@@ -18141,12 +18272,26 @@ class WidgetContainer extends React__namespace.Component {
18141
18272
  widget_type: type,
18142
18273
  widget_id: this.props.id
18143
18274
  },
18144
- onError: () => {
18275
+ onError: error => {
18276
+ // TODO(LEMS-2826): Remove analytics event in LEMS-2826 in favor of ti below.
18145
18277
  analytics.onAnalyticsEvent({
18146
18278
  type: "perseus:widget-rendering-error",
18147
18279
  payload: {
18280
+ widgetSubType: subType,
18281
+ widgetType: type,
18282
+ widgetId: this.props.id,
18283
+ message: error.message,
18284
+ userAgent: userAgent
18285
+ }
18286
+ });
18287
+ analytics.onAnalyticsEvent({
18288
+ type: "perseus:widget-rendering-error:ti",
18289
+ payload: {
18290
+ widgetSubType: subType,
18148
18291
  widgetType: type,
18149
- widgetId: this.props.id
18292
+ widgetId: this.props.id,
18293
+ message: error.message,
18294
+ userAgent: userAgent
18150
18295
  }
18151
18296
  });
18152
18297
  }
@@ -20589,7 +20734,6 @@ var NumericInput$1 = {
20589
20734
  widget: NumericInput,
20590
20735
  transform: propsTransform,
20591
20736
  isLintable: true,
20592
- getPublicWidgetOptions: perseusCore.getNumericInputPublicWidgetOptions,
20593
20737
  // TODO(LEMS-2656): remove TS suppression
20594
20738
  // @ts-expect-error: Type 'Rubric' is not assignable to type 'PerseusNumericInputRubric'
20595
20739
  getOneCorrectAnswerFromRubric(rubric) {
@@ -21396,7 +21540,10 @@ class HighlightTooltip extends React__namespace.PureComponent {
21396
21540
  // using div instead of TooltipContent because
21397
21541
  // TooltipContent wouldn't let me overwrite
21398
21542
  // user-select and onClick
21399
- const content = /*#__PURE__*/React__namespace.createElement("div", {
21543
+ const content =
21544
+ /*#__PURE__*/
21545
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- TODO(LEMS-2871): Address a11y error
21546
+ React__namespace.createElement("div", {
21400
21547
  className: aphrodite.css(styles$x.tooltipLabel),
21401
21548
  onClick: this.props.onClick
21402
21549
  }, this.props.label);
@@ -21407,14 +21554,18 @@ class HighlightTooltip extends React__namespace.PureComponent {
21407
21554
  top: `calc(${Math.round(focusRect.top)}px + 0.95em)`,
21408
21555
  height: 0
21409
21556
  };
21410
- return /*#__PURE__*/React__namespace.createElement("div", {
21411
- style: style,
21412
- onMouseEnter: this.props.onMouseEnter,
21413
- onMouseLeave: this.props.onMouseLeave
21414
- }, /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
21415
- content: content,
21416
- opened: true
21417
- }, /*#__PURE__*/React__namespace.createElement("div", null)));
21557
+ return (
21558
+ /*#__PURE__*/
21559
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO(LEMS-2871): Address a11y error
21560
+ React__namespace.createElement("div", {
21561
+ style: style,
21562
+ onMouseEnter: this.props.onMouseEnter,
21563
+ onMouseLeave: this.props.onMouseLeave
21564
+ }, /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
21565
+ content: content,
21566
+ opened: true
21567
+ }, /*#__PURE__*/React__namespace.createElement("div", null)))
21568
+ );
21418
21569
  }
21419
21570
  }
21420
21571
  const styles$x = aphrodite.StyleSheet.create({
@@ -23377,8 +23528,8 @@ const styles$s = aphrodite.StyleSheet.create({
23377
23528
  crossOutLine: {
23378
23529
  // Center the icon within the container.
23379
23530
  position: "absolute",
23380
- top: `0px`,
23381
- left: `0px`
23531
+ top: `2px`,
23532
+ left: `2px`
23382
23533
  }
23383
23534
  });
23384
23535
 
@@ -23405,43 +23556,6 @@ function ChoiceInner(props) {
23405
23556
  }
23406
23557
  return /*#__PURE__*/React__namespace.createElement(InlineIcon, iconMinus);
23407
23558
  }
23408
-
23409
- // Handle dynamic styling of the multiple choice icon. Most
23410
- // MC icon styles are constant, but we do allow the caller
23411
- // to specify the selected color, and thus must control styles
23412
- // related to the selected state dynamically.
23413
- function getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct) {
23414
- let backgroundColor;
23415
- let borderColor;
23416
- let color;
23417
- if (!showCorrectness && pressed) {
23418
- borderColor = wonderBlocksTokens.color.blue;
23419
- color = wonderBlocksTokens.color.blue;
23420
- backgroundColor = "transparent";
23421
- } else if (checked) {
23422
- const bg = showCorrectness && correct ? wonderBlocksTokens.color.green : wonderBlocksTokens.color.blue;
23423
- color = white;
23424
- backgroundColor = bg;
23425
- borderColor = bg;
23426
- } else {
23427
- borderColor = wonderBlocksTokens.color.offBlack64;
23428
- color = wonderBlocksTokens.color.offBlack64;
23429
- }
23430
-
23431
- // define shape
23432
- let borderRadius;
23433
- if (multipleSelect) {
23434
- borderRadius = 3;
23435
- } else {
23436
- borderRadius = CHOICE_ICON_SIZE;
23437
- }
23438
- return {
23439
- backgroundColor,
23440
- borderColor,
23441
- color,
23442
- borderRadius
23443
- };
23444
- }
23445
23559
  const ChoiceIcon = function (props) {
23446
23560
  const {
23447
23561
  checked,
@@ -23453,9 +23567,41 @@ const ChoiceIcon = function (props) {
23453
23567
  multipleSelect,
23454
23568
  pos,
23455
23569
  previouslyAnswered,
23570
+ // Used in "review mode"/"show rationale" to show that option was previously chosen
23456
23571
  pressed
23457
23572
  } = props;
23458
- const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
23573
+
23574
+ // Accounts for incorrect choice still displaying as learner tries again
23575
+ const choiceIsChecked = checked || showCorrectness && !correct && previouslyAnswered;
23576
+
23577
+ // Core styling
23578
+ 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];
23579
+
23580
+ // Color styling
23581
+ // Handle dynamic styling of the multiple choice icon. Most
23582
+ // MC icon styles are constant, but we do allow the caller
23583
+ // to specify the selected color, and thus must control styles
23584
+ // related to the selected state dynamically.
23585
+ let crossOutColor;
23586
+ if (showCorrectness && correct && checked) {
23587
+ choiceStyling.push(styles$r.choiceCorrect);
23588
+ crossOutColor = wonderBlocksTokens.color.green;
23589
+ } else if (showCorrectness && !correct && (checked || previouslyAnswered)) {
23590
+ choiceStyling.push(styles$r.choiceIncorrect);
23591
+ crossOutColor = wonderBlocksTokens.color.red;
23592
+ } else if (checked) {
23593
+ // Show filled neutral blue color (showCorrectness is false)
23594
+ choiceStyling.push(styles$r.choiceNeutral);
23595
+ crossOutColor = wonderBlocksTokens.color.blue;
23596
+ } else if (pressed) {
23597
+ // Show outlined neutral blue color (showCorrectness is false)
23598
+ choiceStyling.push(styles$r.activeNeutral);
23599
+ crossOutColor = wonderBlocksTokens.color.blue;
23600
+ } else {
23601
+ // choice is not checked
23602
+ choiceStyling.push(styles$r.uncheckedColors);
23603
+ crossOutColor = wonderBlocksTokens.color.offBlack64;
23604
+ }
23459
23605
  return /*#__PURE__*/React__namespace.createElement("div", {
23460
23606
  className: aphrodite.css(styles$t.iconWrapper)
23461
23607
  }, /*#__PURE__*/React__namespace.createElement(FocusRing, {
@@ -23463,9 +23609,8 @@ const ChoiceIcon = function (props) {
23463
23609
  visible: focused || hovered,
23464
23610
  multipleSelect: multipleSelect
23465
23611
  }, /*#__PURE__*/React__namespace.createElement("div", {
23466
- style: dynamicStyles,
23467
23612
  "data-testid": "choice-icon__library-choice-icon",
23468
- className: aphrodite.css(styles$r.circle, showCorrectness && correct && styles$r.circleCorrect, showCorrectness && !correct && styles$r.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$r.circleIncorrectAnswered)
23613
+ className: aphrodite.css(...choiceStyling)
23469
23614
  // used in BaseRadio editMode to check
23470
23615
  // if we actually clicked on the radio icon
23471
23616
  ,
@@ -23477,7 +23622,7 @@ const ChoiceIcon = function (props) {
23477
23622
  showCorrectness: showCorrectness,
23478
23623
  correct: correct
23479
23624
  })))), crossedOut && /*#__PURE__*/React__namespace.createElement(CrossOutLine, {
23480
- color: dynamicStyles.borderColor
23625
+ color: crossOutColor
23481
23626
  }));
23482
23627
  };
23483
23628
  const styles$r = aphrodite.StyleSheet.create({
@@ -23489,8 +23634,7 @@ const styles$r = aphrodite.StyleSheet.create({
23489
23634
  alignItems: "center",
23490
23635
  justifyContent: "center"
23491
23636
  },
23492
- circle: {
23493
- // Make the circle
23637
+ choiceBase: {
23494
23638
  width: CHOICE_ICON_SIZE,
23495
23639
  height: CHOICE_ICON_SIZE,
23496
23640
  boxSizing: "border-box",
@@ -23502,30 +23646,48 @@ const styles$r = aphrodite.StyleSheet.create({
23502
23646
  // "bold font family" so that characters which fall back to the default
23503
23647
  // font get bolded too.
23504
23648
  fontWeight: "bold",
23505
- fontSize: 12,
23506
23649
  // Center the icon wrapper.
23507
23650
  display: "flex",
23508
23651
  alignItems: "center",
23509
- justifyContent: "center",
23510
- // HACK(emily): I don't know why adding this line height makes the text
23511
- // appear centered better than any other value, but it does. In
23512
- // particular, at large zoom levels this line height does almost
23513
- // nothing, but at the default size this shifts the letter down one
23514
- // pixel so it is much better centered.
23515
- lineHeight: "1px"
23516
- },
23517
- circleCorrect: {
23652
+ justifyContent: "center"
23653
+ },
23654
+ choiceHasLetter: {
23655
+ fontSize: 12
23656
+ },
23657
+ choiceHasIcon: {
23518
23658
  fontSize: CHOICE_ICON_SIZE
23519
23659
  },
23520
- circleIncorrect: {
23521
- fontSize: CHOICE_ICON_SIZE,
23522
- borderColor: gray68,
23523
- color: gray68
23660
+ choiceIsChecked: {
23661
+ color: wonderBlocksTokens.color.white
23662
+ },
23663
+ choiceIsUnchecked: {
23664
+ color: wonderBlocksTokens.color.offBlack64
23665
+ },
23666
+ choiceCorrect: {
23667
+ backgroundColor: wonderBlocksTokens.color.green,
23668
+ borderColor: wonderBlocksTokens.color.green
23524
23669
  },
23525
- circleIncorrectAnswered: {
23670
+ choiceIncorrect: {
23526
23671
  backgroundColor: wonderBlocksTokens.color.red,
23527
- borderColor: wonderBlocksTokens.color.red,
23528
- color: wonderBlocksTokens.color.white
23672
+ borderColor: wonderBlocksTokens.color.red
23673
+ },
23674
+ choiceNeutral: {
23675
+ backgroundColor: wonderBlocksTokens.color.blue,
23676
+ borderColor: wonderBlocksTokens.color.blue
23677
+ },
23678
+ activeNeutral: {
23679
+ color: wonderBlocksTokens.color.blue,
23680
+ borderColor: wonderBlocksTokens.color.blue,
23681
+ backgroundColor: "transparent"
23682
+ },
23683
+ multiSelectShape: {
23684
+ borderRadius: 3
23685
+ },
23686
+ singleSelectShape: {
23687
+ borderRadius: CHOICE_ICON_SIZE
23688
+ },
23689
+ uncheckedColors: {
23690
+ borderColor: wonderBlocksTokens.color.offBlack64
23529
23691
  }
23530
23692
  });
23531
23693
 
@@ -24120,15 +24282,19 @@ const BaseRadio = function (_ref) {
24120
24282
  // content somehow? Would changing our choice of key
24121
24283
  // somehow break something happening inside a choice's
24122
24284
  // child Renderers, by changing when we mount/unmount?
24123
- return /*#__PURE__*/React__namespace.createElement("li", {
24124
- key: i,
24125
- ref: e => listElem = e,
24126
- className: className,
24127
- onClick: clickHandler,
24128
- onTouchStart: labelWrap ? undefined : captureScratchpadTouchStart$1
24129
- }, /*#__PURE__*/React__namespace.createElement(Element, _extends({}, elementProps, {
24130
- ref: ref
24131
- })));
24285
+ return (
24286
+ /*#__PURE__*/
24287
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
24288
+ React__namespace.createElement("li", {
24289
+ key: i,
24290
+ ref: e => listElem = e,
24291
+ className: className,
24292
+ onClick: clickHandler,
24293
+ onTouchStart: labelWrap ? undefined : captureScratchpadTouchStart$1
24294
+ }, /*#__PURE__*/React__namespace.createElement(Element, _extends({}, elementProps, {
24295
+ ref: ref
24296
+ })))
24297
+ );
24132
24298
  })));
24133
24299
 
24134
24300
  // Allow for horizontal scrolling if content is too wide, which may be
@@ -24962,8 +25128,7 @@ var Categorizer$1 = {
24962
25128
  staticTransform: editorProps => {
24963
25129
  return ___default["default"].pick(editorProps, "items", "categories", "values", "randomizeItems");
24964
25130
  },
24965
- isLintable: true,
24966
- getPublicWidgetOptions: perseusCore.getCategorizerPublicWidgetOptions
25131
+ isLintable: true
24967
25132
  };
24968
25133
 
24969
25134
  /**
@@ -25173,8 +25338,7 @@ var CSProgram$1 = {
25173
25338
  name: "cs-program",
25174
25339
  displayName: "CS Program",
25175
25340
  widget: CSProgram,
25176
- hidden: true,
25177
- getPublicWidgetOptions: perseusCore.getCSProgramPublicWidgetOptions
25341
+ hidden: true
25178
25342
  };
25179
25343
 
25180
25344
  const getPromptJSON$l = renderProps => {
@@ -25390,8 +25554,7 @@ var Dropdown$1 = {
25390
25554
  displayName: "Drop down",
25391
25555
  accessible: true,
25392
25556
  widget: Dropdown,
25393
- transform: optionsTransform,
25394
- getPublicWidgetOptions: perseusCore.getDropdownPublicWidgetOptions
25557
+ transform: optionsTransform
25395
25558
  };
25396
25559
 
25397
25560
  const getPromptJSON$j = renderProps => {
@@ -27177,12 +27340,16 @@ class Iframe extends React__namespace.Component {
27177
27340
  // that we need. This makes it a bit safer in case some content
27178
27341
  // creator "went wild".
27179
27342
  // http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
27180
- return /*#__PURE__*/React__namespace.createElement("iframe", {
27181
- sandbox: sandboxProperties,
27182
- style: style,
27183
- src: url,
27184
- allowFullScreen: this.props.allowFullScreen
27185
- });
27343
+ return (
27344
+ /*#__PURE__*/
27345
+ // eslint-disable-next-line jsx-a11y/iframe-has-title -- TODO(LEMS-2871): Address a11y error
27346
+ React__namespace.createElement("iframe", {
27347
+ sandbox: sandboxProperties,
27348
+ style: style,
27349
+ src: url,
27350
+ allowFullScreen: this.props.allowFullScreen
27351
+ })
27352
+ );
27186
27353
  }
27187
27354
  }
27188
27355
  var Iframe$1 = {
@@ -27207,7 +27374,7 @@ const getPromptJSON$d = renderProps => {
27207
27374
 
27208
27375
  const defaultBoxSize = 400;
27209
27376
  const defaultRange = [0, 10];
27210
- const defaultBackgroundImage$2 = {
27377
+ const defaultBackgroundImage$1 = {
27211
27378
  url: null,
27212
27379
  width: 0,
27213
27380
  height: 0
@@ -27222,7 +27389,7 @@ class ImageWidget extends React__namespace.Component {
27222
27389
  title: "",
27223
27390
  range: [defaultRange, defaultRange],
27224
27391
  box: [defaultBoxSize, defaultBoxSize],
27225
- backgroundImage: defaultBackgroundImage$2,
27392
+ backgroundImage: defaultBackgroundImage$1,
27226
27393
  labels: [],
27227
27394
  alt: "",
27228
27395
  caption: "",
@@ -27993,306 +28160,6 @@ var Interactive = {
27993
28160
  hidden: true
27994
28161
  };
27995
28162
 
27996
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
27997
- const defaultBackgroundImage$1 = {
27998
- url: null
27999
- };
28000
-
28001
- /* Style objects */
28002
- const defaultInstructionsStyle = {
28003
- fontStyle: "italic",
28004
- fontWeight: "bold",
28005
- fontSize: "32px",
28006
- width: "100%",
28007
- height: "100%",
28008
- textAlign: "center",
28009
- backgroundColor: "white",
28010
- position: "absolute",
28011
- zIndex: 1,
28012
- transition: "opacity .25s ease-in-out"
28013
- };
28014
- const instructionsTextStyle = {
28015
- position: "relative",
28016
- top: "25%"
28017
- };
28018
- function numSteps$1(range, step) {
28019
- return Math.floor((range[1] - range[0]) / step);
28020
- }
28021
- class Graph extends React__namespace.Component {
28022
- graphieDivRef = /*#__PURE__*/React__namespace.createRef();
28023
- protractor;
28024
- ruler;
28025
- _graphie;
28026
- // @ts-expect-error - TS2564 - Property '_hasSetupGraphieThisUpdate' has no initializer and is not definitely assigned in the constructor.
28027
- _hasSetupGraphieThisUpdate;
28028
- // @ts-expect-error - TS2564 - Property '_shouldSetupGraphie' has no initializer and is not definitely assigned in the constructor.
28029
- _shouldSetupGraphie;
28030
- static defaultProps = {
28031
- labels: ["x", "y"],
28032
- range: [[-10, 10], [-10, 10]],
28033
- step: [1, 1],
28034
- gridStep: [1, 1],
28035
- snapStep: [0.5, 0.5],
28036
- markings: "graph",
28037
- backgroundImage: defaultBackgroundImage$1,
28038
- showProtractor: false,
28039
- showRuler: false,
28040
- rulerLabel: "",
28041
- rulerTicks: 10,
28042
- isMobile: false
28043
- };
28044
- componentDidMount() {
28045
- this._setupGraphie(true);
28046
- }
28047
- UNSAFE_componentWillReceiveProps(nextProps) {
28048
- const potentialChanges = ["labels", "range", "step", "markings", "showProtractor", "showRuler", "rulerLabel", "rulerTicks", "gridStep", "snapStep"];
28049
- const self = this;
28050
- ___default["default"].each(potentialChanges, function (prop) {
28051
- if (!___default["default"].isEqual(self.props[prop], nextProps[prop])) {
28052
- self._shouldSetupGraphie = true;
28053
- }
28054
- });
28055
- }
28056
- componentDidUpdate() {
28057
- // Only setupGraphie once per componentDidUpdate().
28058
- // See explanation in setupGraphie().
28059
- this._hasSetupGraphieThisUpdate = false;
28060
- if (this._shouldSetupGraphie) {
28061
- this._setupGraphie(false);
28062
- this._shouldSetupGraphie = false;
28063
- }
28064
- }
28065
-
28066
- /* Reset the graphie canvas to its initial state
28067
- *
28068
- * Use when re-rendering the parent component and you need a blank
28069
- * graphie.
28070
- */
28071
- reset = () => {
28072
- this._setupGraphie(false);
28073
- };
28074
- graphie = () => {
28075
- return this._graphie;
28076
- };
28077
- pointsFromNormalized = (coordsList, noSnap) => {
28078
- const self = this;
28079
- return ___default["default"].map(coordsList, function (coords) {
28080
- return ___default["default"].map(coords, function (coord, i) {
28081
- const range = self.props.range[i];
28082
- if (noSnap) {
28083
- return range[0] + (range[1] - range[0]) * coord;
28084
- }
28085
- const step = self.props.step[i];
28086
- const nSteps = numSteps$1(range, step);
28087
- const tick = Math.round(coord * nSteps);
28088
- return range[0] + step * tick;
28089
- });
28090
- });
28091
- };
28092
- _setupGraphie = initialMount => {
28093
- // Only setupGraphie once per componentDidUpdate().
28094
- // This prevents this component from rendering graphie
28095
- // and then immediately re-render graphie because its
28096
- // parent component asked it to. This will happen when
28097
- // props on the parent and props on this component both
28098
- // require graphie to be re-rendered.
28099
- if (this._hasSetupGraphieThisUpdate) {
28100
- return;
28101
- }
28102
- const graphieDiv = this.graphieDivRef.current;
28103
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'.
28104
- $__default["default"](graphieDiv).empty();
28105
-
28106
- // Content creators may need to explicitly add the dollar signs so the
28107
- // strings are picked up by our translation tools. However, these math
28108
- // annotations are redundant because we already render all graph labels
28109
- // in math mode. For example, a label value of `$\text{Dollars}$` will
28110
- // be translatable, but we only want to pass the string
28111
- // `\text{Dollars}` to the Graph widget.
28112
- const labels = this.props.labels.map(label => Util.unescapeMathMode(label));
28113
- const range = this.props.range;
28114
- // @ts-expect-error - TS2345: Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLElement'.
28115
- const graphie = this._graphie = GraphUtils.createGraphie(graphieDiv);
28116
- const gridConfig = this._getGridConfig();
28117
- // @ts-expect-error - TS2339: Property 'snap' does not exist on type 'Graphie'.
28118
- graphie.snap = this.props.snapStep;
28119
- if (this.props.markings === "graph") {
28120
- graphie.graphInit({
28121
- range: range,
28122
- scale: kmath.vector.map(gridConfig, g => g.scale),
28123
- axisArrows: "<->",
28124
- labelFormat: function (s) {
28125
- return "\\small{" + s + "}";
28126
- },
28127
- gridStep: this.props.gridStep,
28128
- tickStep: kmath.vector.map(gridConfig, g => g.tickStep),
28129
- labelStep: 1,
28130
- unityLabels: kmath.vector.map(gridConfig, g => g.unityLabel),
28131
- isMobile: this.props.isMobile
28132
- });
28133
- graphie.label([0, range[1][1]], labels[1], this.props.isMobile ? "below right" : "above");
28134
- graphie.label([range[0][1], 0], labels[0], this.props.isMobile ? "above left" : "right");
28135
- } else if (this.props.markings === "grid") {
28136
- graphie.graphInit({
28137
- range: range,
28138
- scale: kmath.vector.map(gridConfig, g => g.scale),
28139
- gridStep: this.props.gridStep,
28140
- axes: false,
28141
- ticks: false,
28142
- labels: false,
28143
- isMobile: this.props.isMobile
28144
- });
28145
- } else if (this.props.markings === "none") {
28146
- graphie.init({
28147
- range: range,
28148
- scale: kmath.vector.map(gridConfig, g => g.scale),
28149
- isMobile: this.props.isMobile
28150
- });
28151
- }
28152
-
28153
- // Add instructions just before mouse layer
28154
- const visible = 0.5;
28155
- const invisible = 0.0;
28156
- let $instructionsWrapper;
28157
- if (this.props.instructions) {
28158
- $instructionsWrapper = $__default["default"]("<div/>");
28159
- ___default["default"].each(defaultInstructionsStyle, function (value, key) {
28160
- $instructionsWrapper.css(key, value);
28161
- });
28162
- $instructionsWrapper.css("opacity", visible);
28163
- const $instructions = $__default["default"]("<span/>", {
28164
- text: this.props.instructions
28165
- });
28166
- ___default["default"].each(instructionsTextStyle, function (value, key) {
28167
- $instructions.css(key, value);
28168
- });
28169
- $instructionsWrapper.append($instructions);
28170
- // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'append' does not exist on type 'JQueryStatic'.
28171
- $__default["default"](graphieDiv).append($instructionsWrapper);
28172
- } else {
28173
- $instructionsWrapper = undefined;
28174
- }
28175
-
28176
- // Add some handlers for instructions text (if necessary)
28177
- const onMouseDown = $instructionsWrapper || this.props.onMouseDown ? ___default["default"].bind(function (coord) {
28178
- if ($instructionsWrapper) {
28179
- $instructionsWrapper.remove();
28180
- $instructionsWrapper = null;
28181
- }
28182
- // eslint-disable-next-line @babel/no-invalid-this
28183
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
28184
- // eslint-disable-next-line @babel/no-invalid-this
28185
- this.props.onMouseDown(coord);
28186
- }, this) : undefined;
28187
- const onMouseOver = $instructionsWrapper ? function () {
28188
- $instructionsWrapper?.css("opacity", invisible);
28189
- } : undefined;
28190
- const onMouseOut = $instructionsWrapper ? function () {
28191
- $instructionsWrapper?.css("opacity", visible);
28192
- } : undefined;
28193
- /* eslint-enable indent */
28194
-
28195
- graphie.addMouseLayer({
28196
- onClick: this.props.onClick,
28197
- onMouseDown: onMouseDown,
28198
- onMouseOver: onMouseOver,
28199
- onMouseOut: onMouseOut,
28200
- onMouseMove: this.props.onMouseMove,
28201
- allowScratchpad: true,
28202
- setDrawingAreaAvailable: this.props.setDrawingAreaAvailable
28203
- });
28204
- this._updateProtractor();
28205
- this._updateRuler();
28206
-
28207
- // We set this flag before jumping into our callback
28208
- // to avoid recursing if our callback calls reset() itself
28209
- this._hasSetupGraphieThisUpdate = true;
28210
- if (!initialMount && this.props.onGraphieUpdated) {
28211
- // Calling a parent callback in componentDidMount is bad and
28212
- // results in hard-to-reason-about lifecycle problems (esp. with
28213
- // refs), so we do it only on update and rely on the parent to
28214
- // query for the graphie object on initial mount
28215
- this.props.onGraphieUpdated(graphie);
28216
- }
28217
- };
28218
- _getGridConfig = () => {
28219
- return kmath.vector.map(this.props.step, (step, i) => Util.gridDimensionConfig(step, this.props.range[i], this.props.box[i], this.props.gridStep[i]));
28220
- };
28221
- _updateProtractor = () => {
28222
- if (this.protractor) {
28223
- this.protractor.remove();
28224
- }
28225
- if (this.props.showProtractor) {
28226
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
28227
- const coord = this.pointsFromNormalized([[0.5, 0.05]])[0];
28228
- this.protractor = this._graphie.protractor(coord);
28229
- }
28230
- };
28231
- _updateRuler = () => {
28232
- if (this.ruler) {
28233
- this.ruler.remove();
28234
- }
28235
- if (this.props.showRuler) {
28236
- // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
28237
- const coord = this.pointsFromNormalized([[0.5, 0.25]])[0];
28238
- const extent = this._graphie.range[0][1] - this._graphie.range[0][0];
28239
- this.ruler = this._graphie.ruler({
28240
- center: coord,
28241
- label: this.props.rulerLabel,
28242
- pixelsPerUnit: this._graphie.scale[0],
28243
- ticksPerUnit: this.props.rulerTicks,
28244
- units: Math.round(0.8 * extent)
28245
- });
28246
- }
28247
- };
28248
- toJSON = () => {
28249
- return ___default["default"].pick(this.props, "range", "step", "markings", "labels", "backgroundImage", "showProtractor", "showRuler", "rulerLabel", "rulerTicks", "gridStep", "snapStep");
28250
- };
28251
- render() {
28252
- let image;
28253
- const imageData = this.props.backgroundImage;
28254
- if (imageData.url) {
28255
- const scale = this.props.box[0] / interactiveSizes$1.defaultBoxSize;
28256
- image = /*#__PURE__*/React__namespace.createElement(context$1.Consumer, null, _ref => {
28257
- let {
28258
- setAssetStatus
28259
- } = _ref;
28260
- return /*#__PURE__*/React__namespace.createElement(SvgImage
28261
- // @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">'.
28262
- , {
28263
- src: imageData.url,
28264
- width: imageData.width,
28265
- height: imageData.height,
28266
- scale: scale,
28267
- responsive: false,
28268
- setAssetStatus: setAssetStatus
28269
- });
28270
- });
28271
- } else {
28272
- image = null;
28273
- }
28274
- return /*#__PURE__*/React__namespace.createElement("div", {
28275
- className: "graphie-container blank-background",
28276
- style: {
28277
- width: this.props.box[0],
28278
- height: this.props.box[1]
28279
- }
28280
- // @ts-expect-error - TS2339 - Property 'onMouseOut' does not exist on type 'Graph'.
28281
- ,
28282
- onMouseOut: this.onMouseOut
28283
- // @ts-expect-error - TS2339 - Property 'onMouseOver' does not exist on type 'Graph'.
28284
- ,
28285
- onMouseOver: this.onMouseOver
28286
- // @ts-expect-error - TS2339 - Property 'onClick' does not exist on type 'Graph'.
28287
- ,
28288
- onClick: this.onClick
28289
- }, image, /*#__PURE__*/React__namespace.createElement("div", {
28290
- className: "graphie",
28291
- ref: this.graphieDivRef
28292
- }));
28293
- }
28294
- }
28295
-
28296
28163
  const getPromptJSON$b = (props, userInput) => {
28297
28164
  return {
28298
28165
  type: "interactive-graph",
@@ -28857,13 +28724,19 @@ function escapeIfUnescaped(character) {
28857
28724
  }
28858
28725
  }
28859
28726
 
28860
- function AxisLabels() {
28727
+ function AxisLabels(_ref) {
28728
+ let {
28729
+ i18n
28730
+ } = _ref;
28861
28731
  const {
28862
28732
  range,
28863
28733
  labels,
28864
28734
  width,
28865
28735
  height
28866
28736
  } = useGraphConfig();
28737
+ const {
28738
+ strings
28739
+ } = i18n;
28867
28740
  const yAxisLabelLocation = [0, range[Y][MAX]];
28868
28741
  const xAxisLabelLocation = [range[X][MAX], 0];
28869
28742
  const [xAxisLabelText, yAxisLabelText] = labels;
@@ -28881,6 +28754,7 @@ function AxisLabels() {
28881
28754
  TeX
28882
28755
  } = getDependencies();
28883
28756
  return /*#__PURE__*/React__default["default"].createElement(React__default["default"].Fragment, null, /*#__PURE__*/React__default["default"].createElement("span", {
28757
+ "aria-label": strings.xAxis,
28884
28758
  style: {
28885
28759
  position: "absolute",
28886
28760
  left: x1,
@@ -28889,6 +28763,7 @@ function AxisLabels() {
28889
28763
  transform: "translate(7px, -50%)"
28890
28764
  }
28891
28765
  }, /*#__PURE__*/React__default["default"].createElement(TeX, null, replaceOutsideTeX(xAxisLabelText))), /*#__PURE__*/React__default["default"].createElement("span", {
28766
+ "aria-label": strings.yAxis,
28892
28767
  style: {
28893
28768
  position: "absolute",
28894
28769
  left: x2,
@@ -29697,7 +29572,6 @@ class RangeInput extends React__namespace.Component {
29697
29572
  var components = /*#__PURE__*/Object.freeze({
29698
29573
  __proto__: null,
29699
29574
  ButtonGroup: ButtonGroup,
29700
- Graph: Graph,
29701
29575
  Graphie: Graphie,
29702
29576
  HUD: HUD,
29703
29577
  Icon: Icon,
@@ -29841,9 +29715,9 @@ const LockedFunction = props => {
29841
29715
  const {
29842
29716
  color,
29843
29717
  strokeStyle,
29844
- directionalAxis,
29845
- domain
29718
+ directionalAxis
29846
29719
  } = props;
29720
+ const domain = props.domain ? [Number.isFinite(props.domain[0]) ? props.domain[0] : -Infinity, Number.isFinite(props.domain[1]) ? props.domain[1] : Infinity] : undefined;
29847
29721
  const plotProps = {
29848
29722
  color: perseusCore.lockedFigureColors[color],
29849
29723
  style: strokeStyle,
@@ -29922,14 +29796,90 @@ function Vector(props) {
29922
29796
  }));
29923
29797
  }
29924
29798
 
29925
- function srFormatNumber(a, locale) {
29799
+ function srFormatNumber(a, locale, maximumFractionDigits) {
29800
+ const piBasedNumber = getPiMultiple(a);
29801
+ if (piBasedNumber) {
29802
+ return piBasedNumber;
29803
+ }
29804
+
29926
29805
  // adding zero here converts negative zero to positive zero.
29927
29806
  return (0 + a).toLocaleString(locale, {
29928
- maximumFractionDigits: 3,
29807
+ maximumFractionDigits: maximumFractionDigits ?? 3,
29929
29808
  useGrouping: false // no thousands separators
29930
29809
  });
29931
29810
  }
29932
29811
 
29812
+ // Exported for testing
29813
+ function getPiMultiple(a) {
29814
+ // - Save some calculations by checking if this is an integer first.
29815
+ // If a is an integer, it's definitely not a multiple of π.
29816
+ // - 0 should not be presented as a multiple of π.
29817
+ // - If a is greater than 1e12, then it thinks that it's always a
29818
+ // multiple of π even when it's not, due to rounding errors.
29819
+ if (Number.isInteger(a) || a === 0 || a > 1e12) {
29820
+ return null;
29821
+ }
29822
+
29823
+ // Figure out the coefficient before the π.
29824
+ // Example: If a = π/2, then piCoefficient = 0.5
29825
+ // Example: If a = 2π, then piCoefficient = 2
29826
+ const piCoefficient = a / Math.PI;
29827
+ // Truncate the coefficient to account for floating point errors.
29828
+ // π calculations are only accurate up to precision of 1e-12.
29829
+ const truncatedCoefficient = parseFloat(piCoefficient.toFixed(12));
29830
+
29831
+ // If the coefficient is already an integer, then the number
29832
+ // is a multiple of π. Return it here.
29833
+ // Example: If a = 2π, then truncatedCoefficient = 2, so return "2π"
29834
+ if (Number.isInteger(truncatedCoefficient)) {
29835
+ // Return "π" rather than "1π".
29836
+ if (truncatedCoefficient === 1) {
29837
+ return "π";
29838
+ }
29839
+
29840
+ // Return "-π" rather than "-1π".
29841
+ if (truncatedCoefficient === -1) {
29842
+ return "-π";
29843
+ }
29844
+ return truncatedCoefficient + "π";
29845
+ }
29846
+
29847
+ // If the coefficient is not an integer, then we need to
29848
+ // check if it's a multiple of π/2, π/3, π/4, or π/6.
29849
+ // These are π-based values on a unit circle.
29850
+ const acceptableDivisors = [2, 3, 4, 6];
29851
+
29852
+ // Loop through the acceptable divisors and check if the
29853
+ // coefficient is a multiple of (1/divisor).
29854
+ // Example: If a = 5π/6, the coeff is 5/6, which is a multiple of 1/6.
29855
+ // So we return "5π/6".
29856
+ for (const divisor of acceptableDivisors) {
29857
+ // Check if the coefficient is a multiple of (1/divisor) by
29858
+ // multiplying the coefficient by the divisor and checking if
29859
+ // the result is an integer.
29860
+ // Example: If a = 5π/6, then piCoefficient = 5/6. We multiply
29861
+ // by 6 (divisor) to get 5, which is an integer.
29862
+ const coefficientNumerator = parseFloat(
29863
+ // π calculations are only accurate up to precision of 1e-12.
29864
+ (piCoefficient * divisor).toFixed(12));
29865
+ if (Number.isInteger(coefficientNumerator)) {
29866
+ // Handle the case where the coefficient numberator is just 1.
29867
+ // We don't want to write "π/6" as "1π/6"
29868
+ if (coefficientNumerator === 1) {
29869
+ return "π/" + divisor;
29870
+ }
29871
+
29872
+ // Handle the case where the coefficient numberator is just -1.
29873
+ // We don't want to write "π/6" as "-1π/6"
29874
+ if (coefficientNumerator === -1) {
29875
+ return "-π/" + divisor;
29876
+ }
29877
+ return coefficientNumerator + "π/" + divisor;
29878
+ }
29879
+ }
29880
+ return null;
29881
+ }
29882
+
29933
29883
  /**
29934
29884
  * Given a ray and a rectangular box, find the point where the ray intersects
29935
29885
  * the edge of the box. Assumes the `initialPoint` is inside the box.
@@ -30104,6 +30054,95 @@ a, b, c) {
30104
30054
  return [x1, x2];
30105
30055
  }
30106
30056
 
30057
+ /**
30058
+ * Given a list of points, return the angle made by the point at index i
30059
+ * with the points at indices i - 1 and i + 1.
30060
+ */
30061
+ function getAngleFromPoints(points, i) {
30062
+ // Make sure the index is valid.
30063
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30064
+ return null;
30065
+ }
30066
+
30067
+ // If there are fewer than 3 points, there is no angle.
30068
+ if (points.length < 3) {
30069
+ return null;
30070
+ }
30071
+
30072
+ // Get the points at the current index and the two adjacent points.
30073
+ const point = points.at(i);
30074
+ const pt1 = points.at(i - 1);
30075
+ const pt2 = points[(i + 1) % points.length];
30076
+ if (!point || !pt1 || !pt2) {
30077
+ return null;
30078
+ }
30079
+
30080
+ // Get the lengths of adjacent sides, making a triangle
30081
+ // we can use to calculate the angle.
30082
+ const a = mafs.vec.dist(point, pt1);
30083
+ const b = mafs.vec.dist(point, pt2);
30084
+ const c = mafs.vec.dist(pt1, pt2);
30085
+
30086
+ // Law of cosines
30087
+ const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
30088
+ return angle;
30089
+ }
30090
+ function getSideLengthsFromPoints(points, i, isPolygonOpen) {
30091
+ if (i < 0 || i >= points.length || !Number.isInteger(i)) {
30092
+ return [];
30093
+ }
30094
+ if (points.length < 2) {
30095
+ return [];
30096
+ }
30097
+ const returnArray = [];
30098
+ const point = points[i];
30099
+ // If this point is the first point, then side 1 is the
30100
+ // last point in the list.
30101
+ const point1Index = i === 0 ? points.length - 1 : i - 1;
30102
+ const point1 = points[point1Index];
30103
+ // Make sure the previous point is not the same
30104
+ // as the current point.
30105
+ const side1 = i !== point1Index ? mafs.vec.dist(point, point1) : null;
30106
+ // The first side of the first point does not exist if
30107
+ // the polygon is open.
30108
+ if (side1 && !(isPolygonOpen && i === 0)) {
30109
+ returnArray.push({
30110
+ pointIndex: point1Index,
30111
+ sideLength: side1
30112
+ });
30113
+ }
30114
+
30115
+ // See if there is a side 2.
30116
+ const point2Index = (i + 1) % points.length;
30117
+ const point2 = points[point2Index];
30118
+ // Make sure that the next point is not the same as the
30119
+ // current point, and don't repeat the first point.
30120
+ const side2 = i !== point2Index && point2Index !== point1Index ? mafs.vec.dist(point, point2) : null;
30121
+ // The second side of the last point does not exist if
30122
+ // the polygon is open.
30123
+ if (side2 && !(isPolygonOpen && i === points.length - 1)) {
30124
+ returnArray.push({
30125
+ pointIndex: point2Index,
30126
+ sideLength: side2
30127
+ });
30128
+ }
30129
+ return returnArray;
30130
+ }
30131
+
30132
+ /**
30133
+ * Determine whether to determine the string with the
30134
+ * exact side length or the approximate side length.
30135
+ */
30136
+ function getPolygonSideString(sideLength, pointIndex, strings, locale) {
30137
+ return Number.isInteger(sideLength) ? strings.srPolygonSideLength({
30138
+ pointNum: pointIndex + 1,
30139
+ length: `${sideLength}`
30140
+ }) : strings.srPolygonSideLengthApprox({
30141
+ pointNum: pointIndex + 1,
30142
+ length: srFormatNumber(sideLength, locale, 1)
30143
+ });
30144
+ }
30145
+
30107
30146
  const {
30108
30147
  calculateAngleInDegrees: calculateAngleInDegrees$2
30109
30148
  } = kmath.angles;
@@ -30551,7 +30590,7 @@ const SvgDefs = () => /*#__PURE__*/React__default["default"].createElement("defs
30551
30590
  })));
30552
30591
 
30553
30592
  const {
30554
- clockwise: clockwise$1
30593
+ clockwise
30555
30594
  } = kmath.geometry;
30556
30595
  const {
30557
30596
  getAngleFromVertex: getAngleFromVertex$1
@@ -30566,7 +30605,7 @@ const PolygonAngle = _ref => {
30566
30605
  snapTo
30567
30606
  } = _ref;
30568
30607
  const [centerX, centerY] = centerPoint;
30569
- const areClockwise = clockwise$1([centerPoint, ...endPoints]);
30608
+ const areClockwise = clockwise([centerPoint, ...endPoints]);
30570
30609
  const [[startX, startY], [endX, endY]] = areClockwise ? endPoints : endPoints.reverse();
30571
30610
  const radius = 0.3;
30572
30611
  const a = mafs.vec.dist(centerPoint, endPoints[0]);
@@ -30639,7 +30678,7 @@ const Angle = _ref2 => {
30639
30678
  range
30640
30679
  } = _ref2;
30641
30680
  // Check if the points are clockwise or not, depending on whether we allow reflex angles
30642
- const areClockwise = clockwise$1([...coords, vertex]);
30681
+ const areClockwise = clockwise([...coords, vertex]);
30643
30682
  const shouldReverseCoords = areClockwise && !allowReflexAngles;
30644
30683
 
30645
30684
  // Reverse the coordinates accordingly to ensure the angle is calculated correctly
@@ -31008,6 +31047,34 @@ function range(min, max) {
31008
31047
  return result;
31009
31048
  }
31010
31049
 
31050
+ function Hairlines(props) {
31051
+ const {
31052
+ point
31053
+ } = props;
31054
+ const {
31055
+ range
31056
+ } = useGraphConfig();
31057
+ const [[xMin, xMax], [yMin, yMax]] = range;
31058
+ const [[x, y]] = useTransformVectorsToPixels(point);
31059
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
31060
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
31061
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
31062
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
31063
+ return /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
31064
+ x1: verticalStartX,
31065
+ y1: y,
31066
+ x2: verticalEndX,
31067
+ y2: y,
31068
+ stroke: wonderBlocksTokens.color.blue
31069
+ }), /*#__PURE__*/React__namespace.createElement("line", {
31070
+ x1: x,
31071
+ y1: horizontalStartY,
31072
+ x2: x,
31073
+ y2: horizontalEndY,
31074
+ stroke: wonderBlocksTokens.color.blue
31075
+ }));
31076
+ }
31077
+
31011
31078
  // The hitbox size of 48px by 48px is preserved from the legacy interactive
31012
31079
  // graph.
31013
31080
  const hitboxSizePx = 48;
@@ -31017,7 +31084,6 @@ const hitboxSizePx = 48;
31017
31084
  // on an interactive graph.
31018
31085
  const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
31019
31086
  const {
31020
- range,
31021
31087
  markings,
31022
31088
  showTooltips
31023
31089
  } = useGraphConfig();
@@ -31025,6 +31091,7 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
31025
31091
  point,
31026
31092
  color = wonderBlocksTokens.color.blue,
31027
31093
  dragging,
31094
+ focused,
31028
31095
  cursor,
31029
31096
  showFocusRing,
31030
31097
  onClick = () => {}
@@ -31038,25 +31105,7 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
31038
31105
  })?.[0] ?? "blue";
31039
31106
  const pointClasses = classNames("movable-point", dragging && "movable-point--dragging", showFocusRing && "movable-point--focus");
31040
31107
  const [[x, y]] = useTransformVectorsToPixels(point);
31041
- const [[xMin, xMax], [yMin, yMax]] = range;
31042
- const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
31043
- const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
31044
- const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
31045
- const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
31046
- const showHairlines = dragging && markings !== "none";
31047
- const hairlines = /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
31048
- x1: verticalStartX,
31049
- y1: y,
31050
- x2: verticalEndX,
31051
- y2: y,
31052
- stroke: color
31053
- }), /*#__PURE__*/React__namespace.createElement("line", {
31054
- x1: x,
31055
- y1: horizontalStartY,
31056
- x2: x,
31057
- y2: horizontalEndY,
31058
- stroke: color
31059
- }));
31108
+ const showHairlines = (dragging || focused) && markings !== "none";
31060
31109
  const svgForPoint = /*#__PURE__*/React__namespace.createElement("g", {
31061
31110
  ref: hitboxRef,
31062
31111
  className: pointClasses,
@@ -31092,7 +31141,9 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
31092
31141
  },
31093
31142
  "data-testid": "movable-point__center"
31094
31143
  }));
31095
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
31144
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && /*#__PURE__*/React__namespace.createElement(Hairlines, {
31145
+ point: point
31146
+ }), showTooltips ? /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
31096
31147
  autoUpdate: true,
31097
31148
  backgroundColor: wbColorName,
31098
31149
  content: `(${point[X]}, ${point[Y]})`,
@@ -31188,6 +31239,7 @@ function useControlPoint(params) {
31188
31239
  },
31189
31240
  point: point,
31190
31241
  dragging: dragging,
31242
+ focused: focused,
31191
31243
  color: color,
31192
31244
  ref: visiblePointRef,
31193
31245
  showFocusRing: focused
@@ -31405,13 +31457,13 @@ const {
31405
31457
  getClockwiseAngle: getClockwiseAngle$2,
31406
31458
  polar: polar$1
31407
31459
  } = kmath.angles;
31408
- function renderAngleGraph(state, dispatch) {
31460
+ function renderAngleGraph(state, dispatch, i18n) {
31409
31461
  return {
31410
31462
  graph: /*#__PURE__*/React__namespace.createElement(AngleGraph, {
31411
31463
  graphState: state,
31412
31464
  dispatch: dispatch
31413
31465
  }),
31414
- interactiveElementsDescription: null
31466
+ interactiveElementsDescription: getAngleGraphDescription(state, i18n)
31415
31467
  };
31416
31468
  }
31417
31469
  function AngleGraph(props) {
@@ -31422,6 +31474,9 @@ function AngleGraph(props) {
31422
31474
  const {
31423
31475
  graphDimensionsInPixels
31424
31476
  } = useGraphConfig();
31477
+ const i18n = usePerseusI18n();
31478
+ const id = React__namespace.useId();
31479
+ const descriptionId = id + "-description";
31425
31480
  const {
31426
31481
  coords,
31427
31482
  showAngles,
@@ -31471,80 +31526,106 @@ function AngleGraph(props) {
31471
31526
  showAngles: showAngles || false // Whether to show the angle or not
31472
31527
  };
31473
31528
 
31529
+ // Aria strings
31474
31530
  const {
31475
- strings,
31476
- locale
31477
- } = usePerseusI18n();
31478
- const angleMeasure = srFormatNumber(getClockwiseAngle$2([endPoints[0], centerPoint, endPoints[1]], allowReflexAngles), locale);
31479
- const wholeAngleAriaLabel = strings.srAngleGraphAriaLabel;
31480
- const wholeAngleDescription = strings.srAngleGraphAriaDescription({
31481
- angleMeasure,
31482
- vertexX: srFormatNumber(coords[1][X], locale),
31483
- vertexY: srFormatNumber(coords[1][Y], locale),
31484
- startingSideX: srFormatNumber(coords[2][X], locale),
31485
- startingSideY: srFormatNumber(coords[2][Y], locale),
31486
- endingSideX: srFormatNumber(coords[0][X], locale),
31487
- endingSideY: srFormatNumber(coords[0][Y], locale)
31488
- });
31489
- const formatCoordinates = (x, y) => ({
31490
- x: srFormatNumber(x, locale),
31491
- y: srFormatNumber(y, locale)
31492
- });
31493
- const formatSideAriaLabel = (side, newPoint) => {
31494
- const formattedCoordinates = formatCoordinates(newPoint[X], newPoint[Y]);
31495
- const label = side === "terminal" ? strings.srAngleSideAtCoordinates({
31496
- point: 1,
31497
- side: "terminal side",
31498
- ...formattedCoordinates
31499
- }) : strings.srAngleSideAtCoordinates({
31500
- point: 3,
31501
- side: "initial side",
31502
- ...formattedCoordinates
31503
- });
31504
- return label;
31505
- };
31506
- const formatVertexAriaLabel = newPoint => {
31507
- const formattedVertexCoordinates = formatCoordinates(newPoint[X], newPoint[Y]);
31508
- const label = showAngles ? strings.srAngleVertexAtCoordinatesWithAngleMeasure({
31509
- ...formattedVertexCoordinates,
31510
- angleMeasure: angleMeasure
31511
- }) : strings.srAngleSideAtCoordinates({
31512
- point: 2,
31513
- side: "vertex",
31514
- ...formattedVertexCoordinates
31515
- });
31516
- return label;
31517
- };
31518
- const initialSideAriaLabel = formatSideAriaLabel("initial", coords[2]);
31519
- const terminalSideAriaLabel = formatSideAriaLabel("terminal", coords[0]);
31520
- const vertexAriaLabel = formatVertexAriaLabel(coords[1]);
31531
+ srAngleGraphAriaLabel,
31532
+ srAngleGraphAriaDescription,
31533
+ srAngleStartingSide,
31534
+ srAngleEndingSide,
31535
+ srAngleVertex
31536
+ } = describeAngleGraph(graphState, i18n);
31521
31537
 
31522
31538
  // Render the lines, angle, and then movable points
31523
31539
  return /*#__PURE__*/React__namespace.createElement("g", {
31524
- "aria-label": wholeAngleAriaLabel,
31525
- "aria-describedby": "angle-description"
31540
+ "aria-label": srAngleGraphAriaLabel,
31541
+ "aria-describedby": descriptionId
31526
31542
  }, svgLines, /*#__PURE__*/React__namespace.createElement(Angle, angleParams), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
31527
31543
  point: coords[1],
31528
31544
  sequenceNumber: 1,
31529
31545
  constrain: p => p,
31530
31546
  onMove: destination => dispatch(actions.angle.movePoint(1, destination)),
31531
- ariaLabel: vertexAriaLabel
31547
+ ariaLabel: srAngleVertex
31532
31548
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
31533
31549
  point: coords[0],
31534
31550
  sequenceNumber: 2,
31535
31551
  constrain: getAngleSideConstraint(coords[0], coords[1], snapDegrees || 1),
31536
31552
  onMove: destination => dispatch(actions.angle.movePoint(0, destination)),
31537
- ariaLabel: terminalSideAriaLabel
31553
+ ariaLabel: srAngleEndingSide
31538
31554
  }), /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
31539
31555
  point: coords[2],
31540
31556
  sequenceNumber: 3,
31541
31557
  constrain: getAngleSideConstraint(coords[2], coords[1], snapDegrees || 1),
31542
31558
  onMove: destination => dispatch(actions.angle.movePoint(2, destination)),
31543
- ariaLabel: initialSideAriaLabel
31559
+ ariaLabel: srAngleStartingSide
31544
31560
  }), /*#__PURE__*/React__namespace.createElement("g", {
31545
- id: "angle-description",
31561
+ id: descriptionId,
31546
31562
  style: a11y.srOnly
31547
- }, wholeAngleDescription));
31563
+ }, srAngleGraphAriaDescription));
31564
+ }
31565
+ function getAngleGraphDescription(state, i18n) {
31566
+ const {
31567
+ strings,
31568
+ locale
31569
+ } = i18n;
31570
+ const {
31571
+ coords
31572
+ } = state;
31573
+ return strings.srInteractiveElements({
31574
+ elements: strings.srAngleInteractiveElements({
31575
+ vertexX: srFormatNumber(coords[1][X], locale),
31576
+ vertexY: srFormatNumber(coords[1][Y], locale),
31577
+ startingSideX: srFormatNumber(coords[2][X], locale),
31578
+ startingSideY: srFormatNumber(coords[2][Y], locale),
31579
+ endingSideX: srFormatNumber(coords[0][X], locale),
31580
+ endingSideY: srFormatNumber(coords[0][Y], locale)
31581
+ })
31582
+ });
31583
+ }
31584
+ function describeAngleGraph(state, i18n) {
31585
+ const {
31586
+ strings,
31587
+ locale
31588
+ } = i18n;
31589
+ const {
31590
+ coords,
31591
+ allowReflexAngles,
31592
+ showAngles
31593
+ } = state;
31594
+ const [endingSide, vertex, startingSide] = coords;
31595
+ const angleMeasure = srFormatNumber(getClockwiseAngle$2(coords, allowReflexAngles), locale);
31596
+ const srAngleGraphAriaLabel = strings.srAngleGraphAriaLabel;
31597
+ const srAngleGraphAriaDescription = strings.srAngleGraphAriaDescription({
31598
+ angleMeasure,
31599
+ vertexX: srFormatNumber(vertex[X], locale),
31600
+ vertexY: srFormatNumber(vertex[Y], locale),
31601
+ startingSideX: srFormatNumber(startingSide[X], locale),
31602
+ startingSideY: srFormatNumber(startingSide[Y], locale),
31603
+ endingSideX: srFormatNumber(endingSide[X], locale),
31604
+ endingSideY: srFormatNumber(endingSide[Y], locale)
31605
+ });
31606
+ const srAngleStartingSide = strings.srAngleStartingSide({
31607
+ x: srFormatNumber(startingSide[X], locale),
31608
+ y: srFormatNumber(startingSide[Y], locale)
31609
+ });
31610
+ const srAngleEndingSide = strings.srAngleEndingSide({
31611
+ x: srFormatNumber(endingSide[X], locale),
31612
+ y: srFormatNumber(endingSide[Y], locale)
31613
+ });
31614
+ const srAngleVertex = showAngles ? strings.srAngleVertexWithAngleMeasure({
31615
+ x: srFormatNumber(vertex[X], locale),
31616
+ y: srFormatNumber(vertex[Y], locale),
31617
+ angleMeasure
31618
+ }) : strings.srAngleVertex({
31619
+ x: srFormatNumber(vertex[X], locale),
31620
+ y: srFormatNumber(vertex[Y], locale)
31621
+ });
31622
+ return {
31623
+ srAngleGraphAriaLabel,
31624
+ srAngleGraphAriaDescription,
31625
+ srAngleStartingSide,
31626
+ srAngleEndingSide,
31627
+ srAngleVertex
31628
+ };
31548
31629
  }
31549
31630
  const positiveX = [1, 0];
31550
31631
  const negativeX = [-1, 0];
@@ -31586,9 +31667,6 @@ function getAngleSideConstraint(sidePoint, vertex, snapDegrees) {
31586
31667
  };
31587
31668
  }
31588
31669
 
31589
- const {
31590
- clockwise
31591
- } = kmath.geometry;
31592
31670
  function getGradableGraph(state, initialGraph) {
31593
31671
  if (!state.hasBeenInteractedWith) {
31594
31672
  return {
@@ -31665,17 +31743,9 @@ function getGradableGraph(state, initialGraph) {
31665
31743
  };
31666
31744
  }
31667
31745
  if (state.type === "angle" && initialGraph.type === "angle") {
31668
- // We're going to reverse the coords for scoring if the angle is clockwise and
31669
- // we don't allow reflex angles. This is because the angle is largely defined
31670
- // by the order of the points in the coords array, and we want to maintain
31671
- // the same angle scoring with the legacy graph (which had a bug).
31672
- // (LEMS-2190): When we remove the legacy graph, move this logic to the scoring function.
31673
- const areClockwise = clockwise([state.coords[0], state.coords[2], state.coords[1]]);
31674
- const shouldReverseCoords = areClockwise && !state.allowReflexAngles;
31675
- const coords = shouldReverseCoords ? state.coords.slice().reverse() : state.coords;
31676
31746
  return {
31677
31747
  ...initialGraph,
31678
- coords,
31748
+ coords: state.coords,
31679
31749
  allowReflexAngles: state.allowReflexAngles
31680
31750
  };
31681
31751
  }
@@ -31699,17 +31769,16 @@ function getRadius(graph) {
31699
31769
  return Math.sqrt(Math.pow(edgeX - centerX, 2) + Math.pow(edgeY - centerY, 2));
31700
31770
  }
31701
31771
 
31702
- function renderCircleGraph(state, dispatch) {
31772
+ function renderCircleGraph(state, dispatch, i18n) {
31703
31773
  return {
31704
31774
  graph: /*#__PURE__*/React__namespace.createElement(CircleGraph, {
31705
31775
  graphState: state,
31706
31776
  dispatch: dispatch
31707
31777
  }),
31708
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(CircleGraphDescription, {
31709
- state: state
31710
- })
31778
+ interactiveElementsDescription: getCircleGraphDescription(state, i18n)
31711
31779
  };
31712
31780
  }
31781
+ // Exported for testing
31713
31782
  function CircleGraph(props) {
31714
31783
  const {
31715
31784
  dispatch,
@@ -31802,6 +31871,7 @@ function MovableCircle(props) {
31802
31871
  snapStep,
31803
31872
  disableKeyboardInteraction
31804
31873
  } = useGraphConfig();
31874
+ const [focused, setFocused] = React__namespace.useState(false);
31805
31875
  const draggableRef = React.useRef(null);
31806
31876
  const {
31807
31877
  dragging
@@ -31820,7 +31890,9 @@ function MovableCircle(props) {
31820
31890
  ref: draggableRef,
31821
31891
  role: "button",
31822
31892
  tabIndex: disableKeyboardInteraction ? -1 : 0,
31823
- className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`
31893
+ className: `movable-circle ${dragging ? "movable-circle--dragging" : ""}`,
31894
+ onFocus: () => setFocused(true),
31895
+ onBlur: () => setFocused(false)
31824
31896
  }, /*#__PURE__*/React__namespace.createElement("ellipse", {
31825
31897
  className: "focus-ring",
31826
31898
  cx: centerPx[X],
@@ -31835,19 +31907,29 @@ function MovableCircle(props) {
31835
31907
  rx: radiiPx[X],
31836
31908
  ry: radiiPx[Y]
31837
31909
  }), /*#__PURE__*/React__namespace.createElement(DragHandle, {
31838
- center: center
31910
+ center: center,
31911
+ dragging: dragging,
31912
+ focused: focused
31839
31913
  }));
31840
31914
  }
31841
31915
  const dragHandleDimensions = [24, 14];
31842
31916
  const dragHandleDotPositions = crossProduct([-4.4, 0, 4.4], [-2.1, 2.1]);
31843
31917
  function DragHandle(props) {
31844
31918
  const {
31845
- center
31919
+ center,
31920
+ dragging,
31921
+ focused
31846
31922
  } = props;
31847
- const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31848
31923
  const [centerPx] = useTransformVectorsToPixels(center);
31924
+ const {
31925
+ markings
31926
+ } = useGraphConfig();
31927
+ const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31849
31928
  const topLeft = mafs.vec.sub(centerPx, mafs.vec.scale(dragHandleDimensions, 0.5));
31850
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
31929
+ const showHairlines = (dragging || focused) && markings !== "none";
31930
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && /*#__PURE__*/React__namespace.createElement(Hairlines, {
31931
+ point: center
31932
+ }), /*#__PURE__*/React__namespace.createElement("rect", {
31851
31933
  className: "movable-circle-handle",
31852
31934
  x: topLeft[X],
31853
31935
  y: topLeft[Y],
@@ -31874,14 +31956,7 @@ function crossProduct(as, bs) {
31874
31956
  }
31875
31957
  return result;
31876
31958
  }
31877
- function CircleGraphDescription(_ref) {
31878
- let {
31879
- state
31880
- } = _ref;
31881
- // The reason that CircleGraphDescription is a component (rather than a
31882
- // function that returns a string) is because it needs to use a
31883
- // hook: `usePerseusI18n`.
31884
- const i18n = usePerseusI18n();
31959
+ function getCircleGraphDescription(state, i18n) {
31885
31960
  const strings = describeCircleGraph(state, i18n);
31886
31961
  return strings.srCircleInteractiveElement;
31887
31962
  }
@@ -31934,15 +32009,13 @@ function describeCircleGraph(state, i18n) {
31934
32009
  };
31935
32010
  }
31936
32011
 
31937
- function renderLinearGraph(state, dispatch) {
32012
+ function renderLinearGraph(state, dispatch, i18n) {
31938
32013
  return {
31939
32014
  graph: /*#__PURE__*/React__namespace.createElement(LinearGraph, {
31940
32015
  graphState: state,
31941
32016
  dispatch: dispatch
31942
32017
  }),
31943
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(LinearGraphDescription, {
31944
- state: state
31945
- })
32018
+ interactiveElementsDescription: getLinearGraphDescription(state, i18n)
31946
32019
  };
31947
32020
  }
31948
32021
  const LinearGraph = (props, key) => {
@@ -32007,14 +32080,7 @@ const LinearGraph = (props, key) => {
32007
32080
  style: a11y.srOnly
32008
32081
  }, slopeString));
32009
32082
  };
32010
- function LinearGraphDescription(_ref) {
32011
- let {
32012
- state
32013
- } = _ref;
32014
- // The reason that LinearGraphDescription is a component (rather than a
32015
- // function that returns a string) is because it needs to use a
32016
- // hook: `usePerseusI18n`.
32017
- const i18n = usePerseusI18n();
32083
+ function getLinearGraphDescription(state, i18n) {
32018
32084
  const strings = describeLinearGraph(state, i18n);
32019
32085
  return strings.srLinearInteractiveElement;
32020
32086
  }
@@ -32058,15 +32124,13 @@ function describeLinearGraph(state, i18n) {
32058
32124
  };
32059
32125
  }
32060
32126
 
32061
- function renderLinearSystemGraph(state, dispatch) {
32127
+ function renderLinearSystemGraph(state, dispatch, i18n) {
32062
32128
  return {
32063
32129
  graph: /*#__PURE__*/React__namespace.createElement(LinearSystemGraph, {
32064
32130
  graphState: state,
32065
32131
  dispatch: dispatch
32066
32132
  }),
32067
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(LinearSystemGraphDescription, {
32068
- state: state
32069
- })
32133
+ interactiveElementsDescription: getLinearSystemGraphDescription(state, i18n)
32070
32134
  };
32071
32135
  }
32072
32136
  const LinearSystemGraph = props => {
@@ -32159,19 +32223,9 @@ const LinearSystemGraph = props => {
32159
32223
  }, slopeDescription));
32160
32224
  }));
32161
32225
  };
32162
- function LinearSystemGraphDescription(_ref3) {
32163
- let {
32164
- state
32165
- } = _ref3;
32166
- // The reason that LinearSystemGraphDescription is a component (rather
32167
- // than a function that returns a string) is because it needs to use a
32168
- // hook: `usePerseusI18n`.
32169
- const i18n = usePerseusI18n();
32170
- return describeLinearSystemGraph(state, i18n);
32171
- }
32172
32226
 
32173
32227
  // Exported for testing
32174
- function describeLinearSystemGraph(state, i18n) {
32228
+ function getLinearSystemGraphDescription(state, i18n) {
32175
32229
  const {
32176
32230
  strings,
32177
32231
  locale
@@ -32197,15 +32251,13 @@ function describeLinearSystemGraph(state, i18n) {
32197
32251
  });
32198
32252
  }
32199
32253
 
32200
- function renderPointGraph(state, dispatch) {
32254
+ function renderPointGraph(state, dispatch, i18n) {
32201
32255
  return {
32202
32256
  graph: /*#__PURE__*/React__namespace.createElement(PointGraph, {
32203
32257
  graphState: state,
32204
32258
  dispatch: dispatch
32205
32259
  }),
32206
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(PointGraphDescription, {
32207
- state: state
32208
- })
32260
+ interactiveElementsDescription: getPointGraphDescription(state, i18n)
32209
32261
  };
32210
32262
  }
32211
32263
  function PointGraph(props) {
@@ -32227,7 +32279,9 @@ function PointGraph(props) {
32227
32279
  if (focusedIndex != null) {
32228
32280
  pointsRef.current[focusedIndex]?.focus();
32229
32281
  }
32230
- }, [props.graphState.focusedPointIndex, pointsRef]);
32282
+ }, [props.graphState.focusedPointIndex,
32283
+ // Ensure that we re-focus if a point is deleted.
32284
+ props.graphState.coords.length, pointsRef]);
32231
32285
  const statefulProps = {
32232
32286
  ...props,
32233
32287
  graphConfig,
@@ -32299,18 +32353,9 @@ function UnlimitedPointGraph(statefulProps) {
32299
32353
  }
32300
32354
  })));
32301
32355
  }
32302
- function PointGraphDescription(_ref) {
32303
- let {
32304
- state
32305
- } = _ref;
32306
- // PointGraphDescription needs to `usePerseusI18n`, so it has to be a
32307
- // component rather than a function that simply returns a string.
32308
- const i18n = usePerseusI18n();
32309
- return describePointGraph(state, i18n);
32310
- }
32311
32356
 
32312
32357
  // Exported for testing
32313
- function describePointGraph(state, i18n) {
32358
+ function getPointGraphDescription(state, i18n) {
32314
32359
  const {
32315
32360
  strings,
32316
32361
  locale
@@ -32318,8 +32363,8 @@ function describePointGraph(state, i18n) {
32318
32363
  if (state.coords.length === 0) {
32319
32364
  return strings.srNoInteractiveElements;
32320
32365
  }
32321
- const pointDescriptions = state.coords.map((_ref2, index) => {
32322
- let [x, y] = _ref2;
32366
+ const pointDescriptions = state.coords.map((_ref, index) => {
32367
+ let [x, y] = _ref;
32323
32368
  return strings.srPointAtCoordinates({
32324
32369
  num: index + 1,
32325
32370
  x: srFormatNumber(x, locale),
@@ -32327,17 +32372,20 @@ function describePointGraph(state, i18n) {
32327
32372
  });
32328
32373
  });
32329
32374
  return strings.srInteractiveElements({
32330
- elements: pointDescriptions.join(", ")
32375
+ elements: pointDescriptions.join(" ")
32331
32376
  });
32332
32377
  }
32333
32378
 
32334
- function renderPolygonGraph(state, dispatch) {
32379
+ const {
32380
+ convertRadiansToDegrees
32381
+ } = kmath.angles;
32382
+ function renderPolygonGraph(state, dispatch, i18n, markings) {
32335
32383
  return {
32336
32384
  graph: /*#__PURE__*/React__namespace.createElement(PolygonGraph, {
32337
32385
  graphState: state,
32338
32386
  dispatch: dispatch
32339
32387
  }),
32340
- interactiveElementsDescription: null
32388
+ interactiveElementsDescription: getPolygonGraphDescription(state, i18n, markings)
32341
32389
  };
32342
32390
  }
32343
32391
  const PolygonGraph = props => {
@@ -32396,7 +32444,9 @@ const PolygonGraph = props => {
32396
32444
  if (focusedIndex != null) {
32397
32445
  pointsRef.current[focusedIndex]?.focus();
32398
32446
  }
32399
- }, [props.graphState.focusedPointIndex, pointsRef]);
32447
+ }, [props.graphState.focusedPointIndex,
32448
+ // Ensure that we re-focus if a point is deleted.
32449
+ props.graphState.coords.length, pointsRef]);
32400
32450
 
32401
32451
  // If the unlimited polygon is rendered with 3 or more coordinates
32402
32452
  // Close the polygon, but only on first render.
@@ -32422,7 +32472,7 @@ const PolygonGraph = props => {
32422
32472
  focusVisible,
32423
32473
  setFocusVisible
32424
32474
  };
32425
- return numSides === "unlimited" ? UnlimitedPolygonGraph(statefulProps) : LimitedPolygonGraph(statefulProps);
32475
+ return numSides === "unlimited" ? /*#__PURE__*/React__namespace.createElement(UnlimitedPolygonGraph, statefulProps) : /*#__PURE__*/React__namespace.createElement(LimitedPolygonGraph, statefulProps);
32426
32476
  };
32427
32477
  const LimitedPolygonGraph = statefulProps => {
32428
32478
  const {
@@ -32447,8 +32497,39 @@ const LimitedPolygonGraph = statefulProps => {
32447
32497
  const {
32448
32498
  disableKeyboardInteraction
32449
32499
  } = graphConfig;
32500
+ const {
32501
+ strings,
32502
+ locale
32503
+ } = usePerseusI18n();
32504
+ const id = React__namespace.useId();
32505
+ const pointsOffArray = Array(points.length).fill("off");
32506
+ // When moving an element, set its aria-live to "polite" and the others
32507
+ // to "off". Otherwise, other connected elements that move at the same
32508
+ // time might override the currently focused element's aria live.
32509
+ const [ariaLives, setAriaLives] = React__namespace.useState([
32510
+ // First one represents the aria-live value for the polygon itself.
32511
+ "off",
32512
+ // The rest represent the points.
32513
+ ...pointsOffArray]);
32450
32514
  const lines = getLines(points);
32451
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32515
+ const polygonPointsNumId = id + "-points-num";
32516
+ const polygonPointsId = id + "-points";
32517
+
32518
+ // Aria label srings
32519
+ const {
32520
+ srPolygonGraph,
32521
+ srPolygonGraphPointsNum,
32522
+ srPolygonGraphPoints,
32523
+ srPolygonElementsNum
32524
+ } = describePolygonGraph(statefulProps.graphState, {
32525
+ strings,
32526
+ locale
32527
+ }, statefulProps.graphConfig.markings);
32528
+ return /*#__PURE__*/React__namespace.createElement("g", {
32529
+ // Outer graph minimal description
32530
+ "aria-label": srPolygonGraph,
32531
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32532
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32452
32533
  points: [...points],
32453
32534
  color: "var(--movable-line-stroke-color)",
32454
32535
  svgPolygonProps: {
@@ -32474,13 +32555,17 @@ const LimitedPolygonGraph = statefulProps => {
32474
32555
  });
32475
32556
  }), showSides && lines.map((_ref, i) => {
32476
32557
  let [start, end] = _ref;
32558
+ // Use x and y to find the position of the label
32477
32559
  const [x, y] = mafs.vec.midpoint(start, end);
32478
- const length = parseFloat(mafs.vec.dist(start, end).toFixed(snapTo === "sides" ? 0 : 1));
32560
+ const length = mafs.vec.dist(start, end);
32561
+ // Check if the length needs to indicate
32562
+ // that it's an approximation.
32563
+ const isApprox = !Number.isInteger(length);
32479
32564
  return /*#__PURE__*/React__namespace.createElement(TextLabel, {
32480
32565
  key: "side-" + i,
32481
32566
  x: x,
32482
32567
  y: y
32483
- }, !Number.isInteger(length) && "≈ ", length);
32568
+ }, isApprox ? `≈ ${length.toFixed(snapTo === "sides" ? 0 : 1)}` : length);
32484
32569
  }), /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
32485
32570
  points: [...points],
32486
32571
  color: "transparent",
@@ -32500,27 +32585,80 @@ const LimitedPolygonGraph = statefulProps => {
32500
32585
  setFocusVisible(hasFocusVisible(polygonRef.current));
32501
32586
  },
32502
32587
  // Required for lines to darken on focus
32503
- onFocus: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32588
+ onFocus: () => {
32589
+ setFocusVisible(hasFocusVisible(polygonRef.current));
32590
+ setAriaLives(() => ["polite", ...pointsOffArray]);
32591
+ },
32504
32592
  // Required for line weighting to update on blur. Without this,
32505
32593
  // the user has to hover over the shape for it to update
32506
32594
  onBlur: () => setFocusVisible(hasFocusVisible(polygonRef.current)),
32507
- className: "movable-polygon"
32508
- }
32509
- }), points.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32510
- key: "point-" + i,
32511
- constrain: constrain,
32512
- point: point,
32513
- sequenceNumber: i + 1,
32514
- onMove: destination => {
32515
- const now = Date.now();
32516
- const targetFPS = 40;
32517
- const moveThresholdTime = 1000 / targetFPS;
32518
- if (now - lastMoveTimeRef.current > moveThresholdTime) {
32519
- dispatch(actions.polygon.movePoint(i, destination));
32520
- lastMoveTimeRef.current = now;
32521
- }
32595
+ className: "movable-polygon",
32596
+ // Accessibility-related fields
32597
+ role: "button",
32598
+ "aria-label": srPolygonGraphPoints ? `${srPolygonElementsNum} ${srPolygonGraphPoints}` : srPolygonElementsNum,
32599
+ "aria-live": ariaLives[0]
32522
32600
  }
32523
- })));
32601
+ }), points.map((point, i) => {
32602
+ const angleId = `${id}-angle-${i}`;
32603
+ const side1Id = `${id}-point-${i}-side-1`;
32604
+ const side2Id = `${id}-point-${i}-side-2`;
32605
+
32606
+ // Limited polygons always have an angle at each vertex.
32607
+ const angle = getAngleFromPoints(points, i);
32608
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32609
+
32610
+ // Limited polygons always two sides attached to each vertex.
32611
+ const sidesArray = getSideLengthsFromPoints(points, i);
32612
+ const {
32613
+ pointIndex: point1Index,
32614
+ sideLength: side1Length
32615
+ } = sidesArray[0];
32616
+ const {
32617
+ pointIndex: point2Index,
32618
+ sideLength: side2Length
32619
+ } = sidesArray[1];
32620
+ return /*#__PURE__*/React__namespace.createElement("g", {
32621
+ key: "point-" + i
32622
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32623
+ ariaDescribedBy: `${angleId} ${side1Id} ${side2Id}`,
32624
+ ariaLive: ariaLives[i + 1],
32625
+ constrain: constrain,
32626
+ point: point,
32627
+ sequenceNumber: i + 1,
32628
+ onMove: destination => {
32629
+ const now = Date.now();
32630
+ const targetFPS = 40;
32631
+ const moveThresholdTime = 1000 / targetFPS;
32632
+ if (now - lastMoveTimeRef.current > moveThresholdTime) {
32633
+ dispatch(actions.polygon.movePoint(i, destination));
32634
+ lastMoveTimeRef.current = now;
32635
+ }
32636
+ },
32637
+ onFocus: () => {
32638
+ const newPointAriaLives = [...pointsOffArray];
32639
+ newPointAriaLives[i] = "polite";
32640
+ // Whole polygon is "off", and the current
32641
+ // point is "polite".
32642
+ setAriaLives(["off", ...newPointAriaLives]);
32643
+ }
32644
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32645
+ id: angleId
32646
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32647
+ angle: angleDegree
32648
+ }) : strings.srPolygonPointAngleApprox({
32649
+ angle: srFormatNumber(angleDegree, locale, 1)
32650
+ })), /*#__PURE__*/React__namespace.createElement("g", {
32651
+ id: side1Id
32652
+ }, getPolygonSideString(side1Length, point1Index, strings, locale)), /*#__PURE__*/React__namespace.createElement("g", {
32653
+ id: side2Id
32654
+ }, getPolygonSideString(side2Length, point2Index, strings, locale)));
32655
+ }), /*#__PURE__*/React__namespace.createElement("g", {
32656
+ id: polygonPointsNumId,
32657
+ style: a11y.srOnly
32658
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32659
+ id: polygonPointsId,
32660
+ style: a11y.srOnly
32661
+ }, srPolygonGraphPoints));
32524
32662
  };
32525
32663
  const UnlimitedPolygonGraph = statefulProps => {
32526
32664
  const {
@@ -32535,6 +32673,19 @@ const UnlimitedPolygonGraph = statefulProps => {
32535
32673
  coords,
32536
32674
  closedPolygon
32537
32675
  } = statefulProps.graphState;
32676
+ const {
32677
+ strings,
32678
+ locale
32679
+ } = usePerseusI18n();
32680
+ const id = React__namespace.useId();
32681
+ const polygonPointsNumId = id + "-points-num";
32682
+ const polygonPointsId = id + "-points";
32683
+
32684
+ // When moving an element, set its aria-live to "polite" and the others
32685
+ // to "off". Otherwise, other connected elements that move at the same
32686
+ // time might override the currently focused element's aria live.
32687
+ const pointsOffArray = Array(points.length).fill("off");
32688
+ const [ariaLives, setAriaLives] = React__namespace.useState(pointsOffArray);
32538
32689
 
32539
32690
  // If the polygon is closed, return a LimitedPolygon component.
32540
32691
  if (closedPolygon) {
@@ -32549,7 +32700,22 @@ const UnlimitedPolygonGraph = statefulProps => {
32549
32700
  } = graphConfig;
32550
32701
  const widthPx = graphDimensionsInPixels[0];
32551
32702
  const heightPx = graphDimensionsInPixels[1];
32552
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
32703
+
32704
+ // Aria label srings
32705
+ const emptyGraph = coords.length === 0;
32706
+ const {
32707
+ srPolygonGraph,
32708
+ srPolygonGraphPointsNum,
32709
+ srPolygonGraphPoints
32710
+ } = describePolygonGraph(statefulProps.graphState, {
32711
+ strings,
32712
+ locale
32713
+ }, statefulProps.graphConfig.markings);
32714
+ return /*#__PURE__*/React__namespace.createElement("g", {
32715
+ // Outer graph minimal description
32716
+ "aria-label": emptyGraph ? strings.srUnlimitedPolygonEmpty : srPolygonGraph,
32717
+ "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32718
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
32553
32719
  points: [...points],
32554
32720
  color: "var(--movable-line-stroke-color)",
32555
32721
  svgPolylineProps: {
@@ -32574,28 +32740,71 @@ const UnlimitedPolygonGraph = statefulProps => {
32574
32740
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
32575
32741
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
32576
32742
  }
32577
- }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32578
- key: i,
32579
- point: point,
32580
- sequenceNumber: i + 1,
32581
- onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32582
- ref: ref => {
32583
- pointsRef.current[i] = ref;
32584
- },
32585
- onFocus: () => {
32586
- dispatch(actions.polygon.focusPoint(i));
32587
- },
32588
- onClick: () => {
32589
- // If the point being clicked is the first point and
32590
- // there's enough non-duplicated points to form
32591
- // a polygon (3 or more), close the shape before
32592
- // setting focus.
32593
- if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32594
- dispatch(actions.polygon.closePolygon());
32595
- }
32596
- dispatch(actions.polygon.clickPoint(i));
32743
+ }), coords.map((point, i) => {
32744
+ const angleId = `${id}-angle-${i}`;
32745
+ let sideIds = "";
32746
+
32747
+ // This point only has an angle if it's not the first
32748
+ // or last point in this open polygon.
32749
+ const hasAngle = i > 0 && i < coords.length - 1;
32750
+ const angle = hasAngle ? getAngleFromPoints(points, i) : null;
32751
+ const angleDegree = angle ? convertRadiansToDegrees(angle) : null;
32752
+ const sidesArray = getSideLengthsFromPoints(points, i, true);
32753
+ for (let sideIndex = 0; sideIndex < sidesArray.length; sideIndex++) {
32754
+ sideIds += `${id}-point-${i}-side-${sideIndex} `;
32597
32755
  }
32598
- })));
32756
+ return /*#__PURE__*/React__namespace.createElement("g", {
32757
+ key: "point-" + i
32758
+ }, /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
32759
+ ariaDescribedBy: `${angleId} ${sideIds}`,
32760
+ ariaLive: ariaLives[i],
32761
+ point: point,
32762
+ sequenceNumber: i + 1,
32763
+ onMove: destination => dispatch(actions.polygon.movePoint(i, destination)),
32764
+ ref: ref => {
32765
+ pointsRef.current[i] = ref;
32766
+ },
32767
+ onFocus: () => {
32768
+ dispatch(actions.polygon.focusPoint(i));
32769
+ const newPointAriaLives = [...pointsOffArray];
32770
+ newPointAriaLives[i] = "polite";
32771
+ // Whole polygon is "off", and the current
32772
+ // point is "polite".
32773
+ setAriaLives([...newPointAriaLives]);
32774
+ },
32775
+ onClick: () => {
32776
+ // If the point being clicked is the first point and
32777
+ // there's enough non-duplicated points to form
32778
+ // a polygon (3 or more), close the shape before
32779
+ // setting focus.
32780
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
32781
+ dispatch(actions.polygon.closePolygon());
32782
+ }
32783
+ dispatch(actions.polygon.clickPoint(i));
32784
+ }
32785
+ }), angleDegree && /*#__PURE__*/React__namespace.createElement("g", {
32786
+ id: angleId
32787
+ }, Number.isInteger(angleDegree) ? strings.srPolygonPointAngle({
32788
+ angle: angleDegree
32789
+ }) : strings.srPolygonPointAngleApprox({
32790
+ angle: srFormatNumber(angleDegree, locale, 1)
32791
+ })), sidesArray.map((_ref2, j) => {
32792
+ let {
32793
+ pointIndex,
32794
+ sideLength
32795
+ } = _ref2;
32796
+ return /*#__PURE__*/React__namespace.createElement("g", {
32797
+ key: `${id}-point-${i}-side-${j}`,
32798
+ id: `${id}-point-${i}-side-${j}`
32799
+ }, getPolygonSideString(sideLength, pointIndex, strings, locale));
32800
+ }));
32801
+ }), coords.length > 0 && /*#__PURE__*/React__namespace.createElement("g", {
32802
+ id: polygonPointsNumId,
32803
+ style: a11y.srOnly
32804
+ }, srPolygonGraphPointsNum), srPolygonGraphPoints && /*#__PURE__*/React__namespace.createElement("g", {
32805
+ id: polygonPointsId,
32806
+ style: a11y.srOnly
32807
+ }, srPolygonGraphPoints));
32599
32808
  };
32600
32809
  function getLines(points) {
32601
32810
  return points.map((point, i) => {
@@ -32614,6 +32823,54 @@ const hasFocusVisible = element => {
32614
32823
  return matches(":focus");
32615
32824
  }
32616
32825
  };
32826
+ function getPolygonGraphDescription(state, i18n, markings) {
32827
+ const strings = describePolygonGraph(state, i18n, markings);
32828
+ return strings.srPolygonInteractiveElements;
32829
+ }
32830
+ function describePolygonGraph(state, i18n, markings) {
32831
+ const {
32832
+ strings,
32833
+ locale
32834
+ } = i18n;
32835
+ const {
32836
+ coords
32837
+ } = state;
32838
+ const isCoordinatePlane = markings === "axes" || markings === "graph";
32839
+ const hasOnePoint = coords.length === 1;
32840
+
32841
+ // Figure out graph aria label based on markings.
32842
+ const srPolygonGraph = isCoordinatePlane ? strings.srPolygonGraphCoordinatePlane : strings.srPolygonGraph;
32843
+ const srPolygonGraphPointsNum = hasOnePoint ? strings.srPolygonGraphPointsOne : strings.srPolygonGraphPointsNum({
32844
+ num: coords.length
32845
+ });
32846
+ let srPolygonGraphPoints;
32847
+ // Figure out graph description based on markings.
32848
+ // If the graph is not on a coordinate plane, we should not include
32849
+ // the points' coordinates in the description.
32850
+ if (isCoordinatePlane) {
32851
+ const pointsString = coords.map((coord, i) => {
32852
+ return strings.srPointAtCoordinates({
32853
+ num: i + 1,
32854
+ x: srFormatNumber(coord[0], locale),
32855
+ y: srFormatNumber(coord[1], locale)
32856
+ });
32857
+ });
32858
+ srPolygonGraphPoints = pointsString.join(" ");
32859
+ }
32860
+ const srPolygonElementsNum = hasOnePoint ? strings.srPolygonElementsOne : strings.srPolygonElementsNum({
32861
+ num: coords.length
32862
+ });
32863
+ const srPolygonInteractiveElements = coords.length > 0 ? strings.srInteractiveElements({
32864
+ elements: [srPolygonElementsNum, srPolygonGraphPoints].join(" ")
32865
+ }) : null;
32866
+ return {
32867
+ srPolygonGraph,
32868
+ srPolygonGraphPointsNum,
32869
+ srPolygonGraphPoints,
32870
+ srPolygonElementsNum,
32871
+ srPolygonInteractiveElements
32872
+ };
32873
+ }
32617
32874
 
32618
32875
  function renderQuadraticGraph(state, dispatch, i18n) {
32619
32876
  return {
@@ -32772,15 +33029,13 @@ function describeQuadraticGraph(state, i18n) {
32772
33029
  };
32773
33030
  }
32774
33031
 
32775
- function renderRayGraph(state, dispatch) {
33032
+ function renderRayGraph(state, dispatch, i18n) {
32776
33033
  return {
32777
33034
  graph: /*#__PURE__*/React__namespace.createElement(RayGraph, {
32778
33035
  graphState: state,
32779
33036
  dispatch: dispatch
32780
33037
  }),
32781
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(RayGraphDescription, {
32782
- state: state
32783
- })
33038
+ interactiveElementsDescription: getRayGraphDescription(state, i18n)
32784
33039
  };
32785
33040
  }
32786
33041
  const RayGraph = props => {
@@ -32834,14 +33089,7 @@ const RayGraph = props => {
32834
33089
  style: a11y.srOnly
32835
33090
  }, srRayPoints));
32836
33091
  };
32837
- function RayGraphDescription(_ref) {
32838
- let {
32839
- state
32840
- } = _ref;
32841
- // The reason that RayGraphDescription is a component (rather than a
32842
- // function that returns a string) is because it needs to use a
32843
- // hook: `usePerseusI18n`.
32844
- const i18n = usePerseusI18n();
33092
+ function getRayGraphDescription(state, i18n) {
32845
33093
  const strings = describeRayGraph(state, i18n);
32846
33094
  return strings.srRayInteractiveElement;
32847
33095
  }
@@ -32891,15 +33139,13 @@ function describeRayGraph(state, i18n) {
32891
33139
  };
32892
33140
  }
32893
33141
 
32894
- function renderSegmentGraph(state, dispatch) {
33142
+ function renderSegmentGraph(state, dispatch, i18n) {
32895
33143
  return {
32896
33144
  graph: /*#__PURE__*/React__namespace.createElement(SegmentGraph, {
32897
33145
  graphState: state,
32898
33146
  dispatch: dispatch
32899
33147
  }),
32900
- interactiveElementsDescription: /*#__PURE__*/React__namespace.createElement(SegmentGraphDescription, {
32901
- state: state
32902
- })
33148
+ interactiveElementsDescription: getSegmentGraphDescription(state, i18n)
32903
33149
  };
32904
33150
  }
32905
33151
  const SegmentGraph = _ref => {
@@ -32993,25 +33239,15 @@ const SegmentGraph = _ref => {
32993
33239
  function getLengthOfSegment(segment) {
32994
33240
  return kmath.point.distanceToPoint(...segment);
32995
33241
  }
32996
- function SegmentGraphDescription(_ref2) {
32997
- let {
32998
- state
32999
- } = _ref2;
33000
- // The reason that SegmentGraphDescription is a component (rather than a
33001
- // function that returns a string) is because it needs to use a
33002
- // hook: `usePerseusI18n`.
33003
- const i18n = usePerseusI18n();
33004
- return describeSegmentGraph(state, i18n);
33005
- }
33006
33242
 
33007
33243
  // Exported for testing
33008
- function describeSegmentGraph(state, i18n) {
33244
+ function getSegmentGraphDescription(state, i18n) {
33009
33245
  const {
33010
33246
  strings,
33011
33247
  locale
33012
33248
  } = i18n;
33013
- const segmentDescriptions = state.coords.map((_ref3, index) => {
33014
- let [point1, point2] = _ref3;
33249
+ const segmentDescriptions = state.coords.map((_ref2, index) => {
33250
+ let [point1, point2] = _ref2;
33015
33251
  return strings.srMultipleSegmentIndividualLabel({
33016
33252
  point1X: srFormatNumber(point1[X], locale),
33017
33253
  point1Y: srFormatNumber(point1[Y], locale),
@@ -33025,13 +33261,13 @@ function describeSegmentGraph(state, i18n) {
33025
33261
  });
33026
33262
  }
33027
33263
 
33028
- function renderSinusoidGraph(state, dispatch) {
33264
+ function renderSinusoidGraph(state, dispatch, i18n) {
33029
33265
  return {
33030
33266
  graph: /*#__PURE__*/React__namespace.createElement(SinusoidGraph, {
33031
33267
  graphState: state,
33032
33268
  dispatch: dispatch
33033
33269
  }),
33034
- interactiveElementsDescription: null
33270
+ interactiveElementsDescription: getSinusoidDescription(state, i18n)
33035
33271
  };
33036
33272
  }
33037
33273
  function SinusoidGraph(props) {
@@ -33039,6 +33275,9 @@ function SinusoidGraph(props) {
33039
33275
  dispatch,
33040
33276
  graphState
33041
33277
  } = props;
33278
+ const i18n = usePerseusI18n();
33279
+ const id = React__namespace.useId();
33280
+ const descriptionId = id + "-description";
33042
33281
 
33043
33282
  // Destructure the coordinates from the graph state
33044
33283
  // Note: The order of the coordinates is important:
@@ -33063,15 +33302,30 @@ function SinusoidGraph(props) {
33063
33302
  if (coeffs !== undefined) {
33064
33303
  coeffRef.current = coeffs;
33065
33304
  }
33066
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
33305
+
33306
+ // Aria strings
33307
+ const {
33308
+ srSinusoidGraph,
33309
+ srSinusoidDescription,
33310
+ srSinusoidRootPoint,
33311
+ srSinusoidPeakPoint
33312
+ } = describeSinusoidGraph(graphState, i18n);
33313
+ return /*#__PURE__*/React__namespace.createElement("g", {
33314
+ // Outer graph minimal description
33315
+ "aria-label": srSinusoidGraph,
33316
+ "aria-describedby": descriptionId
33317
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
33067
33318
  y: x => computeSine(x, coeffRef.current),
33068
33319
  color: wonderBlocksTokens.color.blue
33069
33320
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
33321
+ ariaLabel: i === 0 ? srSinusoidRootPoint : srSinusoidPeakPoint,
33070
33322
  key: "point-" + i,
33071
33323
  point: coord,
33072
33324
  sequenceNumber: i + 1,
33073
33325
  onMove: destination => dispatch(actions.sinusoid.movePoint(i, destination))
33074
- })));
33326
+ })), /*#__PURE__*/React__namespace.createElement("g", {
33327
+ id: descriptionId
33328
+ }, srSinusoidDescription));
33075
33329
  }
33076
33330
 
33077
33331
  // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
@@ -33109,6 +33363,52 @@ const getSinusoidCoefficients$1 = coords => {
33109
33363
  verticalOffset
33110
33364
  };
33111
33365
  };
33366
+ function getSinusoidDescription(state, i18n) {
33367
+ const strings = describeSinusoidGraph(state, i18n);
33368
+ return strings.srSinusoidInteractiveElements;
33369
+ }
33370
+ function describeSinusoidGraph(state, i18n) {
33371
+ const {
33372
+ strings,
33373
+ locale
33374
+ } = i18n;
33375
+ const {
33376
+ coords
33377
+ } = state;
33378
+ const [root, peak] = coords;
33379
+ const diffX = Math.abs(peak[X] - root[X]);
33380
+ const diffY = Math.abs(peak[Y] - root[Y]);
33381
+ const srSinusoidGraph = strings.srSinusoidGraph;
33382
+ const srSinusoidDescription = strings.srSinusoidDescription({
33383
+ minValue: srFormatNumber(root[Y] - diffY, locale),
33384
+ maxValue: srFormatNumber(root[Y] + diffY, locale),
33385
+ cycleStart: srFormatNumber(root[X] - 2 * diffX, locale),
33386
+ cycleEnd: srFormatNumber(root[X] + 2 * diffX, locale)
33387
+ });
33388
+ const srSinusoidRootPoint = strings.srSinusoidRootPoint({
33389
+ x: srFormatNumber(root[X], locale),
33390
+ y: srFormatNumber(root[Y], locale)
33391
+ });
33392
+ const srSinusoidPeakPoint = strings.srSinusoidPeakPoint({
33393
+ x: srFormatNumber(peak[X], locale),
33394
+ y: srFormatNumber(peak[Y], locale)
33395
+ });
33396
+ const srSinusoidInteractiveElements = strings.srInteractiveElements({
33397
+ elements: strings.srSinusoidInteractiveElements({
33398
+ point1X: srFormatNumber(root[X], locale),
33399
+ point1Y: srFormatNumber(root[Y], locale),
33400
+ point2X: srFormatNumber(peak[X], locale),
33401
+ point2Y: srFormatNumber(peak[Y], locale)
33402
+ })
33403
+ });
33404
+ return {
33405
+ srSinusoidGraph,
33406
+ srSinusoidDescription,
33407
+ srSinusoidRootPoint,
33408
+ srSinusoidPeakPoint,
33409
+ srSinusoidInteractiveElements
33410
+ };
33411
+ }
33112
33412
 
33113
33413
  const {
33114
33414
  calculateAngleInDegrees,
@@ -33279,6 +33579,7 @@ const MafsGraph = props => {
33279
33579
  const descriptionId = `interactive-graph-description-${uniqueId}`;
33280
33580
  const interactiveElementsDescriptionId = `interactive-graph-interactive-elements-description-${uniqueId}`;
33281
33581
  const unlimitedGraphKeyboardPromptId = `unlimited-graph-keyboard-prompt-${uniqueId}`;
33582
+ const instructionsId = `instructions-${uniqueId}`;
33282
33583
  const graphRef = React__namespace.useRef(null);
33283
33584
  const {
33284
33585
  analytics
@@ -33306,6 +33607,7 @@ const MafsGraph = props => {
33306
33607
  const interactionPrompt = isUnlimitedGraphState(state) && state.showKeyboardInteractionInvitation;
33307
33608
  wonderBlocksCore.useOnMountEffect(() => {
33308
33609
  analytics.onAnalyticsEvent({
33610
+ // TODO(LEMS-2827): Remove analytics event in LEMS-2827 in favor of ti below.
33309
33611
  type: "perseus:interactive-graph-widget:rendered",
33310
33612
  payload: {
33311
33613
  type,
@@ -33313,6 +33615,14 @@ const MafsGraph = props => {
33313
33615
  widgetId: "interactive-graph"
33314
33616
  }
33315
33617
  });
33618
+ analytics.onAnalyticsEvent({
33619
+ type: "perseus:widget:rendered:ti",
33620
+ payload: {
33621
+ widgetSubType: type,
33622
+ widgetType: "INTERACTIVE_GRAPH",
33623
+ widgetId: "interactive-graph"
33624
+ }
33625
+ });
33316
33626
  });
33317
33627
  const {
33318
33628
  graph,
@@ -33320,7 +33630,8 @@ const MafsGraph = props => {
33320
33630
  } = renderGraphElements({
33321
33631
  state,
33322
33632
  dispatch,
33323
- i18n
33633
+ i18n,
33634
+ markings: props.markings
33324
33635
  });
33325
33636
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
33326
33637
  value: {
@@ -33355,7 +33666,7 @@ const MafsGraph = props => {
33355
33666
  handleKeyboardEvent(event, state, dispatch);
33356
33667
  },
33357
33668
  "aria-label": fullGraphAriaLabel,
33358
- "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && "unlimited-graph-keyboard-prompt"),
33669
+ "aria-describedby": describedByIds(fullGraphAriaDescription && descriptionId, interactiveElementsDescription && interactiveElementsDescriptionId, isUnlimitedGraphState(state) && unlimitedGraphKeyboardPromptId, state.type !== "none" && instructionsId),
33359
33670
  ref: graphRef,
33360
33671
  tabIndex: 0,
33361
33672
  onFocus: event => {
@@ -33372,7 +33683,11 @@ const MafsGraph = props => {
33372
33683
  id: interactiveElementsDescriptionId,
33373
33684
  tabIndex: -1,
33374
33685
  className: "mafs-sr-only"
33375
- }, interactiveElementsDescription), /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
33686
+ }, interactiveElementsDescription), state.type !== "none" && /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
33687
+ id: instructionsId,
33688
+ tabIndex: -1,
33689
+ className: "mafs-sr-only"
33690
+ }, isUnlimitedGraphState(state) ? strings.srUnlimitedGraphInstructions : strings.srGraphInstructions), /*#__PURE__*/React__namespace.createElement(LegacyGrid, {
33376
33691
  box: props.box,
33377
33692
  backgroundImage: props.backgroundImage
33378
33693
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
@@ -33381,7 +33696,21 @@ const MafsGraph = props => {
33381
33696
  bottom: 0,
33382
33697
  left: 0
33383
33698
  }
33384
- }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, null)), /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33699
+ }, (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisLabels, {
33700
+ i18n: i18n
33701
+ })), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
33702
+ // If we have locked figures, we set aria-hidden
33703
+ // to false so that screen readers can read the
33704
+ // locked figures. If we don't have any locked
33705
+ // figures, we need to set aria-hidden to true so
33706
+ // that screen readers don't read this Mafs element
33707
+ // as an empty image.
33708
+ // Note: Adding this in a wrapping View element
33709
+ // so that `aria-hidden` is a valid property.
33710
+ // It does not work on `Mafs` or svg elements.
33711
+ , {
33712
+ "aria-hidden": props.lockedFigures && props.lockedFigures.length > 0 ? false : true
33713
+ }, /*#__PURE__*/React__namespace.createElement(mafs.Mafs, {
33385
33714
  preserveAspectRatio: false,
33386
33715
  viewBox: {
33387
33716
  x: state.range[X],
@@ -33401,10 +33730,10 @@ const MafsGraph = props => {
33401
33730
  height: height
33402
33731
  })),
33403
33732
  // Only render the axis ticks and arrows if the markings are set to a full "graph"
33404
- (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
33733
+ (props.markings === "graph" || props.markings === "axes") && /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(AxisTicks, null), /*#__PURE__*/React__namespace.createElement(AxisArrows, null)), props.lockedFigures && props.lockedFigures.length > 0 && /*#__PURE__*/React__namespace.createElement("svg", nestedSVGAttributes, /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
33405
33734
  lockedFigures: props.lockedFigures,
33406
33735
  range: state.range
33407
- }))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33736
+ })))), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLabelsLayer, {
33408
33737
  lockedFigures: props.lockedFigures
33409
33738
  }), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
33410
33739
  style: {
@@ -33698,32 +34027,33 @@ const renderGraphElements = props => {
33698
34027
  const {
33699
34028
  state,
33700
34029
  dispatch,
33701
- i18n
34030
+ i18n,
34031
+ markings
33702
34032
  } = props;
33703
34033
  const {
33704
34034
  type
33705
34035
  } = state;
33706
34036
  switch (type) {
33707
34037
  case "angle":
33708
- return renderAngleGraph(state, dispatch);
34038
+ return renderAngleGraph(state, dispatch, i18n);
33709
34039
  case "segment":
33710
- return renderSegmentGraph(state, dispatch);
34040
+ return renderSegmentGraph(state, dispatch, i18n);
33711
34041
  case "linear-system":
33712
- return renderLinearSystemGraph(state, dispatch);
34042
+ return renderLinearSystemGraph(state, dispatch, i18n);
33713
34043
  case "linear":
33714
- return renderLinearGraph(state, dispatch);
34044
+ return renderLinearGraph(state, dispatch, i18n);
33715
34045
  case "ray":
33716
- return renderRayGraph(state, dispatch);
34046
+ return renderRayGraph(state, dispatch, i18n);
33717
34047
  case "polygon":
33718
- return renderPolygonGraph(state, dispatch);
34048
+ return renderPolygonGraph(state, dispatch, i18n, markings);
33719
34049
  case "point":
33720
- return renderPointGraph(state, dispatch);
34050
+ return renderPointGraph(state, dispatch, i18n);
33721
34051
  case "circle":
33722
- return renderCircleGraph(state, dispatch);
34052
+ return renderCircleGraph(state, dispatch, i18n);
33723
34053
  case "quadratic":
33724
34054
  return renderQuadraticGraph(state, dispatch, i18n);
33725
34055
  case "sinusoid":
33726
- return renderSinusoidGraph(state, dispatch);
34056
+ return renderSinusoidGraph(state, dispatch, i18n);
33727
34057
  case "none":
33728
34058
  return {
33729
34059
  graph: null,
@@ -34143,13 +34473,13 @@ const {
34143
34473
  polar
34144
34474
  } = kmath.angles;
34145
34475
  const {
34146
- angleMeasures: angleMeasures$1,
34147
- ccw: ccw$1,
34148
- lawOfCosines: lawOfCosines$1,
34476
+ angleMeasures,
34477
+ ccw,
34478
+ lawOfCosines,
34149
34479
  magnitude: magnitude$1,
34150
34480
  polygonSidesIntersect,
34151
34481
  reverseVector,
34152
- sign: sign$1,
34482
+ sign,
34153
34483
  vector: vector$1
34154
34484
  } = kmath.geometry;
34155
34485
  const {
@@ -34656,7 +34986,7 @@ function doAddPoint(state, action) {
34656
34986
  ...state,
34657
34987
  hasBeenInteractedWith: true,
34658
34988
  coords: newCoords,
34659
- showRemovePointButton: false,
34989
+ showRemovePointButton: true,
34660
34990
  focusedPointIndex: newCoords.length - 1
34661
34991
  };
34662
34992
  }
@@ -34664,17 +34994,12 @@ function doRemovePoint(state, action) {
34664
34994
  if (!isUnlimitedGraphState(state)) {
34665
34995
  return state;
34666
34996
  }
34667
- let nextFocusedPointIndex;
34668
- if (state.interactionMode === "mouse") {
34669
- nextFocusedPointIndex = null;
34670
- } else {
34671
- nextFocusedPointIndex = state.coords.length > 1 ? state.coords.length - 2 : null;
34672
- }
34997
+ const nextFocusedPointIndex = state.coords.length > 1 ? state.coords.length - 2 : null;
34673
34998
  return {
34674
34999
  ...state,
34675
35000
  coords: state.coords.filter((_, i) => i !== action.index),
34676
35001
  focusedPointIndex: nextFocusedPointIndex,
34677
- showRemovePointButton: false
35002
+ showRemovePointButton: nextFocusedPointIndex !== null ? true : false
34678
35003
  };
34679
35004
  }
34680
35005
  const getDeltaVertex = (maxMoves, minMoves, delta) => {
@@ -34700,7 +35025,7 @@ const getChange = (coords, delta, constraintOpts) => {
34700
35025
  return [dx, dy];
34701
35026
  };
34702
35027
  // Less than or approximately equal
34703
- function leq$1(a, b) {
35028
+ function leq(a, b) {
34704
35029
  return a < b || perseusCore.approximateEqual(a, b);
34705
35030
  }
34706
35031
  function boundAndSnapToGrid(point, _ref) {
@@ -34851,7 +35176,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
34851
35176
  });
34852
35177
 
34853
35178
  // Gets the radian angles between the coords and maps them to degrees
34854
- const angles = angleMeasures$1(coordsCopy).map(angle => angle * 180 / Math.PI);
35179
+ const angles = angleMeasures(coordsCopy).map(angle => angle * 180 / Math.PI);
34855
35180
 
34856
35181
  // Gets the relative index of a point
34857
35182
  const rel = j => {
@@ -34871,7 +35196,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
34871
35196
 
34872
35197
  // Avoid degenerate triangles
34873
35198
  if (innerAngles.some(function (angle) {
34874
- return leq$1(angle, 1);
35199
+ return leq(angle, 1);
34875
35200
  })) {
34876
35201
  return startingPoint;
34877
35202
  }
@@ -34885,7 +35210,7 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
34885
35210
  // we want to add the inner angle to the outer angle, and if it's on the right,
34886
35211
  // we want to subtract the inner angle from the outer angle. The angle solved
34887
35212
  // for is then used in the polar function to determine the new point.
34888
- const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
35213
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34889
35214
 
34890
35215
  // Solve for side by using the law of sines
34891
35216
  const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
@@ -34933,13 +35258,13 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
34933
35258
  });
34934
35259
 
34935
35260
  // Avoid degenerate triangles
34936
- 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])) {
35261
+ if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
34937
35262
  return startingPoint;
34938
35263
  }
34939
35264
 
34940
35265
  // Solve for angle by using the law of cosines
34941
35266
  // Angle at the first vertex of the polygon
34942
- const innerAngle = lawOfCosines$1(sides[0], sides[2], sides[1]);
35267
+ const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
34943
35268
 
34944
35269
  // Angle at the second vertex of the polygon
34945
35270
  const outerAngle = getAngleFromVertex(coordsCopy[rel(1)], coordsCopy[rel(-1)]);
@@ -34950,7 +35275,7 @@ function boundAndSnapToSides(destinationPoint, _ref7, index) {
34950
35275
  // we want to add the inner angle to the outer angle, and if it's on the right,
34951
35276
  // we want to subtract the inner angle from the outer angle. The angle solved
34952
35277
  // for is then used in the polar function to determine the new point.
34953
- const onLeft = sign$1(ccw$1(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
35278
+ const onLeft = sign(ccw(coordsCopy[rel(-1)], coordsCopy[rel(1)], coordsCopy[index])) === 1;
34954
35279
 
34955
35280
  // Uses the length of the first side of the polygon (radial coordinate)
34956
35281
  // and the angle between the first and second sides of the
@@ -35088,19 +35413,11 @@ const {
35088
35413
  getClockwiseAngle
35089
35414
  } = kmath.angles;
35090
35415
  const {
35091
- angleMeasures,
35092
- ccw,
35093
- collinear,
35094
35416
  getLineEquation,
35095
35417
  getLineIntersection,
35096
- intersects,
35097
- lawOfCosines,
35098
35418
  magnitude,
35099
- rotate,
35100
- sign,
35101
35419
  vector
35102
35420
  } = kmath.geometry;
35103
- const TRASH_ICON_URI = "https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
35104
35421
  const defaultBackgroundImage = {
35105
35422
  url: null
35106
35423
  };
@@ -35109,31 +35426,9 @@ const UNLIMITED = "unlimited";
35109
35426
  // Sample background image:
35110
35427
  // https://ka-perseus-graphie.s3.amazonaws.com/29c1b0fcd17fe63df0f148fe357044d5d5c7d0bb.png
35111
35428
 
35112
- // default to defaultValue if actual is null or undefined
35113
- function defaultVal(actual, defaultValue) {
35114
- return actual == null ? defaultValue : actual;
35115
- }
35116
-
35117
- // Less than or approximately equal
35118
- function leq(a, b) {
35119
- return a < b || perseusCore.approximateEqual(a, b);
35120
- }
35121
- function capitalize(str) {
35122
- return str.replace(/(?:^|-)(.)/g, function (match, letter) {
35123
- return letter.toUpperCase();
35124
- });
35125
- }
35126
35429
  function numSteps(range, step) {
35127
35430
  return Math.floor((range[1] - range[0]) / step);
35128
35431
  }
35129
- const _getShouldShowInstructions = props => {
35130
- return _isClickToAddPoints(props) && (
35131
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35132
- props.graph.coords == null || props.graph.coords.length === 0);
35133
- };
35134
- const _isClickToAddPoints = props => {
35135
- return props.graph.type === "point" && props.graph.numPoints === UNLIMITED || props.graph.type === "polygon" && props.graph.numSides === UNLIMITED;
35136
- };
35137
35432
  const makeInvalidTypeError = (functionName, graphType) => {
35138
35433
  return new perseusCore.PerseusError(`${functionName} called but current graph type is not a '${graphType}'`, perseusCore.Errors.NotAllowed, {
35139
35434
  metadata: {
@@ -35174,1202 +35469,7 @@ function getQuadraticCoefficients(coords) {
35174
35469
  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;
35175
35470
  return [a, b, c];
35176
35471
  }
35177
-
35178
- // (LEMS-2190): Move the Mafs Angle Graph coordinate reversal logic in interactive-graph-state.ts
35179
- // to this file when we remove the legacy graph. This logic allows us to support bi-directional angles
35180
- // for the new (non-reflexive) Mafs graphs, while maintaining the same scoring behaviour as the legacy graph.
35181
- // Once the legacy graph is removed, we should move this logic directly into the validate function below.
35182
- class LegacyInteractiveGraph extends React__namespace.Component {
35183
- static contextType = PerseusI18nContext;
35184
- angle;
35185
- circle;
35186
- graphie;
35187
- horizHairline;
35188
- line;
35189
- lines;
35190
- parabola;
35191
- pointA;
35192
- pointB;
35193
- pointC;
35194
- // @ts-expect-error - TS2564 - Property 'points' has no initializer and is not definitely assigned in the constructor.
35195
- points;
35196
- polygon;
35197
- // @ts-expect-error - TS2564 - Property 'shouldResetGraphie' has no initializer and is not definitely assigned in the constructor.
35198
- shouldResetGraphie;
35199
- sinusoid;
35200
- trashCan;
35201
- vertHairline;
35202
- static defaultProps = {
35203
- labels: ["x", "y"],
35204
- range: [[-10, 10], [-10, 10]],
35205
- step: [1, 1],
35206
- backgroundImage: defaultBackgroundImage,
35207
- markings: "graph",
35208
- showTooltips: false,
35209
- showProtractor: false,
35210
- graph: {
35211
- type: "linear"
35212
- }
35213
- };
35214
- state = {
35215
- shouldShowInstructions: _getShouldShowInstructions(this.props)
35216
- };
35217
- componentDidMount() {
35218
- if (this.refs.graph) {
35219
- // eslint-disable-next-line react/no-string-refs
35220
- // @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
35221
- this.setGraphie(this.refs.graph.graphie());
35222
- }
35223
- }
35224
- UNSAFE_componentWillReceiveProps(nextProps) {
35225
- if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) {
35226
- this.shouldResetGraphie = true;
35227
- this.setState({
35228
- shouldShowInstructions: _getShouldShowInstructions(nextProps)
35229
- });
35230
- }
35231
- if (this.props.backgroundImage?.url !== nextProps.backgroundImage?.url || this.props.backgroundImage !== nextProps.backgroundImage || this.props.containerSizeClass !== nextProps.containerSizeClass) {
35232
- this.shouldResetGraphie = true;
35233
- }
35234
- }
35235
- componentDidUpdate(prevProps, prevState) {
35236
- // TODO(jeremy): This feels underspecified. For example, if the props
35237
- // change the coords of a segment graph, but not the numSegments, we'd
35238
- // not count that as an update.
35239
- const oldType = prevProps.graph.type;
35240
- const newType = this.props.graph.type;
35241
- 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)) {
35242
- this["remove" + capitalize(oldType) + "Controls"]();
35243
- this["add" + capitalize(newType) + "Controls"]();
35244
- }
35245
- if (this.shouldResetGraphie) {
35246
- this.resetGraphie();
35247
- }
35248
- }
35249
- _getShouldShowInstructions = props => {
35250
- props = props || this.props;
35251
- return this.isClickToAddPoints(props) && (
35252
- // @ts-expect-error - TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'. | TS2339 - Property 'coords' does not exist on type 'PerseusGraphType'.
35253
- props.graph.coords == null || props.graph.coords.length === 0);
35254
- };
35255
- setGraphie = newGraphie => {
35256
- this.graphie = newGraphie;
35257
- this.setupGraphie();
35258
- };
35259
- handleAddPointsMouseDown = coord => {
35260
- // This function should only be called when this.isClickToAddPoints()
35261
- // is true
35262
- if (!this.isClickToAddPoints()) {
35263
- throw new perseusCore.PerseusError("handleAddPointsClick should not be registered" + "when isClickToAddPoints() is false", perseusCore.Errors.NotAllowed);
35264
- }
35265
- if (!this.isCoordInTrash(coord)) {
35266
- let point;
35267
- if (this.props.graph.type === "point") {
35268
- point = this.createPointForPointsType(coord, this.points.length);
35269
- if (!point.constrain()) {
35270
- point.remove();
35271
- return;
35272
- }
35273
- this.points.push(point);
35274
-
35275
- // interactive2 allows us to grab the point
35276
- const idx = this.points.length - 1;
35277
- this.points[idx].grab(coord);
35278
- this.updateCoordsFromPoints();
35279
- } else if (this.props.graph.type === "polygon") {
35280
- if (this.polygon?.closed()) {
35281
- return;
35282
- }
35283
- point = this.createPointForPolygonType(coord, this.points.length);
35284
- this.points.push(point);
35285
- const idx = this.points.length - 1;
35286
- this.points[idx].grab(coord);
35287
-
35288
- // We don't call updateCoordsFromPoints for
35289
- // polygons, since the polygon won't be
35290
- // closed yet.
35291
- this.updatePolygon();
35292
- }
35293
- this.setState({
35294
- shouldShowInstructions: false
35295
- });
35296
- }
35297
- };
35298
- resetGraphie = () => {
35299
- this.shouldResetGraphie = false;
35300
- this.parabola = null;
35301
- this.sinusoid = null;
35302
- // eslint-disable-next-line react/no-string-refs
35303
- // @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
35304
- this.refs.graph?.reset();
35305
- };
35306
- setupGraphie = () => {
35307
- this.setTrashCanVisibility(0);
35308
- if (this.isClickToAddPoints()) {
35309
- this.setTrashCanVisibility(0.5);
35310
- }
35311
- if (this.props.apiOptions.isMobile) {
35312
- this.horizHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35313
- normalStyle: {
35314
- strokeWidth: 1
35315
- }
35316
- });
35317
- this.horizHairline.attr({
35318
- stroke: KhanColors.INTERACTIVE
35319
- });
35320
- this.horizHairline?.hide();
35321
- this.vertHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
35322
- normalStyle: {
35323
- strokeWidth: 1
35324
- }
35325
- });
35326
- this.vertHairline.attr({
35327
- stroke: KhanColors.INTERACTIVE
35328
- });
35329
- this.vertHairline?.hide();
35330
- }
35331
- const type = this.props.graph.type;
35332
- this["add" + capitalize(type) + "Controls"]();
35333
- };
35334
- showHairlines = point => {
35335
- if (this.props.apiOptions.isMobile && this.props.markings !== "none") {
35336
- // Hairlines are already initialized when the graph is loaded, so
35337
- // here we just move them to the updated location and make them
35338
- // visible.
35339
- this.horizHairline?.moveTo([this.props.range[0][0], point[1]], [this.props.range[0][1], point[1]]);
35340
- this.horizHairline?.show();
35341
- this.vertHairline?.moveTo([point[0], this.props.range[1][0]], [point[0], this.props.range[1][1]]);
35342
- this.vertHairline?.show();
35343
- }
35344
- };
35345
- hideHairlines = () => {
35346
- if (this.props.apiOptions.isMobile) {
35347
- this.horizHairline?.hide();
35348
- this.vertHairline?.hide();
35349
- }
35350
- };
35351
- setTrashCanVisibility = opacity => {
35352
- const graphie = this.graphie;
35353
- if (kmath.number.equal(opacity, 0)) {
35354
- if (this.trashCan) {
35355
- this.trashCan.remove();
35356
- this.trashCan = null;
35357
- }
35358
- } else if (!this.props.apiOptions.isMobile) {
35359
- // Only if trash tooltips are not being used, we initialize the old
35360
- // trash can area.
35361
- if (!this.trashCan) {
35362
- this.trashCan = graphie?.raphael?.image(TRASH_ICON_URI, graphie.xpixels - 40, graphie.ypixels - 40, 40, 40);
35363
- }
35364
- this.trashCan?.attr({
35365
- opacity: opacity
35366
- });
35367
- }
35368
- };
35369
- isClickToAddPoints = props => {
35370
- props = props || this.props;
35371
- return _isClickToAddPoints(props);
35372
- };
35373
- areAngleGraphsEqual(prevGraph, currentGraph) {
35374
- return prevGraph.allowReflexAngles !== currentGraph.allowReflexAngles || prevGraph.angleOffsetDeg !== currentGraph.angleOffsetDeg || prevGraph.snapDegrees !== currentGraph.snapDegrees;
35375
- }
35376
- arePointGraphsEqual(prevGraph, currentGraph) {
35377
- return prevGraph.numPoints !== currentGraph.numPoints;
35378
- }
35379
- arePolygonGraphsEqual(prevGraph, currentGraph) {
35380
- return prevGraph.numSides !== currentGraph.numSides || prevGraph.showAngles !== currentGraph.showAngles || prevGraph.showSides !== currentGraph.showSides || prevGraph.snapTo !== currentGraph.snapTo;
35381
- }
35382
- areSegmentGraphsEqual(prevGraph, currentGraph) {
35383
- return prevGraph.numSegments !== currentGraph.numSegments;
35384
- }
35385
- _lineStroke = () => {
35386
- // This should probably use: this.props.apiOptions.isMobile
35387
- // @ts-expect-error - TS2339 - Property 'isMobile' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
35388
- return this.props.isMobile ? {
35389
- "stroke-width": 3
35390
- } : {};
35391
- };
35392
- addLine = type => {
35393
- const self = this;
35394
- const graphie = self.graphie;
35395
- const coords = InteractiveGraph.getLineCoords(self.props.graph, self.props);
35396
- const points = self.points = ___default["default"].map(coords, coord => {
35397
- return Interactive2.addMaybeMobileMovablePoint(this, {
35398
- coord: coord,
35399
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap()],
35400
- onMove: () => {
35401
- const graph = ___default["default"].extend({}, self.props.graph, {
35402
- coords: ___default["default"].invoke(points, "coord")
35403
- });
35404
- self.onChange({
35405
- graph: graph
35406
- });
35407
- }
35408
- });
35409
- });
35410
- const lineConfig = {
35411
- points: points,
35412
- static: true,
35413
- normalStyle: {
35414
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
35415
- ...this._lineStroke()
35416
- }
35417
- };
35418
- if (type === "line") {
35419
- // @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"; }; }'.
35420
- lineConfig.extendLine = true;
35421
- } else if (type === "ray") {
35422
- // @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"; }; }'.
35423
- lineConfig.extendRay = true;
35424
- }
35425
- self.line = Interactive2.addMovableLine(graphie, lineConfig);
35426
-
35427
- // A and B can't be in the same place
35428
- points[0].listen("constraints", "isLine", coord => {
35429
- return !kmath.point.equal(coord, points[1].coord());
35430
- });
35431
- points[1].listen("constraints", "isLine", coord => {
35432
- return !kmath.point.equal(coord, points[0].coord());
35433
- });
35434
- };
35435
- removeLine = () => {
35436
- ___default["default"].invoke(this.points, "remove");
35437
- this.line?.remove();
35438
- };
35439
- addNoneControls = () => {};
35440
- removeNoneControls = () => {};
35441
- addLinearControls = () => {
35442
- this.addLine("line");
35443
- };
35444
- removeLinearControls = () => {
35445
- this.removeLine();
35446
- };
35447
- addQuadraticControls = () => {
35448
- if (this.props.graph.type !== "quadratic") {
35449
- throw makeInvalidTypeError("addQuadraticControls", "quadratic");
35450
- }
35451
- let coords = this.props.graph.coords;
35452
- if (!coords) {
35453
- coords = InteractiveGraph.defaultQuadraticCoords(this.props);
35454
- }
35455
- const onMoveHandler = () => {
35456
- const graph = ___default["default"].extend({}, this.props.graph, {
35457
- // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'. | TS2531 - Object is possibly 'null'.
35458
- coords: [pointA.coord(), pointB.coord(), pointC.coord()]
35459
- });
35460
- this.onChange({
35461
- graph
35462
- });
35463
- this.updateQuadratic();
35464
- };
35465
-
35466
- // NOTE(jeresig): This code attempts to access the variable while it's
35467
- // still being defined, it should be refactored! The callbacks appear
35468
- // to be executed synchronously, which causes this issue.
35469
- let pointA = null;
35470
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35471
- coord: coords[0],
35472
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35473
- return !pointA ||
35474
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35475
- coord[0] !== pointB.coord()[0] &&
35476
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35477
- coord[0] !== pointC.coord()[0];
35478
- }],
35479
- onMove: onMoveHandler
35480
- });
35481
-
35482
- // NOTE(jeresig): This code attempts to access the variable while it's
35483
- // still being defined, it should be refactored! The callbacks appear
35484
- // to be executed synchronously, which causes this issue.
35485
- let pointB = null;
35486
- pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35487
- coord: coords[1],
35488
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35489
- return !pointB ||
35490
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35491
- coord[0] !== pointA.coord()[0] &&
35492
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35493
- coord[0] !== pointC.coord()[0];
35494
- }],
35495
- onMove: onMoveHandler
35496
- });
35497
-
35498
- // NOTE(jeresig): This code attempts to access the variable while it's
35499
- // still being defined, it should be refactored! The callbacks appear
35500
- // to be executed synchronously, which causes this issue.
35501
- let pointC = null;
35502
- pointC = this.pointC = Interactive2.addMaybeMobileMovablePoint(this, {
35503
- coord: coords[2],
35504
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35505
- return !pointC ||
35506
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35507
- coord[0] !== pointA.coord()[0] &&
35508
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35509
- coord[0] !== pointB.coord()[0];
35510
- }],
35511
- onMove: onMoveHandler
35512
- });
35513
- this.updateQuadratic();
35514
- };
35515
- updateQuadratic = () => {
35516
- const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(this.props);
35517
- if (!coeffs) {
35518
- return;
35519
- }
35520
-
35521
- // Extract coefficients the parabola
35522
- const a = coeffs[0];
35523
- const b = coeffs[1];
35524
- const c = coeffs[2];
35525
-
35526
- // Plot and style
35527
- if (this.parabola) {
35528
- const path = this.graphie?.svgParabolaPath(a, b, c);
35529
- this.parabola?.attr({
35530
- path: path
35531
- });
35532
- } else {
35533
- this.parabola = this.graphie?.parabola(a, b, c);
35534
- this.parabola?.attr({
35535
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
35536
- ...this._lineStroke()
35537
- });
35538
- this.parabola?.toBack();
35539
- }
35540
- };
35541
- removeQuadraticControls = () => {
35542
- this.pointA?.remove();
35543
- this.pointB?.remove();
35544
- this.pointC?.remove();
35545
- if (this.parabola) {
35546
- this.parabola.remove();
35547
- this.parabola = null;
35548
- }
35549
- };
35550
- addSinusoidControls = () => {
35551
- if (this.props.graph.type !== "sinusoid") {
35552
- throw makeInvalidTypeError("addSinusoidControls", "sinusoid");
35553
- }
35554
- let coords = this.props.graph.coords;
35555
- if (!coords) {
35556
- coords = InteractiveGraph.defaultSinusoidCoords(this.props);
35557
- }
35558
- const onMoveHandler = () => {
35559
- const graph = ___default["default"].extend({}, this.props.graph, {
35560
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35561
- coords: [pointA.coord(), pointB.coord()]
35562
- });
35563
- this.onChange({
35564
- graph: graph
35565
- });
35566
- this.updateSinusoid();
35567
- };
35568
-
35569
- // NOTE(jeresig): This code attempts to access the variable while it's
35570
- // still being defined, it should be refactored! The callbacks appear
35571
- // to be executed synchronously, which causes this issue.
35572
- let pointA = null;
35573
- pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
35574
- coord: coords[0],
35575
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35576
- return !pointA || coord[0] !== pointB.coord()[0];
35577
- }],
35578
- onMove: onMoveHandler
35579
- });
35580
- const pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
35581
- coord: coords[1],
35582
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35583
- // @ts-expect-error - TS2339 - Property 'coord' does not exist on type 'never'.
35584
- return !pointA || coord[0] !== pointA.coord()[0];
35585
- }],
35586
- onMove: onMoveHandler
35587
- });
35588
- this.updateSinusoid();
35589
- };
35590
- updateSinusoid = () => {
35591
- const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(this.props);
35592
- if (!coeffs) {
35593
- return;
35594
- }
35595
- const a = coeffs[0];
35596
- const b = coeffs[1];
35597
- const c = coeffs[2];
35598
- const d = coeffs[3];
35599
-
35600
- // Plot and style
35601
- if (this.sinusoid) {
35602
- const path = this.graphie?.svgSinusoidPath(a, b, c, d);
35603
- this.sinusoid?.attr({
35604
- path: path
35605
- });
35606
- } else {
35607
- this.sinusoid = this.graphie?.sinusoid(a, b, c, d);
35608
- this.sinusoid?.attr({
35609
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.DYNAMIC,
35610
- ...this._lineStroke()
35611
- });
35612
- this.sinusoid?.toBack();
35613
- }
35614
- };
35615
- removeSinusoidControls = () => {
35616
- this.pointA?.remove();
35617
- this.pointB?.remove();
35618
- if (this.sinusoid) {
35619
- this.sinusoid.remove();
35620
- this.sinusoid = null;
35621
- }
35622
- };
35623
- addCircleControls = () => {
35624
- const graphie = this.graphie;
35625
- const minSnap = ___default["default"].min(graphie.snap);
35626
- const circle = this.circle = graphie?.addCircleGraph({
35627
- // @ts-expect-error - TS2339 - Property 'center' does not exist on type 'PerseusGraphType'.
35628
- center: this.props.graph.center || [0, 0],
35629
- // @ts-expect-error - TS2339 - Property 'radius' does not exist on type 'PerseusGraphType'.
35630
- radius: this.props.graph.radius || ___default["default"].min(this.props.step),
35631
- snapX: graphie.snap[0],
35632
- snapY: graphie.snap[1],
35633
- minRadius: minSnap,
35634
- snapRadius: minSnap
35635
- });
35636
- $__default["default"](circle).on("move", () => {
35637
- const graph = ___default["default"].extend({}, this.props.graph, {
35638
- center: circle.center,
35639
- radius: circle.radius
35640
- });
35641
- this.onChange({
35642
- graph: graph
35643
- });
35644
- });
35645
- };
35646
- removeCircleControls = () => {
35647
- this.circle?.remove();
35648
- };
35649
- addLinearSystemControls = () => {
35650
- const graphie = this.graphie;
35651
- const coords = InteractiveGraph.getLinearSystemCoords(this.props.graph, this.props);
35652
- const segmentColors = [KhanColors.INTERACTIVE, KhanColors.GREEN];
35653
- const points = this.points = ___default["default"].map(coords, (segmentCoords, segmentIndex) => {
35654
- // NOTE(jeresig): This code attempts to access the variable
35655
- // while it's still being defined, it should be refactored!
35656
- // The callbacks appear to be executed synchronously, which
35657
- // causes this issue.
35658
- const segmentPoints = [];
35659
- for (let i = 0; i < segmentCoords.length; i += 1) {
35660
- const coord = segmentCoords[i];
35661
- segmentPoints.push(Interactive2.addMaybeMobileMovablePoint(this, {
35662
- coord: coord,
35663
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
35664
- const otherSegment = segmentPoints[1 - i];
35665
- if (!otherSegment) {
35666
- // segment hasn't been defined yet
35667
- // because we're still creating them
35668
- return;
35669
- }
35670
- return !kmath.point.equal(coord, otherSegment.coord());
35671
- }],
35672
- onMove: () => {
35673
- const graph = ___default["default"].extend({}, this.props.graph, {
35674
- coords: ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "coord"))
35675
- });
35676
- this.onChange({
35677
- graph: graph
35678
- });
35679
- },
35680
- normalStyle: {
35681
- fill: segmentColors[segmentIndex]
35682
- },
35683
- highlightStyle: {
35684
- fill: segmentColors[segmentIndex]
35685
- }
35686
- }));
35687
- }
35688
- return segmentPoints;
35689
- });
35690
- this.lines = ___default["default"].map(points, (segmentPoints, segmentIndex) => {
35691
- return Interactive2.addMovableLine(graphie, {
35692
- points: segmentPoints,
35693
- static: true,
35694
- extendLine: true,
35695
- normalStyle: {
35696
- stroke: segmentColors[segmentIndex]
35697
- }
35698
- });
35699
- });
35700
- };
35701
- removeLinearSystemControls = () => {
35702
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
35703
- ___default["default"].invoke(this.lines, "remove");
35704
- ___default["default"].map(this.points, segment => ___default["default"].invoke(segment, "remove"));
35705
- };
35706
- isCoordInTrash = coord => {
35707
- if (this.props.apiOptions.isMobile) {
35708
- return false;
35709
- }
35710
- const graphie = this.graphie;
35711
- const screenPoint = graphie?.scalePoint(coord);
35712
- return screenPoint[0] >= graphie.xpixels - 40 && screenPoint[1] >= graphie.ypixels - 40;
35713
- };
35714
- createPointForPointsType = (coord, i) => {
35715
- const self = this;
35716
- const remove = () => {
35717
- self.points = ___default["default"].filter(self.points, function (pt) {
35718
- return pt !== point;
35719
- });
35720
- // update the correct answer box
35721
- self.updateCoordsFromPoints();
35722
-
35723
- // remove this movablePoint from graphie.
35724
- // we wait to do this until we're not inside of
35725
- // said point's onMoveEnd method so its state is
35726
- // consistent throughout this method call
35727
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35728
- // @ts-expect-error - TS2531 - Object is possibly 'null'.
35729
- setTimeout(point.remove.bind(point), 0); // eslint-disable-line no-restricted-syntax
35730
- };
35731
-
35732
- // NOTE(jeresig): This code attempts to access the variable while it's
35733
- // still being defined, it should be refactored! The callbacks appear
35734
- // to be executed synchronously, which causes this issue.
35735
- let point = null;
35736
- point = Interactive2.addMaybeMobileMovablePoint(this, {
35737
- coord: coord,
35738
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), function (coord) {
35739
- // TODO(jack): There ought to be a
35740
- // MovablePoint.constraints.avoid
35741
- // default that lets you do things like this
35742
- return ___default["default"].all(self.points, function (pt) {
35743
- return point === pt || !kmath.point.equal(coord, pt.coord());
35744
- });
35745
- }],
35746
- onMoveStart: function () {
35747
- if (self.isClickToAddPoints()) {
35748
- self.setTrashCanVisibility(1);
35749
- }
35750
- },
35751
- onMove: self.updateCoordsFromPoints,
35752
- onMoveEnd: function (coord) {
35753
- if (self.isClickToAddPoints()) {
35754
- if (self.isCoordInTrash(coord)) {
35755
- remove();
35756
- }
35757
- // In case we mouseup'd off the graphie and that
35758
- // stopped the move (in which case, we might not
35759
- // be in isCoordInTrash()
35760
- self.setTrashCanVisibility(0.5);
35761
- }
35762
- },
35763
- ...(this.props.apiOptions.isMobile && self.isClickToAddPoints() ? {
35764
- onRemove: remove
35765
- } : {})
35766
- });
35767
- return point;
35768
- };
35769
- removePoint = point => {
35770
- let index = null;
35771
- this.points = ___default["default"].filter(this.points, function (pt, i) {
35772
- if (pt === point) {
35773
- // @ts-expect-error - TS2322 - Type 'number' is not assignable to type 'null'.
35774
- index = i;
35775
- return false;
35776
- }
35777
- return true;
35778
- });
35779
- return index;
35780
- };
35781
- createPointForPolygonType = (coord, i) => {
35782
- if (this.props.graph.type !== "polygon") {
35783
- throw makeInvalidTypeError("createPointForPolygonType", "polygon");
35784
- }
35785
-
35786
- // TODO(alex): check against "grid" instead, use constants
35787
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
35788
-
35789
- // Index relative to current point -> absolute index
35790
- // NOTE: This does not work when isClickToAddPoints() == true,
35791
- // as `i` can be changed by dragging a point to the trash
35792
- // Currently this function is only called when !isClickToAddPoints()
35793
- const rel = j => {
35794
- return (i + j + this.points.length) % this.points.length;
35795
- };
35796
- const remove = () => {
35797
- // remove this point from points
35798
- const index = this.removePoint(point);
35799
- if (this.polygon?.closed()) {
35800
- // We should be checking if this.points is defined before rotating them.
35801
- // @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'.
35802
- this.points = rotate(this.points, index);
35803
- this.polygon?.update({
35804
- closed: false
35805
- });
35806
- }
35807
- this.updatePolygon();
35808
- // the polygon is now unclosed, so we need to
35809
- // remove any points props
35810
- this.clearCoords();
35811
-
35812
- // remove this movablePoint from graphie.
35813
- // wait to do this until we're not inside of
35814
- // said point's onMoveEnd method so state is
35815
- // consistent throughout the method call
35816
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35817
- // eslint-disable-next-line no-restricted-syntax
35818
- setTimeout(point.remove.bind(point), 0);
35819
- };
35820
- const setInitialMove = debounce__default["default"]((point, bool) => {
35821
- point.state.isInitialMove = bool;
35822
- }, 1000);
35823
- const onMoveEndHandler = coord => {
35824
- // If the user clicked and dragged a point over endpoint,
35825
- // join the them
35826
- const overlappedPoints = this.points.length > 1 && (point === this.points[0] && kmath.point.equal(coord, ___default["default"].last(this.points).coord()) || point === ___default["default"].last(this.points) && kmath.point.equal(coord, this.points[0].coord()));
35827
- if (this.isClickToAddPoints()) {
35828
- if (this.isCoordInTrash(coord)) {
35829
- remove();
35830
- } else if (overlappedPoints) {
35831
- const pointToRemove = this.points.pop();
35832
- if (this.points.length > 2) {
35833
- this.polygon?.update({
35834
- closed: true
35835
- });
35836
- this.updateCoordsFromPoints();
35837
- } else {
35838
- this.polygon?.update({
35839
- closed: false
35840
- });
35841
- this.clearCoords();
35842
- }
35843
- this.updatePolygon();
35844
- // remove this movablePoint from graphie.
35845
- // wait to do this until we're not inside of
35846
- // said point's onMoveEnd method so state is
35847
- // consistent throughout the method call
35848
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35849
- // eslint-disable-next-line no-restricted-syntax
35850
- setTimeout(pointToRemove.remove.bind(pointToRemove), 0);
35851
- } else {
35852
- // If the user clicked and dragged a point over any other
35853
- // existing point, fix shape
35854
- const shouldRemove = ___default["default"].any(this.points, function (pt) {
35855
- return pt !== point && kmath.point.equal(pt.coord(), coord);
35856
- });
35857
- if (shouldRemove) {
35858
- this.removePoint(point);
35859
- if (this.points.length < 3) {
35860
- this.polygon?.update({
35861
- closed: false
35862
- });
35863
- this.clearCoords();
35864
- } else if (this.polygon?.closed()) {
35865
- this.updateCoordsFromPoints();
35866
- }
35867
- this.updatePolygon();
35868
- // remove this movablePoint from graphie.
35869
- // wait to do this until we're not inside
35870
- // said point's onMoveEnd method so state
35871
- // is consistent throughout the method call
35872
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
35873
- // eslint-disable-next-line no-restricted-syntax
35874
- setTimeout(point.remove.bind(point), 0);
35875
- } else {
35876
- // If this was
35877
- // * a click on the first or last point
35878
- // * and not a deletion
35879
- // * and not a drag,
35880
- // * and not a creation of a new point
35881
- // (see !point.state.isInitialMove, below),
35882
- // * and our polygon is not closed,
35883
- // * and we can close it (we need at least the same
35884
- // number of points as coordinates asked for in the
35885
- // question),
35886
- // then close it
35887
- const isFirstOrLastPoint = point === ___default["default"].head(this.points) || point === ___default["default"].last(this.points);
35888
- const pointNotMoved = !point.hasMoved();
35889
- // NOTE (jsatk): Currently different on mobile versus
35890
- // destkop
35891
- const notANewPoint = !point.state.isInitialMove;
35892
- const polygonNotClosed = !this.polygon?.closed();
35893
- const allPointsPlotted = this.points.length > 2;
35894
- const canCloseUnclosedPolygon = isFirstOrLastPoint && pointNotMoved && notANewPoint && polygonNotClosed && allPointsPlotted;
35895
- if (canCloseUnclosedPolygon) {
35896
- this.polygon?.update({
35897
- closed: true
35898
- });
35899
- this.updatePolygon();
35900
-
35901
- // We finally have a closed polygon, so save our
35902
- // points to props
35903
- this.updateCoordsFromPoints();
35904
- }
35905
- }
35906
- }
35907
-
35908
- // In case we mouseup'd off the graphie and that
35909
- // stopped the move
35910
- this.setTrashCanVisibility(0.5);
35911
- }
35912
- setInitialMove(point, false);
35913
- };
35914
- const graphConstraint = coord => {
35915
- // These constraints are all relative to the other points, so if
35916
- // we're creating the initial points and haven't added any others
35917
- // to the graph, we can't enforce them.
35918
- if (this.points == null || this.points.length === 0) {
35919
- return true;
35920
- }
35921
- const coords = ___default["default"].invoke(this.points, "coord");
35922
- coords[i] = coord;
35923
-
35924
- // Check for invalid positioning, but only if we aren't adding
35925
- // points one click at a time, since those added points could
35926
- // have already violated these constraints
35927
- if (!this.isClickToAddPoints()) {
35928
- // Polygons can't have consecutive collinear points
35929
- 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)])) {
35930
- return false;
35931
- }
35932
- const segments = ___default["default"].zip(coords, rotate(coords));
35933
- if (this.points.length > 3) {
35934
- // Constrain to simple (non self-intersecting) polygon by
35935
- // testing whether adjacent segments intersect any others
35936
- for (let j = -1; j <= 0; j++) {
35937
- const segment = segments[rel(j)];
35938
- const others = ___default["default"].without(segments, segment, segments[rel(j - 1)], segments[rel(j + 1)]);
35939
- for (let k = 0; k < others.length; k++) {
35940
- const other = others[k];
35941
- // @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Line'.
35942
- if (intersects(segment, other)) {
35943
- return false;
35944
- }
35945
- }
35946
- }
35947
- }
35948
- }
35949
- if (
35950
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35951
- this.props.graph.snapTo === "angles" && this.points.length > 2) {
35952
- // Snap to whole degree interior angles
35953
-
35954
- const angles = ___default["default"].map(angleMeasures(coords), function (rad) {
35955
- return rad * 180 / Math.PI;
35956
- });
35957
- ___default["default"].each([-1, 1], function (j) {
35958
- angles[rel(j)] = Math.round(angles[rel(j)]);
35959
- });
35960
- const getAngle = function (a, vertex, b) {
35961
- const angle = GraphUtils.findAngleDeprecated(coords[rel(a)], coords[rel(b)], coords[rel(vertex)]);
35962
- return (angle + 360) % 360;
35963
- };
35964
- const innerAngles = [angles[rel(-1)] - getAngle(-2, -1, 1), angles[rel(1)] - getAngle(-1, 1, 2)];
35965
- innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
35966
-
35967
- // Avoid degenerate triangles
35968
- if (___default["default"].any(innerAngles, function (angle) {
35969
- return leq(angle, 1);
35970
- })) {
35971
- return false;
35972
- }
35973
- const knownSide = magnitude(
35974
- // @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
35975
- vector(coords[rel(-1)], coords[rel(1)]));
35976
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
35977
-
35978
- // Solve for side by using the law of sines
35979
- const side = Math.sin(innerAngles[1] * Math.PI / 180) / Math.sin(innerAngles[2] * Math.PI / 180) * knownSide;
35980
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
35981
- const offset = polar$2(side, outerAngle + (onLeft ? 1 : -1) * innerAngles[0]);
35982
- return this.graphie?.addPoints(coords[rel(-1)], offset);
35983
- }
35984
- // @ts-expect-error - TS2339 - Property 'snapTo' does not exist on type 'PerseusGraphType'.
35985
- if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
35986
- // Snap to whole unit side measures
35987
-
35988
- const sides = ___default["default"].map([[coords[rel(-1)], coords[i]], [coords[i], coords[rel(1)]], [coords[rel(-1)], coords[rel(1)]]], function (coords) {
35989
- // @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.
35990
- return magnitude(vector(...coords));
35991
- });
35992
- ___default["default"].each([0, 1], function (j) {
35993
- sides[j] = Math.round(sides[j]);
35994
- });
35995
-
35996
- // Avoid degenerate triangles
35997
- if (leq(sides[1] + sides[2], sides[0]) || leq(sides[0] + sides[2], sides[1]) || leq(sides[0] + sides[1], sides[2])) {
35998
- return false;
35999
- }
36000
-
36001
- // Solve for angle by using the law of cosines
36002
- const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
36003
- const outerAngle = GraphUtils.findAngleDeprecated(coords[rel(1)], coords[rel(-1)]);
36004
- const onLeft = sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
36005
- const offset = polar$2(sides[0], outerAngle + (onLeft ? 1 : -1) * innerAngle);
36006
- return this.graphie?.addPoints(coords[rel(-1)], offset);
36007
- }
36008
- // Snap to grid (already done)
36009
- return true;
36010
- };
36011
- const point = Interactive2.addMaybeMobileMovablePoint(this, {
36012
- coord: coord,
36013
- constraints: [Interactive2.MovablePoint.constraints.bound(), snapToGrid ? Interactive2.MovablePoint.constraints.snap() : null, graphConstraint],
36014
- onMoveStart: () => {
36015
- if (this.isClickToAddPoints()) {
36016
- this.setTrashCanVisibility(1);
36017
- }
36018
- },
36019
- onMove: () => {
36020
- if (this.polygon?.closed()) {
36021
- this.updateCoordsFromPoints();
36022
- }
36023
- },
36024
- onMoveEnd: onMoveEndHandler,
36025
- ...(this.props.apiOptions.isMobile && this.isClickToAddPoints() ? {
36026
- onRemove: remove
36027
- } : {})
36028
- });
36029
- point.state.isInitialMove = true;
36030
- return point;
36031
- };
36032
- updateCoordsFromPoints = () => {
36033
- const graph = ___default["default"].extend({}, this.props.graph, {
36034
- // Handle old movable points with .coord, or
36035
- // Interactive2.MovablePoint's with .coord()
36036
- coords: ___default["default"].map(this.points, function (point) {
36037
- return ___default["default"].result(point, "coord");
36038
- })
36039
- });
36040
- this.onChange({
36041
- graph: graph
36042
- });
36043
- };
36044
- clearCoords = () => {
36045
- const graph = ___default["default"].extend({}, this.props.graph, {
36046
- coords: null
36047
- });
36048
- this.onChange({
36049
- graph: graph
36050
- });
36051
- };
36052
- onChange = data => {
36053
- this.props.onChange(data);
36054
- this.props.trackInteraction();
36055
- };
36056
- addPointControls = () => {
36057
- if (this.props.graph.type !== "point") {
36058
- throw makeInvalidTypeError("addPointControls", "point");
36059
- }
36060
- const coords = InteractiveGraph.getPointCoords(this.props.graph, this.props);
36061
- // Clear out our old points so that newly added points don't
36062
- // "collide" with them and reposition when being added
36063
- // Without this, when added, each point checks whether it is on top
36064
- // of a point in this.points, which (a) shouldn't matter since
36065
- // we're clearing out this.points anyways, and (b) can cause problems
36066
- // if each of this.points is a MovablePoint instead of an
36067
- // Interactive2.MovablePoint, since one has a .coord and the other
36068
- // has .coord()
36069
- // TODO(jack): Figure out a better way to do this
36070
- this.points = [];
36071
- this.points = ___default["default"].map(coords, this.createPointForPointsType, this);
36072
- };
36073
- removePointControls = () => {
36074
- ___default["default"].invoke(this.points, "remove");
36075
- };
36076
- addSegmentControls = () => {
36077
- if (this.props.graph.type !== "segment") {
36078
- throw makeInvalidTypeError("addSegmentControls", "segment");
36079
- }
36080
- const self = this;
36081
- const graphie = this.graphie;
36082
- const coords = InteractiveGraph.getSegmentCoords(this.props.graph, this.props);
36083
- const createPoint = options => Interactive2.addMaybeMobileMovablePoint(this, options);
36084
- this.points = [];
36085
- this.lines = ___default["default"].map(coords, function (segment, i) {
36086
- const updateCoordProps = function () {
36087
- const graph = ___default["default"].extend({}, self.props.graph, {
36088
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36089
- coords: ___default["default"].invoke(self.lines, "coords")
36090
- });
36091
- self.onChange({
36092
- graph: graph
36093
- });
36094
- };
36095
-
36096
- // NOTE(jeresig): This code attempts to access the variable
36097
- // while it's still being defined, it should be refactored!
36098
- // The callbacks appear to be executed synchronously, which
36099
- // causes this issue.
36100
- const points = [];
36101
- for (let i = 0; i < segment.length; i += 1) {
36102
- const coord = segment[i];
36103
- points.push(createPoint({
36104
- coord: coord,
36105
- constraints: [Interactive2.MovablePoint.constraints.bound(), Interactive2.MovablePoint.constraints.snap(), coord => {
36106
- const otherPoint = points[1 - i];
36107
- if (!otherPoint) {
36108
- // point hasn't been defined yet
36109
- // because we're still creating them
36110
- // We return false as the points aren't
36111
- // going to be in the same position at
36112
- // this point.
36113
- return false;
36114
- }
36115
- return !kmath.point.equal(coord, otherPoint.coord());
36116
- }],
36117
- onMove: updateCoordProps
36118
- }));
36119
- }
36120
- self.points = self.points.concat(points);
36121
- const line = Interactive2.addMovableLine(graphie, {
36122
- points: points,
36123
- static: false,
36124
- constraints: [
36125
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36126
- Interactive2.MovableLine.constraints.bound(),
36127
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36128
- Interactive2.MovableLine.constraints.snap()],
36129
- onMove: [
36130
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => void'.
36131
- Interactive2.MovableLine.onMove.updatePoints, updateCoordProps],
36132
- normalStyle: {
36133
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36134
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36135
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36136
- ...this._lineStroke()
36137
- },
36138
- highlightStyle: {
36139
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36140
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTING,
36141
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36142
- ...this._lineStroke()
36143
- }
36144
- });
36145
- ___default["default"].invoke(points, "toFront");
36146
- return line;
36147
- }, this);
36148
- };
36149
- removeSegmentControls = () => {
36150
- ___default["default"].invoke(this.points, "remove");
36151
- // @ts-expect-error - TS2345 - Argument of type 'readonly (readonly Coord[])[] | null | undefined' is not assignable to parameter of type 'Collection<any>'.
36152
- ___default["default"].invoke(this.lines, "remove");
36153
- };
36154
- addRayControls = () => {
36155
- this.addLine("ray");
36156
- };
36157
- removeRayControls = () => {
36158
- this.removeLine();
36159
- };
36160
- addPolygonControls = () => {
36161
- this.polygon = null;
36162
- const coords = InteractiveGraph.getPolygonCoords(this.props.graph, this.props);
36163
- // Clear out our old points so that newly added points don't
36164
- // "collide", as in `addPointControls`
36165
- this.points = [];
36166
- this.points = ___default["default"].map(coords, this.createPointForPolygonType, this);
36167
- this.updatePolygon();
36168
- };
36169
- updatePolygon = () => {
36170
- if (this.props.graph.type !== "polygon") {
36171
- throw makeInvalidTypeError("updatePolygon", "polygon");
36172
- }
36173
- let closed;
36174
- if (this.polygon) {
36175
- closed = this.polygon.closed();
36176
- } else if (this.points.length >= 3) {
36177
- closed = true;
36178
- } else {
36179
- // There will only be fewer than 3 points in click-to-add-vertices
36180
- // mode, so we don't need to explicitly check for that here.
36181
- closed = false;
36182
- }
36183
- const graphie = this.graphie;
36184
- const n = this.points.length;
36185
-
36186
- // TODO(alex): check against "grid" instead, use constants
36187
- const snapToGrid = !___default["default"].contains(["angles", "sides"], this.props.graph.snapTo);
36188
- const angleLabels = ___default["default"].times(n, function (i) {
36189
- if (
36190
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36191
- !this.props.graph.showAngles || !closed && (i === 0 || i === n - 1)) {
36192
- return "";
36193
- }
36194
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36195
- if (this.props.graph.snapTo === "angles") {
36196
- return "$deg0";
36197
- }
36198
- return "$deg1";
36199
- }, this);
36200
- const showRightAngleMarkers = ___default["default"].times(n, function (i) {
36201
- return closed || i !== 0 && i !== n - 1;
36202
- }, this);
36203
- const numArcs = ___default["default"].times(n, function (i) {
36204
- if (
36205
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36206
- this.props.graph.showAngles && (closed || i !== 0 && i !== n - 1)) {
36207
- return 1;
36208
- }
36209
- return 0;
36210
- }, this);
36211
- const sideLabels = ___default["default"].times(n, function (i) {
36212
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36213
- if (!this.props.graph.showSides || !closed && i === n - 1) {
36214
- return "";
36215
- }
36216
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36217
- if (this.props.graph.snapTo === "sides") {
36218
- return "$len0";
36219
- }
36220
- return "$len1";
36221
- }, this);
36222
- if (this.polygon == null) {
36223
- const self = this;
36224
- self.polygon = Interactive2.addMovablePolygon(graphie, {
36225
- constraints: [
36226
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36227
- Interactive2.MovablePolygon.constraints.bound(), snapToGrid ?
36228
- // @ts-expect-error - TS2339 - Property 'constraints' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36229
- Interactive2.MovablePolygon.constraints.snap() : null],
36230
- closed: closed,
36231
- points: self.points,
36232
- angleLabels: angleLabels,
36233
- showRightAngleMarkers: showRightAngleMarkers,
36234
- numArcs: numArcs,
36235
- sideLabels: sideLabels,
36236
- onMove: [
36237
- // @ts-expect-error - TS2339 - Property 'onMove' does not exist on type '(graphie: any, movable: any, options: any) => undefined'.
36238
- Interactive2.MovablePolygon.onMove.updatePoints, function () {
36239
- // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
36240
- if (this.closed()) {
36241
- self.updateCoordsFromPoints();
36242
- }
36243
- }],
36244
- normalStyle: {
36245
- stroke: this.props.apiOptions.isMobile ? KhanColors.BLUE_C : KhanColors.INTERACTIVE,
36246
- ...this._lineStroke()
36247
- }
36248
- });
36249
- } else {
36250
- // We only need to pass in the properties that might've changed
36251
- this.polygon.update({
36252
- closed: closed,
36253
- points: this.points,
36254
- angleLabels: angleLabels,
36255
- showRightAngleMarkers: showRightAngleMarkers,
36256
- numArcs: numArcs,
36257
- sideLabels: sideLabels
36258
- });
36259
- }
36260
- };
36261
- removePolygonControls = () => {
36262
- ___default["default"].invoke(this.points, "remove");
36263
- this.polygon?.remove();
36264
- };
36265
- addAngleControls = () => {
36266
- if (this.props.graph.type !== "angle") {
36267
- throw makeInvalidTypeError("addAngleControls", "angle");
36268
- }
36269
- const graph = this.props.graph;
36270
- const graphie = this.graphie;
36271
- const coords = InteractiveGraph.getAngleCoords(this.props.graph, this.props);
36272
-
36273
- // The vertex snaps to the grid, but the rays don't...
36274
- this.points = coords.map(function (coord, i) {
36275
- return graphie?.addMovablePoint(___default["default"].extend({
36276
- coord: coord,
36277
- normalStyle: {
36278
- stroke: KhanColors.INTERACTIVE,
36279
- fill: KhanColors.INTERACTIVE
36280
- }
36281
- }, i === 1 ? {
36282
- snapX: graphie.snap[0],
36283
- snapY: graphie.snap[1]
36284
- } : {}));
36285
- });
36286
-
36287
- // ...they snap to whole-degree angles from the vertex.
36288
- this.angle = graphie?.addMovableAngle({
36289
- points: this.points,
36290
- snapDegrees: graph.snapDegrees || 1,
36291
- snapOffsetDeg: graph.angleOffsetDeg || 0,
36292
- angleLabel: graph.showAngles ? "$deg0" : "",
36293
- pushOut: 2,
36294
- allowReflex: defaultVal(graph.allowReflexAngles, true)
36295
- });
36296
- $__default["default"](this.angle).on("move", () => {
36297
- this.onChange({
36298
- graph: {
36299
- ...graph,
36300
- coords: this.angle?.getClockwiseCoords()
36301
- }
36302
- });
36303
- });
36304
- };
36305
- removeAngleControls = () => {
36306
- ___default["default"].invoke(this.points, "remove");
36307
- this.angle?.remove();
36308
- };
36309
- toggleShowSides = () => {
36310
- if (this.props.graph.type !== "polygon") {
36311
- throw makeInvalidTypeError("toggleShowSides", "polygon");
36312
- }
36313
- const graph = ___default["default"].extend({}, this.props.graph, {
36314
- showSides: !this.props.graph.showSides
36315
- });
36316
- this.onChange({
36317
- graph: graph
36318
- });
36319
- };
36320
- getUserInput() {
36321
- return InteractiveGraph.getUserInputFromProps(this.props);
36322
- }
36323
- focus = $__default["default"].noop;
36324
- render() {
36325
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36326
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36327
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36328
- let instructions;
36329
- // isClickToAddPoints() only applies to points and polygons
36330
- if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
36331
- if (this.props.graph.type === "point") {
36332
- instructions = this.context.strings.addPoints;
36333
- } else if (this.props.graph.type === "polygon") {
36334
- instructions = this.context.strings.addVertices;
36335
- }
36336
- } else {
36337
- instructions = undefined;
36338
- }
36339
- let onMouseDown;
36340
- if (this.isClickToAddPoints()) {
36341
- onMouseDown = this.handleAddPointsMouseDown;
36342
- }
36343
- const isMobile = this.props.apiOptions.isMobile;
36344
- return /*#__PURE__*/React__namespace.createElement("div", {
36345
- className: "perseus-widget " + "perseus-widget-interactive-graph",
36346
- style: {
36347
- width: box[0],
36348
- height: box[1]
36349
- }
36350
- }, /*#__PURE__*/React__namespace.createElement(Graph, {
36351
- instructions: instructions
36352
- // eslint-disable-next-line react/no-string-refs
36353
- ,
36354
- ref: "graph",
36355
- box: box,
36356
- labels: this.props.labels,
36357
- range: this.props.range,
36358
- step: isMobile ? Util.constrainedTickStepsFromTickSteps(this.props.step, this.props.range) : this.props.step,
36359
- gridStep: gridStep,
36360
- snapStep: snapStep,
36361
- markings: this.props.markings,
36362
- backgroundImage: this.props.backgroundImage,
36363
- showProtractor: this.props.showProtractor,
36364
- onMouseDown: onMouseDown,
36365
- onGraphieUpdated: this.setGraphie,
36366
- setDrawingAreaAvailable: this.props.apiOptions.setDrawingAreaAvailable,
36367
- isMobile: isMobile
36368
- }));
36369
- }
36370
- }
36371
35472
  class InteractiveGraph extends React__namespace.Component {
36372
- legacyGraphRef = /*#__PURE__*/React__namespace.createRef();
36373
35473
  mafsRef = /*#__PURE__*/React__namespace.createRef();
36374
35474
  static defaultProps = {
36375
35475
  labels: ["x", "y"],
@@ -36387,34 +35487,23 @@ class InteractiveGraph extends React__namespace.Component {
36387
35487
  if (this.mafsRef.current?.getUserInput) {
36388
35488
  return this.mafsRef.current.getUserInput();
36389
35489
  }
36390
- if (this.legacyGraphRef.current?.getUserInput) {
36391
- return this.legacyGraphRef.current.getUserInput();
36392
- }
36393
35490
  throw new perseusCore.PerseusError("Cannot getUserInput from a graph that has never rendered", perseusCore.Errors.NotAllowed);
36394
35491
  }
36395
35492
  getPromptJSON() {
36396
35493
  return getPromptJSON$b(this.props, this.getUserInput());
36397
35494
  }
36398
35495
  render() {
36399
- // Mafs shim
36400
- const mafsFlags = this.props.apiOptions?.flags?.["mafs"];
36401
- if (shouldUseMafs(mafsFlags, this.props.graph)) {
36402
- const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
36403
- const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
36404
- const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
36405
- return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
36406
- flags: this.props.apiOptions?.flags,
36407
- ref: this.mafsRef,
36408
- gridStep: gridStep,
36409
- snapStep: snapStep,
36410
- box: box,
36411
- showTooltips: !!this.props.showTooltips,
36412
- readOnly: this.props.apiOptions?.readOnly
36413
- }));
36414
- }
36415
- return /*#__PURE__*/React__namespace.createElement(LegacyInteractiveGraph, _extends({
36416
- ref: this.legacyGraphRef
36417
- }, this.props));
35496
+ const box = getInteractiveBoxFromSizeClass(this.props.containerSizeClass);
35497
+ const gridStep = this.props.gridStep || Util.getGridStep(this.props.range, this.props.step, box[0]);
35498
+ const snapStep = this.props.snapStep || Util.snapStepFromGridStep(gridStep);
35499
+ return /*#__PURE__*/React__namespace.createElement(StatefulMafsGraph, _extends({}, this.props, {
35500
+ ref: this.mafsRef,
35501
+ gridStep: gridStep,
35502
+ snapStep: snapStep,
35503
+ box: box,
35504
+ showTooltips: !!this.props.showTooltips,
35505
+ readOnly: this.props.apiOptions?.readOnly
35506
+ }));
36418
35507
  }
36419
35508
 
36420
35509
  /**
@@ -36591,8 +35680,32 @@ class InteractiveGraph extends React__namespace.Component {
36591
35680
  }
36592
35681
  static getEquationString(props) {
36593
35682
  const type = props.graph.type;
36594
- const funcName = "get" + capitalize(type) + "EquationString";
36595
- return InteractiveGraph[funcName](props);
35683
+ switch (type) {
35684
+ case "none":
35685
+ return InteractiveGraph.getNoneEquationString();
35686
+ case "linear":
35687
+ return InteractiveGraph.getLinearEquationString(props);
35688
+ case "quadratic":
35689
+ return InteractiveGraph.getQuadraticEquationString(props);
35690
+ case "sinusoid":
35691
+ return InteractiveGraph.getSinusoidEquationString(props);
35692
+ case "circle":
35693
+ return InteractiveGraph.getCircleEquationString(props);
35694
+ case "linear-system":
35695
+ return InteractiveGraph.getLinearSystemEquationString(props);
35696
+ case "point":
35697
+ return InteractiveGraph.getPointEquationString(props);
35698
+ case "segment":
35699
+ return InteractiveGraph.getSegmentEquationString(props);
35700
+ case "ray":
35701
+ return InteractiveGraph.getRayEquationString(props);
35702
+ case "polygon":
35703
+ return InteractiveGraph.getPolygonEquationString(props);
35704
+ case "angle":
35705
+ return InteractiveGraph.getAngleEquationString(props);
35706
+ default:
35707
+ throw new wonderStuffCore.UnreachableCaseError(type);
35708
+ }
36596
35709
  }
36597
35710
  static pointsFromNormalized(props, coordsList, noSnap) {
36598
35711
  // @ts-expect-error - TS2322 - Type 'number[][]' is not assignable to type 'readonly Coord[]'.
@@ -36730,28 +35843,6 @@ class InteractiveGraph extends React__namespace.Component {
36730
35843
  }
36731
35844
  }
36732
35845
 
36733
- // exported for testing
36734
- function shouldUseMafs(mafsFlags, graph) {
36735
- if (typeof mafsFlags === "boolean" || typeof mafsFlags === "undefined") {
36736
- return false;
36737
- }
36738
- switch (graph.type) {
36739
- case "none":
36740
- return true;
36741
- case "point":
36742
- if (graph.numPoints === UNLIMITED) {
36743
- return Boolean(mafsFlags["unlimited-point"]);
36744
- }
36745
- return Boolean(mafsFlags["point"]);
36746
- case "polygon":
36747
- if (graph.numSides === UNLIMITED) {
36748
- return Boolean(mafsFlags["unlimited-polygon"]);
36749
- }
36750
- return Boolean(mafsFlags["polygon"]);
36751
- default:
36752
- return Boolean(mafsFlags[graph.type]);
36753
- }
36754
- }
36755
35846
  // We don't need to change any of the original props for static mode
36756
35847
  const staticTransform$3 = ___default["default"].identity;
36757
35848
  var InteractiveGraph$1 = {
@@ -37394,10 +36485,15 @@ class LabelImage extends React__namespace.Component {
37394
36485
  });
37395
36486
  }
37396
36487
  activateMarker(index, opened) {
36488
+ // TODO(LEMS-2830): Remove analytics event in LEMS-2830 in favor of ti below.
37397
36489
  this.props.analytics?.onAnalyticsEvent({
37398
36490
  type: "perseus:label-image:marker-interacted-with",
37399
36491
  payload: null
37400
36492
  });
36493
+ this.props.analytics?.onAnalyticsEvent({
36494
+ type: "perseus:label-image:marker-interacted-with:ti",
36495
+ payload: null
36496
+ });
37401
36497
  const {
37402
36498
  activeMarkerIndex
37403
36499
  } = this.state;
@@ -37541,10 +36637,15 @@ class LabelImage extends React__namespace.Component {
37541
36637
  })),
37542
36638
  multipleSelect: this.props.multipleAnswers,
37543
36639
  onChange: selection => {
36640
+ // TODO(LEMS-2829): Remove analytics event in LEMS-2829 in favor of ti below.
37544
36641
  this.props.analytics?.onAnalyticsEvent({
37545
36642
  type: "perseus:label-image:choiced-interacted-with",
37546
36643
  payload: null
37547
36644
  });
36645
+ this.props.analytics?.onAnalyticsEvent({
36646
+ type: "perseus:label-image:choiced-interacted-with:ti",
36647
+ payload: null
36648
+ });
37548
36649
  this.handleAnswerChoicesChangeForMarker(index, selection);
37549
36650
  },
37550
36651
  onToggle: opened => this.activateMarker(index, opened)
@@ -37649,10 +36750,15 @@ class LabelImage extends React__namespace.Component {
37649
36750
  })), this.renderMarkers()), /*#__PURE__*/React__namespace.createElement(HideAnswersToggle, {
37650
36751
  areAnswersHidden: this.state.hideAnswers,
37651
36752
  onChange: hideAnswers => {
36753
+ // TODO(LEMS-2831): Remove analytics event in LEMS-2831 in favor of ti below.
37652
36754
  this.props.analytics?.onAnalyticsEvent({
37653
36755
  type: "perseus:label-image:toggle-answers-hidden",
37654
36756
  payload: null
37655
36757
  });
36758
+ this.props.analytics?.onAnalyticsEvent({
36759
+ type: "perseus:label-image:toggle-answers-hidden:ti",
36760
+ payload: null
36761
+ });
37656
36762
  this.setState({
37657
36763
  hideAnswers
37658
36764
  });
@@ -37715,8 +36821,7 @@ var LabelImage$1 = {
37715
36821
  displayName: "Label Image",
37716
36822
  widget: LabelImageWithDependencies,
37717
36823
  accessible: true,
37718
- isLintable: true,
37719
- getPublicWidgetOptions: perseusCore.getLabelImagePublicWidgetOptions
36824
+ isLintable: true
37720
36825
  };
37721
36826
 
37722
36827
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -37975,20 +37080,24 @@ class Draggable extends React__namespace.Component {
37975
37080
  if (this.props.margin != null) {
37976
37081
  style.margin = this.props.margin;
37977
37082
  }
37978
- return /*#__PURE__*/React__namespace.createElement("li", {
37979
- className: className,
37980
- style: style,
37981
- onMouseDown: this.onMouseDown,
37982
- onTouchStart: this.onMouseDown,
37983
- onTouchMove: this.onMouseMove,
37984
- onTouchEnd: this.onMouseUp,
37985
- onTouchCancel: this.onMouseUp
37986
- }, /*#__PURE__*/React__namespace.createElement(Renderer, {
37987
- content: this.props.content,
37988
- linterContext: PerseusLinter__namespace.pushContextStack(this.props.linterContext, "draggable"),
37989
- onRender: this.props.onRender,
37990
- strings: this.context.strings
37991
- }));
37083
+ return (
37084
+ /*#__PURE__*/
37085
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- TODO(LEMS-2871): Address a11y error
37086
+ React__namespace.createElement("li", {
37087
+ className: className,
37088
+ style: style,
37089
+ onMouseDown: this.onMouseDown,
37090
+ onTouchStart: this.onMouseDown,
37091
+ onTouchMove: this.onMouseMove,
37092
+ onTouchEnd: this.onMouseUp,
37093
+ onTouchCancel: this.onMouseUp
37094
+ }, /*#__PURE__*/React__namespace.createElement(Renderer, {
37095
+ content: this.props.content,
37096
+ linterContext: PerseusLinter__namespace.pushContextStack(this.props.linterContext, "draggable"),
37097
+ onRender: this.props.onRender,
37098
+ strings: this.context.strings
37099
+ }))
37100
+ );
37992
37101
  }
37993
37102
  }
37994
37103
  class Sortable extends React__namespace.Component {
@@ -40624,8 +39733,7 @@ var NumberLine$1 = {
40624
39733
  displayName: "Number line",
40625
39734
  widget: NumberLine,
40626
39735
  transform: numberLineTransform,
40627
- staticTransform: staticTransform$1,
40628
- getPublicWidgetOptions: perseusCore.getNumberLinePublicWidgetOptions
39736
+ staticTransform: staticTransform$1
40629
39737
  };
40630
39738
 
40631
39739
  const getPromptJSON$5 = (renderProps, userInput) => {
@@ -40792,19 +39900,23 @@ class Card extends React__namespace.Component {
40792
39900
  // Pull out the content to get rendered
40793
39901
  const rendererProps = ___default["default"].pick(this.props, "content");
40794
39902
  const onMouseDown = this.props.animating ? $__default["default"].noop : this.onMouseDown;
40795
- return /*#__PURE__*/React__namespace.createElement("div", {
40796
- className: "card-wrap",
40797
- style: style,
40798
- onMouseDown: onMouseDown,
40799
- onTouchStart: onMouseDown,
40800
- onTouchEnd: this.onMouseUp,
40801
- onTouchCancel: this.onMouseUp
40802
- }, /*#__PURE__*/React__namespace.createElement("div", {
40803
- className: className.join(" ")
40804
- }, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, rendererProps, {
40805
- linterContext: this.props.linterContext,
40806
- strings: this.context.strings
40807
- }))));
39903
+ return (
39904
+ /*#__PURE__*/
39905
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO(LEMS-2871): Address a11y error
39906
+ React__namespace.createElement("div", {
39907
+ className: "card-wrap",
39908
+ style: style,
39909
+ onMouseDown: onMouseDown,
39910
+ onTouchStart: onMouseDown,
39911
+ onTouchEnd: this.onMouseUp,
39912
+ onTouchCancel: this.onMouseUp
39913
+ }, /*#__PURE__*/React__namespace.createElement("div", {
39914
+ className: className.join(" ")
39915
+ }, /*#__PURE__*/React__namespace.createElement(Renderer, _extends({}, rendererProps, {
39916
+ linterContext: this.props.linterContext,
39917
+ strings: this.context.strings
39918
+ }))))
39919
+ );
40808
39920
  }
40809
39921
  }
40810
39922
  class Orderer extends React__namespace.Component {
@@ -41176,8 +40288,7 @@ var Orderer$1 = {
41176
40288
  displayName: "Orderer",
41177
40289
  hidden: true,
41178
40290
  widget: Orderer,
41179
- isLintable: true,
41180
- getPublicWidgetOptions: perseusCore.getOrdererPublicWidgetOptions
40291
+ isLintable: true
41181
40292
  };
41182
40293
 
41183
40294
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
@@ -42403,8 +41514,7 @@ var Sorter$1 = {
42403
41514
  name: "sorter",
42404
41515
  displayName: "Sorter",
42405
41516
  widget: Sorter,
42406
- isLintable: true,
42407
- getPublicWidgetOptions: perseusCore.getSorterPublicWidgetOptions
41517
+ isLintable: true
42408
41518
  };
42409
41519
 
42410
41520
  const {
@@ -42801,7 +41911,7 @@ const init = function () {
42801
41911
 
42802
41912
  // This file is processed by a Rollup plugin (replace) to inject the production
42803
41913
  const libName = "@khanacademy/perseus";
42804
- const libVersion = "53.0.0";
41914
+ const libVersion = "54.0.0";
42805
41915
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
42806
41916
 
42807
41917
  const apiVersion = {
@@ -44295,6 +43405,17 @@ function getWidgetTypeByWidgetId(WidgetId, widgetMap) {
44295
43405
  const widget = widgetMap[WidgetId];
44296
43406
  return widget?.type ?? null;
44297
43407
  }
43408
+ function getWidgetSubTypeByWidgetId(widgetId, widgetMap) {
43409
+ const widget = widgetMap[widgetId];
43410
+ const widgetType = widget?.type ?? null;
43411
+ switch (widgetType) {
43412
+ case "interactive-graph":
43413
+ const graph = widget.options.graph;
43414
+ return graph?.type ?? null;
43415
+ default:
43416
+ return null;
43417
+ }
43418
+ }
44298
43419
 
44299
43420
  /**
44300
43421
  * Does the content have a specific type of widget?
@@ -45173,6 +44294,7 @@ exports.getSegmentCoords = getSegmentCoords;
45173
44294
  exports.getSinusoidCoords = getSinusoidCoords;
45174
44295
  exports.getValidWidgetIds = getValidWidgetIds;
45175
44296
  exports.getWidgetFromWidgetMap = getWidgetFromWidgetMap;
44297
+ exports.getWidgetSubTypeByWidgetId = getWidgetSubTypeByWidgetId;
45176
44298
  exports.getWidgetTypeByWidgetId = getWidgetTypeByWidgetId;
45177
44299
  exports.getWidgetsFromWidgetMap = getWidgetsFromWidgetMap;
45178
44300
  exports.getWidgetsMapFromItemData = getWidgetsMapFromItemData;