@khanacademy/perseus 56.0.0 → 56.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/es/index.css CHANGED
@@ -4446,6 +4446,14 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4446
4446
  --mafs-green: #00a60e; /* WB color.green */
4447
4447
  --mafs-violet: #9059ff; /* WB color.purple */
4448
4448
  --mafs-yellow: #ffb100; /* WB color.gold */
4449
+ /* Note: offBlack50 is not the best for contrast. However, this color needs
4450
+ to be distinguishable from our regular interactive blue so it is clear that
4451
+ the element is not interactive, even though the visual elements (e.g.
4452
+ outlines, shadows) may indicate otherwise. When using a darker gray, it
4453
+ looks the same as our WB blue in grayscale. We had to make a call here, and
4454
+ we decided to make it lighter and more distinguishable even if it sacrifices
4455
+ color contrast. (Nisha 2025) */
4456
+ --static-gray: #909296 /* WB color.offBlack50 */;
4449
4457
 
4450
4458
  /* overridden on a per-point basis */
4451
4459
  --movable-point-color: var(--mafs-blue);
@@ -4595,7 +4603,6 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4595
4603
  }
4596
4604
 
4597
4605
  .MafsView .movable-circle .circle {
4598
- stroke: var(--mafs-blue);
4599
4606
  stroke-width: 2px;
4600
4607
  fill: transparent;
4601
4608
  }
@@ -4626,7 +4633,6 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4626
4633
  }
4627
4634
 
4628
4635
  .MafsView .movable-circle .movable-circle-handle {
4629
- fill: var(--mafs-blue); /* WB fadedOffBlack64 */
4630
4636
  stroke: #fff;
4631
4637
  stroke-width: 2px;
4632
4638
  }
@@ -4646,12 +4652,16 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4646
4652
  stroke: rgba(33, 36, 44, 0.32);
4647
4653
  }
4648
4654
 
4649
- .MafsView .angle-arc,
4650
- .MafsView .arc-right-angle {
4655
+ .MafsView .angle-arc-interactive {
4651
4656
  stroke: var(--mafs-blue);
4652
4657
  stroke-width: 0.1px;
4653
4658
  }
4654
4659
 
4660
+ .MafsView .angle-arc-static {
4661
+ stroke: var(--static-gray);
4662
+ stroke-width: 0.1px;
4663
+ }
4664
+
4655
4665
  .MafsView .axis-tick-label {
4656
4666
  /* There is a bug in the MathJax font for Safari, so we use KaTeX */
4657
4667
  font-family: "KaTeX_Main", "Mafs-MJXTEX", sans-serif;
package/dist/es/index.js CHANGED
@@ -27696,7 +27696,8 @@ const defaultGraphConfig = {
27696
27696
  width: 0,
27697
27697
  height: 0,
27698
27698
  labels: [],
27699
- disableKeyboardInteraction: false
27699
+ disableKeyboardInteraction: false,
27700
+ interactiveColor: "var(--mafs-blue)"
27700
27701
  };
27701
27702
  const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
27702
27703
  function useGraphConfig() {
@@ -27767,6 +27768,11 @@ function Arrowhead(props) {
27767
27768
  var _props$color;
27768
27769
  const [point] = useTransformVectorsToPixels(props.tip);
27769
27770
  return /*#__PURE__*/React.createElement("g", {
27771
+ // Use aria-hidden to hide the line from screen readers
27772
+ // so it doesn't read as "image" with no context.
27773
+ // The elements using this should have their own aria-labels,
27774
+ // so this is okay.
27775
+ "aria-hidden": true,
27770
27776
  className: "interactive-graph-arrowhead",
27771
27777
  transform: `translate(${point[X]} ${point[Y]}) rotate(${props.angle})`
27772
27778
  }, /*#__PURE__*/React.createElement("g", {
@@ -28931,6 +28937,11 @@ function SVGLine(props) {
28931
28937
  testId
28932
28938
  } = props;
28933
28939
  return /*#__PURE__*/React.createElement("line", {
28940
+ // Use aria-hidden to hide the line from screen readers
28941
+ // so it doesn't read as "image" with no context.
28942
+ // The elements using this should have their own aria-labels,
28943
+ // so this is okay.
28944
+ "aria-hidden": true,
28934
28945
  x1: start[X],
28935
28946
  y1: start[Y],
28936
28947
  x2: end[X],
@@ -28941,15 +28952,23 @@ function SVGLine(props) {
28941
28952
  });
28942
28953
  }
28943
28954
 
28955
+ /**
28956
+ * Used to visualize vectors for both interative elements
28957
+ * as well as locked figures within an Interactive Graph widget.
28958
+ */
28944
28959
  const {
28945
28960
  calculateAngleInDegrees: calculateAngleInDegrees$3
28946
28961
  } = angles;
28947
28962
  function Vector(props) {
28963
+ const {
28964
+ interactiveColor
28965
+ } = useGraphConfig();
28948
28966
  const {
28949
28967
  tail,
28950
28968
  tip,
28951
- color,
28952
- style
28969
+ color = interactiveColor,
28970
+ style,
28971
+ testId
28953
28972
  } = props;
28954
28973
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
28955
28974
  const direction = vec.sub(tipPx, tailPx);
@@ -28958,7 +28977,8 @@ function Vector(props) {
28958
28977
  style: {
28959
28978
  stroke: color,
28960
28979
  strokeWidth: 2
28961
- }
28980
+ },
28981
+ "data-testid": testId
28962
28982
  }, /*#__PURE__*/React.createElement(SVGLine, {
28963
28983
  start: tailPx,
28964
28984
  end: tipPx,
@@ -29253,9 +29273,17 @@ function getAngleFromPoints(points, i) {
29253
29273
  const a = vec.dist(point, pt1);
29254
29274
  const b = vec.dist(point, pt2);
29255
29275
  const c = vec.dist(pt1, pt2);
29276
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
29277
+
29278
+ // If the equation results in a number greater than 1 or less than -1.
29279
+ // Correct to ensure a valid angle.
29280
+ // This ensures we are not producing NaN results from Math.acos.
29281
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
29282
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
29283
+ }
29256
29284
 
29257
29285
  // Law of cosines
29258
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29286
+ const angle = Math.acos(lawOfCosinesRadicand);
29259
29287
  return angle;
29260
29288
  }
29261
29289
  function getSideLengthsFromPoints(points, i, isPolygonOpen) {
@@ -29779,9 +29807,17 @@ const PolygonAngle = ({
29779
29807
  const a = vec.dist(centerPoint, endPoints[0]);
29780
29808
  const b = vec.dist(centerPoint, endPoints[1]);
29781
29809
  const c = vec.dist(endPoints[0], endPoints[1]);
29810
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
29811
+
29812
+ // If the equation results in a number greater than 1 or less than -1.
29813
+ // Correct to ensure a valid angle.
29814
+ // This ensures we are not producing NaN results from Math.acos.
29815
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
29816
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
29817
+ }
29782
29818
 
29783
29819
  // Law of cosines
29784
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29820
+ const angle = Math.acos(lawOfCosinesRadicand);
29785
29821
  const y1 = centerY + (startY - centerY) / a * radius;
29786
29822
  const x2 = centerX + (endX - centerX) / b * radius;
29787
29823
  const x1 = centerX + (startX - centerX) / a * radius;
@@ -29897,6 +29933,12 @@ const Angle = ({
29897
29933
  // Calculate the text position based on the angle and whether we allow reflex angles
29898
29934
  // Let's try the angle bisector method to find the midpoint of the arc
29899
29935
  const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
29936
+
29937
+ // Determine the color style for the arc when interactive vs disabled
29938
+ const {
29939
+ disableKeyboardInteraction
29940
+ } = useGraphConfig();
29941
+ const arcClassName = disableKeyboardInteraction ? "angle-arc-static" : "angle-arc-interactive";
29900
29942
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
29901
29943
  id: "background",
29902
29944
  x: "-5%",
@@ -29913,10 +29955,10 @@ const Angle = ({
29913
29955
  start: [x1, y1],
29914
29956
  vertex: [x2, y2],
29915
29957
  end: [x3, y3],
29916
- className: "arc-right-angle"
29958
+ className: arcClassName
29917
29959
  }) : /*#__PURE__*/React.createElement(Arc, {
29918
29960
  arc: arc,
29919
- className: "angle-arc"
29961
+ className: arcClassName
29920
29962
  }), showAngles && /*#__PURE__*/React.createElement(TextLabel, {
29921
29963
  x: textX,
29922
29964
  y: textY,
@@ -29934,10 +29976,16 @@ const RightAngleSquare = ({
29934
29976
  end: [x3, y3],
29935
29977
  className
29936
29978
  }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29979
+ // Use aria-hidden to hide the line from screen readers
29980
+ // so it doesn't read as "image" with no context.
29981
+ // The elements using this should have their own aria-labels,
29982
+ // so this is okay.
29983
+ "aria-hidden": true,
29937
29984
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
29938
29985
  strokeWidth: 0.02,
29939
29986
  fill: "none",
29940
- className: className
29987
+ className: className,
29988
+ "data-testid": "angle-indicators__right-angle"
29941
29989
  }));
29942
29990
 
29943
29991
  // We're conditionally adding the class name here so that we can style the arc differently
@@ -29947,10 +29995,16 @@ const Arc = ({
29947
29995
  className
29948
29996
  }) => {
29949
29997
  return /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29998
+ // Use aria-hidden to hide the line from screen readers
29999
+ // so it doesn't read as "image" with no context.
30000
+ // The elements using this should have their own aria-labels,
30001
+ // so this is okay.
30002
+ "aria-hidden": true,
29950
30003
  d: arc,
29951
30004
  strokeWidth: 0.02,
29952
30005
  fill: "none",
29953
- className: className
30006
+ className: className,
30007
+ "data-testid": "angle-indicators__arc"
29954
30008
  }));
29955
30009
  };
29956
30010
  const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
@@ -30222,7 +30276,13 @@ function Hairlines(props) {
30222
30276
  const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
30223
30277
  const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
30224
30278
  const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
30225
- return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
30279
+ return /*#__PURE__*/React.createElement("g", {
30280
+ // Use aria-hidden to hide the line from screen readers
30281
+ // so it doesn't read as "image" with no context.
30282
+ // The elements using this should have their own aria-labels,
30283
+ // so this is okay.
30284
+ "aria-hidden": true
30285
+ }, /*#__PURE__*/React.createElement("line", {
30226
30286
  x1: verticalStartX,
30227
30287
  y1: y,
30228
30288
  x2: verticalEndX,
@@ -30248,11 +30308,12 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30248
30308
  var _Object$entries$find$, _Object$entries$find;
30249
30309
  const {
30250
30310
  markings,
30251
- showTooltips
30311
+ showTooltips,
30312
+ interactiveColor
30252
30313
  } = useGraphConfig();
30253
30314
  const {
30254
30315
  point,
30255
- color: color$1 = color.blue,
30316
+ color: color$1 = interactiveColor,
30256
30317
  dragging,
30257
30318
  focused,
30258
30319
  cursor,
@@ -30267,6 +30328,11 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30267
30328
  const [[x, y]] = useTransformVectorsToPixels(point);
30268
30329
  const showHairlines = (dragging || focused) && markings !== "none";
30269
30330
  const svgForPoint = /*#__PURE__*/React.createElement("g", {
30331
+ // Use aria-hidden to hide the line from screen readers
30332
+ // so it doesn't read as "image" with no context.
30333
+ // The elements using this should have their own aria-labels,
30334
+ // so this is okay.
30335
+ "aria-hidden": true,
30270
30336
  ref: hitboxRef,
30271
30337
  className: pointClasses,
30272
30338
  style: {
@@ -30367,9 +30433,22 @@ function useControlPoint(params) {
30367
30433
  x: srFormatNumber(point[X], locale),
30368
30434
  y: srFormatNumber(point[Y], locale)
30369
30435
  });
30436
+
30437
+ // Set the forwarded ref to the focusable handle element when it changes.
30370
30438
  useLayoutEffect(() => {
30371
30439
  setForwardedRef(forwardedRef, focusableHandleRef.current);
30372
30440
  }, [forwardedRef]);
30441
+
30442
+ // If the point is being dragged and is not focused, focus the focusable handle.
30443
+ useLayoutEffect(() => {
30444
+ if (dragging && !focused) {
30445
+ var _focusableHandleRef$c;
30446
+ // If the point is being dragged, focus the focusable handle so that
30447
+ // users can continue to interact with the point using the keyboard or buttons.
30448
+ // This particular focus call ensures that the focus ring and hairlines are visible.
30449
+ (_focusableHandleRef$c = focusableHandleRef.current) == null || _focusableHandleRef$c.focus();
30450
+ }
30451
+ }, [dragging, focused]);
30373
30452
  const focusableHandle = /*#__PURE__*/React.createElement("g", {
30374
30453
  "data-testid": "movable-point__focusable-handle",
30375
30454
  className: "movable-point__focusable-handle",
@@ -30391,9 +30470,9 @@ function useControlPoint(params) {
30391
30470
  const visiblePoint = /*#__PURE__*/React.createElement(MovablePointView, {
30392
30471
  cursor: cursor,
30393
30472
  onClick: () => {
30394
- var _focusableHandleRef$c;
30473
+ var _focusableHandleRef$c2;
30395
30474
  onClick();
30396
- (_focusableHandleRef$c = focusableHandleRef.current) == null || _focusableHandleRef$c.focus();
30475
+ (_focusableHandleRef$c2 = focusableHandleRef.current) == null || _focusableHandleRef$c2.focus();
30397
30476
  },
30398
30477
  point: point,
30399
30478
  dragging: dragging,
@@ -30431,7 +30510,6 @@ const MovableLine = props => {
30431
30510
  points: [start, end],
30432
30511
  ariaLabels,
30433
30512
  ariaDescribedBy,
30434
- color,
30435
30513
  extend,
30436
30514
  onMoveLine = () => {},
30437
30515
  onMovePoint = () => {}
@@ -30466,7 +30544,6 @@ const MovableLine = props => {
30466
30544
  ariaLive: ariaLives[0],
30467
30545
  point: start,
30468
30546
  sequenceNumber: 1,
30469
- color,
30470
30547
  onMove: p => {
30471
30548
  setAriaLives(["polite", "off", "off"]);
30472
30549
  onMovePoint(0, p);
@@ -30482,7 +30559,6 @@ const MovableLine = props => {
30482
30559
  ariaLive: ariaLives[1],
30483
30560
  point: end,
30484
30561
  sequenceNumber: 2,
30485
- color,
30486
30562
  onMove: p => {
30487
30563
  setAriaLives(["off", "polite", "off"]);
30488
30564
  onMovePoint(1, p);
@@ -30495,7 +30571,6 @@ const MovableLine = props => {
30495
30571
  ariaLive: ariaLives[2],
30496
30572
  start: start,
30497
30573
  end: end,
30498
- stroke: color,
30499
30574
  extend: extend,
30500
30575
  onMove: delta => {
30501
30576
  setAriaLives(["off", "off", "polite"]);
@@ -30504,7 +30579,6 @@ const MovableLine = props => {
30504
30579
  });
30505
30580
  return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle1, line, focusableHandle2, visiblePoint1, visiblePoint2);
30506
30581
  };
30507
- const defaultStroke = "var(--movable-line-stroke-color)";
30508
30582
  const Line$1 = props => {
30509
30583
  const {
30510
30584
  start,
@@ -30513,7 +30587,6 @@ const Line$1 = props => {
30513
30587
  ariaDescribedBy,
30514
30588
  ariaLive,
30515
30589
  extend,
30516
- stroke = defaultStroke,
30517
30590
  onMove
30518
30591
  } = props;
30519
30592
  const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
@@ -30521,7 +30594,8 @@ const Line$1 = props => {
30521
30594
  range,
30522
30595
  graphDimensionsInPixels,
30523
30596
  snapStep,
30524
- disableKeyboardInteraction
30597
+ disableKeyboardInteraction,
30598
+ interactiveColor
30525
30599
  } = useGraphConfig();
30526
30600
  let startExtend = undefined;
30527
30601
  let endExtend = undefined;
@@ -30578,7 +30652,7 @@ const Line$1 = props => {
30578
30652
  start: startPtPx,
30579
30653
  end: endPtPx,
30580
30654
  style: {
30581
- stroke,
30655
+ stroke: interactiveColor,
30582
30656
  strokeWidth: "var(--movable-line-stroke-weight)"
30583
30657
  },
30584
30658
  className: dragging ? "movable-dragging" : "",
@@ -30586,11 +30660,11 @@ const Line$1 = props => {
30586
30660
  })), startExtend && /*#__PURE__*/React.createElement(Vector, {
30587
30661
  tail: start,
30588
30662
  tip: startExtend,
30589
- color: stroke
30663
+ testId: "movable-line__vector"
30590
30664
  }), endExtend && /*#__PURE__*/React.createElement(Vector, {
30591
30665
  tail: end,
30592
30666
  tip: endExtend,
30593
- color: stroke
30667
+ testId: "movable-line__vector"
30594
30668
  }));
30595
30669
  };
30596
30670
  const getMovableLineKeyboardConstraint = (line, snapStep, pointIndex) => {
@@ -30662,7 +30736,8 @@ function AngleGraph(props) {
30662
30736
  graphState
30663
30737
  } = props;
30664
30738
  const {
30665
- graphDimensionsInPixels
30739
+ graphDimensionsInPixels,
30740
+ interactiveColor
30666
30741
  } = useGraphConfig();
30667
30742
  const i18n = usePerseusI18n();
30668
30743
  const id = React.useId();
@@ -30693,13 +30768,14 @@ function AngleGraph(props) {
30693
30768
  start: startPtPx,
30694
30769
  end: endPtPx,
30695
30770
  style: {
30696
- stroke: "var(--movable-line-stroke-color)",
30771
+ stroke: interactiveColor,
30697
30772
  strokeWidth: "var(--movable-line-stroke-weight)"
30698
- }
30773
+ },
30774
+ testId: "angle-graph__line"
30699
30775
  }), /*#__PURE__*/React.createElement(Vector, {
30700
30776
  tail: angleLines[i][1],
30701
30777
  tip: endExtend,
30702
- color: "var(--movable-line-stroke-color)"
30778
+ testId: "angle-graph__vector"
30703
30779
  }));
30704
30780
  });
30705
30781
 
@@ -31043,7 +31119,8 @@ function MovableCircle(props) {
31043
31119
  } = props;
31044
31120
  const {
31045
31121
  snapStep,
31046
- disableKeyboardInteraction
31122
+ disableKeyboardInteraction,
31123
+ interactiveColor
31047
31124
  } = useGraphConfig();
31048
31125
  const [focused, setFocused] = React.useState(false);
31049
31126
  const draggableRef = useRef(null);
@@ -31079,7 +31156,9 @@ function MovableCircle(props) {
31079
31156
  cx: centerPx[X],
31080
31157
  cy: centerPx[Y],
31081
31158
  rx: radiiPx[X],
31082
- ry: radiiPx[Y]
31159
+ ry: radiiPx[Y],
31160
+ stroke: interactiveColor,
31161
+ "data-testid": "movable-circle__circle"
31083
31162
  }), /*#__PURE__*/React.createElement(DragHandle, {
31084
31163
  center: center,
31085
31164
  dragging: dragging,
@@ -31096,7 +31175,8 @@ function DragHandle(props) {
31096
31175
  } = props;
31097
31176
  const [centerPx] = useTransformVectorsToPixels(center);
31098
31177
  const {
31099
- markings
31178
+ markings,
31179
+ interactiveColor
31100
31180
  } = useGraphConfig();
31101
31181
  const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31102
31182
  const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
@@ -31110,7 +31190,9 @@ function DragHandle(props) {
31110
31190
  width: dragHandleDimensions[X],
31111
31191
  height: dragHandleDimensions[Y],
31112
31192
  rx: cornerRadius,
31113
- ry: cornerRadius
31193
+ ry: cornerRadius,
31194
+ fill: interactiveColor,
31195
+ "data-testid": "movable-circle__handle"
31114
31196
  }), dragHandleDotPositions.map(offsetPx => {
31115
31197
  const [xPx, yPx] = vec.add(offsetPx, centerPx);
31116
31198
  return /*#__PURE__*/React.createElement("circle", {
@@ -31269,8 +31351,7 @@ const LinearGraph = (props, key) => {
31269
31351
  start: true,
31270
31352
  end: true
31271
31353
  },
31272
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination)),
31273
- color: "var(--movable-line-stroke-color)"
31354
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination))
31274
31355
  }), /*#__PURE__*/React.createElement("g", {
31275
31356
  id: pointsDescriptionId,
31276
31357
  style: a11y.srOnly
@@ -31409,8 +31490,7 @@ const LinearSystemGraph = props => {
31409
31490
  start: true,
31410
31491
  end: true
31411
31492
  },
31412
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
31413
- color: "var(--movable-line-stroke-color)"
31493
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination))
31414
31494
  })), linesAriaInfo.map(({
31415
31495
  pointsDescriptionId,
31416
31496
  interceptDescriptionId,
@@ -32226,6 +32306,7 @@ function doMovePoint(state, action) {
32226
32306
  {
32227
32307
  return _extends({}, state, {
32228
32308
  hasBeenInteractedWith: true,
32309
+ focusedPointIndex: action.index,
32229
32310
  coords: setAtIndex({
32230
32311
  array: state.coords,
32231
32312
  index: action.index,
@@ -32545,7 +32626,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
32545
32626
  coords
32546
32627
  }, index) {
32547
32628
  const startingPoint = coords[index];
32548
-
32629
+ return calculateAngleSnap(destinationPoint, range, coords, index, startingPoint);
32630
+ }
32631
+ function calculateAngleSnap(destinationPoint, range, coords, index, startingPoint) {
32549
32632
  // Needed to prevent updating the original coords before the checks for
32550
32633
  // degenerate triangles and overlapping sides
32551
32634
  const coordsCopy = [...coords];
@@ -32834,7 +32917,8 @@ const LimitedPolygonGraph = statefulProps => {
32834
32917
  snapStep
32835
32918
  } = statefulProps.graphState;
32836
32919
  const {
32837
- disableKeyboardInteraction
32920
+ disableKeyboardInteraction,
32921
+ interactiveColor
32838
32922
  } = graphConfig;
32839
32923
  const {
32840
32924
  strings,
@@ -32870,12 +32954,16 @@ const LimitedPolygonGraph = statefulProps => {
32870
32954
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32871
32955
  }, /*#__PURE__*/React.createElement(Polygon, {
32872
32956
  points: [...points],
32873
- color: "var(--movable-line-stroke-color)",
32957
+ color: interactiveColor,
32874
32958
  svgPolygonProps: {
32875
32959
  strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
32876
32960
  style: {
32877
32961
  fill: "transparent"
32878
- }
32962
+ },
32963
+ // Use aria-hidden to hide the line from screen readers
32964
+ // so it doesn't read as "image" with no context.
32965
+ // This is okay because the graph has its own aria-label.
32966
+ "aria-hidden": true
32879
32967
  }
32880
32968
  }), points.map((point, i) => {
32881
32969
  const pt1 = points.at(i - 1);
@@ -33015,6 +33103,9 @@ const UnlimitedPolygonGraph = statefulProps => {
33015
33103
  strings,
33016
33104
  locale
33017
33105
  } = usePerseusI18n();
33106
+ const {
33107
+ interactiveColor
33108
+ } = useGraphConfig();
33018
33109
  const id = React.useId();
33019
33110
  const polygonPointsNumId = id + "-points-num";
33020
33111
  const polygonPointsId = id + "-points";
@@ -33054,14 +33145,22 @@ const UnlimitedPolygonGraph = statefulProps => {
33054
33145
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
33055
33146
  }, /*#__PURE__*/React.createElement(Polyline, {
33056
33147
  points: [...points],
33057
- color: "var(--movable-line-stroke-color)",
33148
+ color: interactiveColor,
33058
33149
  svgPolylineProps: {
33059
33150
  strokeWidth: "var(--movable-line-stroke-weight)",
33060
33151
  style: {
33061
33152
  fill: "transparent"
33062
- }
33153
+ },
33154
+ // Use aria-hidden to hide the line from screen readers
33155
+ // so it doesn't read as "image" with no context.
33156
+ // This is okay because the graph has its own aria-label.
33157
+ "aria-hidden": true
33063
33158
  }
33064
33159
  }), /*#__PURE__*/React.createElement("rect", {
33160
+ // Use aria-hidden to hide the line from screen readers
33161
+ // so it doesn't read as "image" with no context.
33162
+ // This is okay because the graph has its own aria-label.
33163
+ "aria-hidden": true,
33065
33164
  style: {
33066
33165
  fill: "rgba(0,0,0,0)",
33067
33166
  cursor: "crosshair"
@@ -33215,7 +33314,7 @@ function getKeyboardMovementConstraintForPoint(points, index, range, snapStep, s
33215
33314
  case "sides":
33216
33315
  return getSideSnapConstraint(points, index, range);
33217
33316
  case "angles":
33218
- return p => p;
33317
+ return getAngleSnapConstraint(points, index, range);
33219
33318
  default:
33220
33319
  throw new UnreachableCaseError(snapTo);
33221
33320
  }
@@ -33269,6 +33368,49 @@ function getSideSnapConstraint(points, index, range) {
33269
33368
  right: movePointWithConstraint(coord => vec.add(coord, [1, 0]))
33270
33369
  };
33271
33370
  }
33371
+ function getAngleSnapConstraint(points, index, range) {
33372
+ // Make newPoints mutable.
33373
+ const newPoints = [...points];
33374
+
33375
+ // Get the point that is being moved.
33376
+ const pointToBeMoved = newPoints[index];
33377
+
33378
+ // Create a helper function that moves the point to a valid location
33379
+ // for angle snapping.
33380
+ const movePointWithConstraint = moveFunc => {
33381
+ // The direction the user is attempting to move the point in.
33382
+ let destinationAttempt = bound$1({
33383
+ snapStep: [0, 0],
33384
+ range,
33385
+ point: moveFunc(pointToBeMoved)
33386
+ });
33387
+ // The new point we're moving to.
33388
+ let newPoint = pointToBeMoved;
33389
+
33390
+ // Move the point and keep trying until we are at the boarder
33391
+ // of the graph.
33392
+ while (newPoint[0] === pointToBeMoved[0] && newPoint[1] === pointToBeMoved[1] && isInBound({
33393
+ range,
33394
+ point: destinationAttempt
33395
+ })) {
33396
+ newPoint = calculateAngleSnap(destinationAttempt, range, newPoints, index, pointToBeMoved);
33397
+
33398
+ // Increment the destinationAttempt.
33399
+ // For every time it does not work increment the direction for x and y.
33400
+ destinationAttempt = moveFunc(destinationAttempt);
33401
+ }
33402
+ return newPoint;
33403
+ };
33404
+
33405
+ // For each direction look for the next movable point by a small step to increase changes
33406
+ // of finding the next angle value.
33407
+ return {
33408
+ up: movePointWithConstraint(coord => vec.add(coord, [0, 0.1])),
33409
+ down: movePointWithConstraint(coord => vec.sub(coord, [0, 0.1])),
33410
+ left: movePointWithConstraint(coord => vec.sub(coord, [0.1, 0])),
33411
+ right: movePointWithConstraint(coord => vec.add(coord, [0.1, 0]))
33412
+ };
33413
+ }
33272
33414
 
33273
33415
  function renderQuadraticGraph(state, dispatch, i18n) {
33274
33416
  return {
@@ -33288,6 +33430,9 @@ function QuadraticGraph(props) {
33288
33430
  coords,
33289
33431
  snapStep
33290
33432
  } = graphState;
33433
+ const {
33434
+ interactiveColor
33435
+ } = useGraphConfig();
33291
33436
  const {
33292
33437
  strings,
33293
33438
  locale
@@ -33329,7 +33474,13 @@ function QuadraticGraph(props) {
33329
33474
  "aria-describedby": `${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`
33330
33475
  }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
33331
33476
  y: y,
33332
- color: color.blue
33477
+ color: interactiveColor,
33478
+ svgPathProps: {
33479
+ // Use aria-hidden to hide the line from screen readers
33480
+ // so it doesn't read as "image" with no context.
33481
+ // This is okay because the graph has its own aria-label.
33482
+ "aria-hidden": true
33483
+ }
33333
33484
  }), coords.map((coord, i) => {
33334
33485
  const srQuadraticPoint = getQuadraticPointString(i + 1, coord, strings, locale);
33335
33486
  // Vertex might not exist if the quadratic graph is a line
@@ -33724,6 +33875,9 @@ function SinusoidGraph(props) {
33724
33875
  dispatch,
33725
33876
  graphState
33726
33877
  } = props;
33878
+ const {
33879
+ interactiveColor
33880
+ } = useGraphConfig();
33727
33881
  const i18n = usePerseusI18n();
33728
33882
  const id = React.useId();
33729
33883
  const descriptionId = id + "-description";
@@ -33766,7 +33920,13 @@ function SinusoidGraph(props) {
33766
33920
  "aria-describedby": descriptionId
33767
33921
  }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
33768
33922
  y: x => computeSine(x, coeffRef.current),
33769
- color: color.blue
33923
+ color: interactiveColor,
33924
+ svgPathProps: {
33925
+ // Use aria-hidden to hide the line from screen readers
33926
+ // so it doesn't read as "image" with no context.
33927
+ // This is okay because the graph has its own aria-label.
33928
+ "aria-hidden": true
33929
+ }
33770
33930
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33771
33931
  ariaLabel: i === 0 ? srSinusoidRootPoint : srSinusoidPeakPoint,
33772
33932
  key: "point-" + i,
@@ -34115,6 +34275,7 @@ const MafsGraph = props => {
34115
34275
  i18n,
34116
34276
  markings: props.markings
34117
34277
  });
34278
+ const disableInteraction = readOnly || !!props.static;
34118
34279
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
34119
34280
  value: {
34120
34281
  range: state.range,
@@ -34127,7 +34288,12 @@ const MafsGraph = props => {
34127
34288
  width,
34128
34289
  height,
34129
34290
  labels,
34130
- disableKeyboardInteraction: readOnly || !!props.static
34291
+ disableKeyboardInteraction: disableInteraction,
34292
+ // If the graph is read-only or static, we want to make it
34293
+ // visually clear that the graph is no longer interactive.
34294
+ // We do this by changing the color of the interactive elements
34295
+ // to a static gray color rather than our standard blue.
34296
+ interactiveColor: disableInteraction ? "var(--static-gray)" : "var(--mafs-blue)"
34131
34297
  }
34132
34298
  }, /*#__PURE__*/React.createElement(View, {
34133
34299
  className: "mafs-graph-container"
@@ -35143,7 +35309,8 @@ InteractiveGraph.defaultProps = {
35143
35309
  const staticTransform$3 = _.identity;
35144
35310
  var InteractiveGraph$1 = {
35145
35311
  name: "interactive-graph",
35146
- displayName: "Interactive graph (Assessments only)",
35312
+ displayName: "Interactive graph",
35313
+ accessible: true,
35147
35314
  widget: InteractiveGraph,
35148
35315
  staticTransform: staticTransform$3
35149
35316
  };
@@ -41232,7 +41399,7 @@ const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
41232
41399
 
41233
41400
  // This file is processed by a Rollup plugin (replace) to inject the production
41234
41401
  const libName = "@khanacademy/perseus";
41235
- const libVersion = "56.0.0";
41402
+ const libVersion = "56.1.0";
41236
41403
  addLibraryVersionToPerseusDebug(libName, libVersion);
41237
41404
 
41238
41405
  const apiVersion = {