@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/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/index.js CHANGED
@@ -28143,7 +28143,8 @@ const defaultGraphConfig = {
28143
28143
  width: 0,
28144
28144
  height: 0,
28145
28145
  labels: [],
28146
- disableKeyboardInteraction: false
28146
+ disableKeyboardInteraction: false,
28147
+ interactiveColor: "var(--mafs-blue)"
28147
28148
  };
28148
28149
  const GraphConfigContext = /*#__PURE__*/React.createContext(defaultGraphConfig);
28149
28150
  function useGraphConfig() {
@@ -28220,6 +28221,11 @@ const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).
28220
28221
  function Arrowhead(props) {
28221
28222
  const [point] = useTransformVectorsToPixels(props.tip);
28222
28223
  return /*#__PURE__*/React__namespace.createElement("g", {
28224
+ // Use aria-hidden to hide the line from screen readers
28225
+ // so it doesn't read as "image" with no context.
28226
+ // The elements using this should have their own aria-labels,
28227
+ // so this is okay.
28228
+ "aria-hidden": true,
28223
28229
  className: "interactive-graph-arrowhead",
28224
28230
  transform: `translate(${point[X]} ${point[Y]}) rotate(${props.angle})`
28225
28231
  }, /*#__PURE__*/React__namespace.createElement("g", {
@@ -29389,6 +29395,11 @@ function SVGLine(props) {
29389
29395
  testId
29390
29396
  } = props;
29391
29397
  return /*#__PURE__*/React__namespace.createElement("line", {
29398
+ // Use aria-hidden to hide the line from screen readers
29399
+ // so it doesn't read as "image" with no context.
29400
+ // The elements using this should have their own aria-labels,
29401
+ // so this is okay.
29402
+ "aria-hidden": true,
29392
29403
  x1: start[X],
29393
29404
  y1: start[Y],
29394
29405
  x2: end[X],
@@ -29399,15 +29410,23 @@ function SVGLine(props) {
29399
29410
  });
29400
29411
  }
29401
29412
 
29413
+ /**
29414
+ * Used to visualize vectors for both interative elements
29415
+ * as well as locked figures within an Interactive Graph widget.
29416
+ */
29402
29417
  const {
29403
29418
  calculateAngleInDegrees: calculateAngleInDegrees$3
29404
29419
  } = kmath.angles;
29405
29420
  function Vector(props) {
29421
+ const {
29422
+ interactiveColor
29423
+ } = useGraphConfig();
29406
29424
  const {
29407
29425
  tail,
29408
29426
  tip,
29409
- color,
29410
- style
29427
+ color = interactiveColor,
29428
+ style,
29429
+ testId
29411
29430
  } = props;
29412
29431
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
29413
29432
  const direction = mafs.vec.sub(tipPx, tailPx);
@@ -29416,7 +29435,8 @@ function Vector(props) {
29416
29435
  style: {
29417
29436
  stroke: color,
29418
29437
  strokeWidth: 2
29419
- }
29438
+ },
29439
+ "data-testid": testId
29420
29440
  }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
29421
29441
  start: tailPx,
29422
29442
  end: tipPx,
@@ -29714,9 +29734,17 @@ function getAngleFromPoints(points, i) {
29714
29734
  const a = mafs.vec.dist(point, pt1);
29715
29735
  const b = mafs.vec.dist(point, pt2);
29716
29736
  const c = mafs.vec.dist(pt1, pt2);
29737
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
29738
+
29739
+ // If the equation results in a number greater than 1 or less than -1.
29740
+ // Correct to ensure a valid angle.
29741
+ // This ensures we are not producing NaN results from Math.acos.
29742
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
29743
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
29744
+ }
29717
29745
 
29718
29746
  // Law of cosines
29719
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29747
+ const angle = Math.acos(lawOfCosinesRadicand);
29720
29748
  return angle;
29721
29749
  }
29722
29750
  function getSideLengthsFromPoints(points, i, isPolygonOpen) {
@@ -30243,9 +30271,17 @@ const PolygonAngle = _ref => {
30243
30271
  const a = mafs.vec.dist(centerPoint, endPoints[0]);
30244
30272
  const b = mafs.vec.dist(centerPoint, endPoints[1]);
30245
30273
  const c = mafs.vec.dist(endPoints[0], endPoints[1]);
30274
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
30275
+
30276
+ // If the equation results in a number greater than 1 or less than -1.
30277
+ // Correct to ensure a valid angle.
30278
+ // This ensures we are not producing NaN results from Math.acos.
30279
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
30280
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
30281
+ }
30246
30282
 
30247
30283
  // Law of cosines
30248
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
30284
+ const angle = Math.acos(lawOfCosinesRadicand);
30249
30285
  const y1 = centerY + (startY - centerY) / a * radius;
30250
30286
  const x2 = centerX + (endX - centerX) / b * radius;
30251
30287
  const x1 = centerX + (startX - centerX) / a * radius;
@@ -30362,6 +30398,12 @@ const Angle = _ref2 => {
30362
30398
  // Calculate the text position based on the angle and whether we allow reflex angles
30363
30399
  // Let's try the angle bisector method to find the midpoint of the arc
30364
30400
  const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
30401
+
30402
+ // Determine the color style for the arc when interactive vs disabled
30403
+ const {
30404
+ disableKeyboardInteraction
30405
+ } = useGraphConfig();
30406
+ const arcClassName = disableKeyboardInteraction ? "angle-arc-static" : "angle-arc-interactive";
30365
30407
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("defs", null, /*#__PURE__*/React__namespace.createElement("filter", {
30366
30408
  id: "background",
30367
30409
  x: "-5%",
@@ -30378,10 +30420,10 @@ const Angle = _ref2 => {
30378
30420
  start: [x1, y1],
30379
30421
  vertex: [x2, y2],
30380
30422
  end: [x3, y3],
30381
- className: "arc-right-angle"
30423
+ className: arcClassName
30382
30424
  }) : /*#__PURE__*/React__namespace.createElement(Arc, {
30383
30425
  arc: arc,
30384
- className: "angle-arc"
30426
+ className: arcClassName
30385
30427
  }), showAngles && /*#__PURE__*/React__namespace.createElement(TextLabel, {
30386
30428
  x: textX,
30387
30429
  y: textY,
@@ -30401,10 +30443,16 @@ const RightAngleSquare = _ref3 => {
30401
30443
  className
30402
30444
  } = _ref3;
30403
30445
  return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
30446
+ // Use aria-hidden to hide the line from screen readers
30447
+ // so it doesn't read as "image" with no context.
30448
+ // The elements using this should have their own aria-labels,
30449
+ // so this is okay.
30450
+ "aria-hidden": true,
30404
30451
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
30405
30452
  strokeWidth: 0.02,
30406
30453
  fill: "none",
30407
- className: className
30454
+ className: className,
30455
+ "data-testid": "angle-indicators__right-angle"
30408
30456
  }));
30409
30457
  };
30410
30458
 
@@ -30416,10 +30464,16 @@ const Arc = _ref4 => {
30416
30464
  className
30417
30465
  } = _ref4;
30418
30466
  return /*#__PURE__*/React__namespace.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React__namespace.createElement("path", {
30467
+ // Use aria-hidden to hide the line from screen readers
30468
+ // so it doesn't read as "image" with no context.
30469
+ // The elements using this should have their own aria-labels,
30470
+ // so this is okay.
30471
+ "aria-hidden": true,
30419
30472
  d: arc,
30420
30473
  strokeWidth: 0.02,
30421
30474
  fill: "none",
30422
- className: className
30475
+ className: className,
30476
+ "data-testid": "angle-indicators__arc"
30423
30477
  }));
30424
30478
  };
30425
30479
  const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
@@ -30692,7 +30746,13 @@ function Hairlines(props) {
30692
30746
  const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
30693
30747
  const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
30694
30748
  const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
30695
- return /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
30749
+ return /*#__PURE__*/React__namespace.createElement("g", {
30750
+ // Use aria-hidden to hide the line from screen readers
30751
+ // so it doesn't read as "image" with no context.
30752
+ // The elements using this should have their own aria-labels,
30753
+ // so this is okay.
30754
+ "aria-hidden": true
30755
+ }, /*#__PURE__*/React__namespace.createElement("line", {
30696
30756
  x1: verticalStartX,
30697
30757
  y1: y,
30698
30758
  x2: verticalEndX,
@@ -30717,11 +30777,12 @@ const hitboxSizePx = 48;
30717
30777
  const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
30718
30778
  const {
30719
30779
  markings,
30720
- showTooltips
30780
+ showTooltips,
30781
+ interactiveColor
30721
30782
  } = useGraphConfig();
30722
30783
  const {
30723
30784
  point,
30724
- color = wonderBlocksTokens.color.blue,
30785
+ color = interactiveColor,
30725
30786
  dragging,
30726
30787
  focused,
30727
30788
  cursor,
@@ -30739,6 +30800,11 @@ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
30739
30800
  const [[x, y]] = useTransformVectorsToPixels(point);
30740
30801
  const showHairlines = (dragging || focused) && markings !== "none";
30741
30802
  const svgForPoint = /*#__PURE__*/React__namespace.createElement("g", {
30803
+ // Use aria-hidden to hide the line from screen readers
30804
+ // so it doesn't read as "image" with no context.
30805
+ // The elements using this should have their own aria-labels,
30806
+ // so this is okay.
30807
+ "aria-hidden": true,
30742
30808
  ref: hitboxRef,
30743
30809
  className: pointClasses,
30744
30810
  style: {
@@ -30842,9 +30908,21 @@ function useControlPoint(params) {
30842
30908
  x: srFormatNumber(point[X], locale),
30843
30909
  y: srFormatNumber(point[Y], locale)
30844
30910
  });
30911
+
30912
+ // Set the forwarded ref to the focusable handle element when it changes.
30845
30913
  React.useLayoutEffect(() => {
30846
30914
  setForwardedRef(forwardedRef, focusableHandleRef.current);
30847
30915
  }, [forwardedRef]);
30916
+
30917
+ // If the point is being dragged and is not focused, focus the focusable handle.
30918
+ React.useLayoutEffect(() => {
30919
+ if (dragging && !focused) {
30920
+ // If the point is being dragged, focus the focusable handle so that
30921
+ // users can continue to interact with the point using the keyboard or buttons.
30922
+ // This particular focus call ensures that the focus ring and hairlines are visible.
30923
+ focusableHandleRef.current?.focus();
30924
+ }
30925
+ }, [dragging, focused]);
30848
30926
  const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
30849
30927
  "data-testid": "movable-point__focusable-handle",
30850
30928
  className: "movable-point__focusable-handle",
@@ -30905,7 +30983,6 @@ const MovableLine = props => {
30905
30983
  points: [start, end],
30906
30984
  ariaLabels,
30907
30985
  ariaDescribedBy,
30908
- color,
30909
30986
  extend,
30910
30987
  onMoveLine = () => {},
30911
30988
  onMovePoint = () => {}
@@ -30940,7 +31017,6 @@ const MovableLine = props => {
30940
31017
  ariaLive: ariaLives[0],
30941
31018
  point: start,
30942
31019
  sequenceNumber: 1,
30943
- color,
30944
31020
  onMove: p => {
30945
31021
  setAriaLives(["polite", "off", "off"]);
30946
31022
  onMovePoint(0, p);
@@ -30956,7 +31032,6 @@ const MovableLine = props => {
30956
31032
  ariaLive: ariaLives[1],
30957
31033
  point: end,
30958
31034
  sequenceNumber: 2,
30959
- color,
30960
31035
  onMove: p => {
30961
31036
  setAriaLives(["off", "polite", "off"]);
30962
31037
  onMovePoint(1, p);
@@ -30969,7 +31044,6 @@ const MovableLine = props => {
30969
31044
  ariaLive: ariaLives[2],
30970
31045
  start: start,
30971
31046
  end: end,
30972
- stroke: color,
30973
31047
  extend: extend,
30974
31048
  onMove: delta => {
30975
31049
  setAriaLives(["off", "off", "polite"]);
@@ -30978,7 +31052,6 @@ const MovableLine = props => {
30978
31052
  });
30979
31053
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle1, line, focusableHandle2, visiblePoint1, visiblePoint2);
30980
31054
  };
30981
- const defaultStroke = "var(--movable-line-stroke-color)";
30982
31055
  const Line$1 = props => {
30983
31056
  const {
30984
31057
  start,
@@ -30987,7 +31060,6 @@ const Line$1 = props => {
30987
31060
  ariaDescribedBy,
30988
31061
  ariaLive,
30989
31062
  extend,
30990
- stroke = defaultStroke,
30991
31063
  onMove
30992
31064
  } = props;
30993
31065
  const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
@@ -30995,7 +31067,8 @@ const Line$1 = props => {
30995
31067
  range,
30996
31068
  graphDimensionsInPixels,
30997
31069
  snapStep,
30998
- disableKeyboardInteraction
31070
+ disableKeyboardInteraction,
31071
+ interactiveColor
30999
31072
  } = useGraphConfig();
31000
31073
  let startExtend = undefined;
31001
31074
  let endExtend = undefined;
@@ -31052,7 +31125,7 @@ const Line$1 = props => {
31052
31125
  start: startPtPx,
31053
31126
  end: endPtPx,
31054
31127
  style: {
31055
- stroke,
31128
+ stroke: interactiveColor,
31056
31129
  strokeWidth: "var(--movable-line-stroke-weight)"
31057
31130
  },
31058
31131
  className: dragging ? "movable-dragging" : "",
@@ -31060,11 +31133,11 @@ const Line$1 = props => {
31060
31133
  })), startExtend && /*#__PURE__*/React__namespace.createElement(Vector, {
31061
31134
  tail: start,
31062
31135
  tip: startExtend,
31063
- color: stroke
31136
+ testId: "movable-line__vector"
31064
31137
  }), endExtend && /*#__PURE__*/React__namespace.createElement(Vector, {
31065
31138
  tail: end,
31066
31139
  tip: endExtend,
31067
- color: stroke
31140
+ testId: "movable-line__vector"
31068
31141
  }));
31069
31142
  };
31070
31143
  const getMovableLineKeyboardConstraint = (line, snapStep, pointIndex) => {
@@ -31137,7 +31210,8 @@ function AngleGraph(props) {
31137
31210
  graphState
31138
31211
  } = props;
31139
31212
  const {
31140
- graphDimensionsInPixels
31213
+ graphDimensionsInPixels,
31214
+ interactiveColor
31141
31215
  } = useGraphConfig();
31142
31216
  const i18n = usePerseusI18n();
31143
31217
  const id = React__namespace.useId();
@@ -31169,13 +31243,14 @@ function AngleGraph(props) {
31169
31243
  start: startPtPx,
31170
31244
  end: endPtPx,
31171
31245
  style: {
31172
- stroke: "var(--movable-line-stroke-color)",
31246
+ stroke: interactiveColor,
31173
31247
  strokeWidth: "var(--movable-line-stroke-weight)"
31174
- }
31248
+ },
31249
+ testId: "angle-graph__line"
31175
31250
  }), /*#__PURE__*/React__namespace.createElement(Vector, {
31176
31251
  tail: angleLines[i][1],
31177
31252
  tip: endExtend,
31178
- color: "var(--movable-line-stroke-color)"
31253
+ testId: "angle-graph__vector"
31179
31254
  }));
31180
31255
  });
31181
31256
 
@@ -31532,7 +31607,8 @@ function MovableCircle(props) {
31532
31607
  } = props;
31533
31608
  const {
31534
31609
  snapStep,
31535
- disableKeyboardInteraction
31610
+ disableKeyboardInteraction,
31611
+ interactiveColor
31536
31612
  } = useGraphConfig();
31537
31613
  const [focused, setFocused] = React__namespace.useState(false);
31538
31614
  const draggableRef = React.useRef(null);
@@ -31568,7 +31644,9 @@ function MovableCircle(props) {
31568
31644
  cx: centerPx[X],
31569
31645
  cy: centerPx[Y],
31570
31646
  rx: radiiPx[X],
31571
- ry: radiiPx[Y]
31647
+ ry: radiiPx[Y],
31648
+ stroke: interactiveColor,
31649
+ "data-testid": "movable-circle__circle"
31572
31650
  }), /*#__PURE__*/React__namespace.createElement(DragHandle, {
31573
31651
  center: center,
31574
31652
  dragging: dragging,
@@ -31585,7 +31663,8 @@ function DragHandle(props) {
31585
31663
  } = props;
31586
31664
  const [centerPx] = useTransformVectorsToPixels(center);
31587
31665
  const {
31588
- markings
31666
+ markings,
31667
+ interactiveColor
31589
31668
  } = useGraphConfig();
31590
31669
  const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31591
31670
  const topLeft = mafs.vec.sub(centerPx, mafs.vec.scale(dragHandleDimensions, 0.5));
@@ -31599,7 +31678,9 @@ function DragHandle(props) {
31599
31678
  width: dragHandleDimensions[X],
31600
31679
  height: dragHandleDimensions[Y],
31601
31680
  rx: cornerRadius,
31602
- ry: cornerRadius
31681
+ ry: cornerRadius,
31682
+ fill: interactiveColor,
31683
+ "data-testid": "movable-circle__handle"
31603
31684
  }), dragHandleDotPositions.map(offsetPx => {
31604
31685
  const [xPx, yPx] = mafs.vec.add(offsetPx, centerPx);
31605
31686
  return /*#__PURE__*/React__namespace.createElement("circle", {
@@ -31758,8 +31839,7 @@ const LinearGraph = (props, key) => {
31758
31839
  start: true,
31759
31840
  end: true
31760
31841
  },
31761
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination)),
31762
- color: "var(--movable-line-stroke-color)"
31842
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination))
31763
31843
  }), /*#__PURE__*/React__namespace.createElement("g", {
31764
31844
  id: pointsDescriptionId,
31765
31845
  style: a11y.srOnly
@@ -31901,8 +31981,7 @@ const LinearSystemGraph = props => {
31901
31981
  start: true,
31902
31982
  end: true
31903
31983
  },
31904
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
31905
- color: "var(--movable-line-stroke-color)"
31984
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination))
31906
31985
  })), linesAriaInfo.map(_ref2 => {
31907
31986
  let {
31908
31987
  pointsDescriptionId,
@@ -32748,6 +32827,7 @@ function doMovePoint(state, action) {
32748
32827
  return {
32749
32828
  ...state,
32750
32829
  hasBeenInteractedWith: true,
32830
+ focusedPointIndex: action.index,
32751
32831
  coords: setAtIndex({
32752
32832
  array: state.coords,
32753
32833
  index: action.index,
@@ -33083,7 +33163,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, _ref6, index) {
33083
33163
  coords
33084
33164
  } = _ref6;
33085
33165
  const startingPoint = coords[index];
33086
-
33166
+ return calculateAngleSnap(destinationPoint, range, coords, index, startingPoint);
33167
+ }
33168
+ function calculateAngleSnap(destinationPoint, range, coords, index, startingPoint) {
33087
33169
  // Needed to prevent updating the original coords before the checks for
33088
33170
  // degenerate triangles and overlapping sides
33089
33171
  const coordsCopy = [...coords];
@@ -33375,7 +33457,8 @@ const LimitedPolygonGraph = statefulProps => {
33375
33457
  snapStep
33376
33458
  } = statefulProps.graphState;
33377
33459
  const {
33378
- disableKeyboardInteraction
33460
+ disableKeyboardInteraction,
33461
+ interactiveColor
33379
33462
  } = graphConfig;
33380
33463
  const {
33381
33464
  strings,
@@ -33411,12 +33494,16 @@ const LimitedPolygonGraph = statefulProps => {
33411
33494
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
33412
33495
  }, /*#__PURE__*/React__namespace.createElement(mafs.Polygon, {
33413
33496
  points: [...points],
33414
- color: "var(--movable-line-stroke-color)",
33497
+ color: interactiveColor,
33415
33498
  svgPolygonProps: {
33416
33499
  strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
33417
33500
  style: {
33418
33501
  fill: "transparent"
33419
- }
33502
+ },
33503
+ // Use aria-hidden to hide the line from screen readers
33504
+ // so it doesn't read as "image" with no context.
33505
+ // This is okay because the graph has its own aria-label.
33506
+ "aria-hidden": true
33420
33507
  }
33421
33508
  }), points.map((point, i) => {
33422
33509
  const pt1 = points.at(i - 1);
@@ -33557,6 +33644,9 @@ const UnlimitedPolygonGraph = statefulProps => {
33557
33644
  strings,
33558
33645
  locale
33559
33646
  } = usePerseusI18n();
33647
+ const {
33648
+ interactiveColor
33649
+ } = useGraphConfig();
33560
33650
  const id = React__namespace.useId();
33561
33651
  const polygonPointsNumId = id + "-points-num";
33562
33652
  const polygonPointsId = id + "-points";
@@ -33597,14 +33687,22 @@ const UnlimitedPolygonGraph = statefulProps => {
33597
33687
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
33598
33688
  }, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
33599
33689
  points: [...points],
33600
- color: "var(--movable-line-stroke-color)",
33690
+ color: interactiveColor,
33601
33691
  svgPolylineProps: {
33602
33692
  strokeWidth: "var(--movable-line-stroke-weight)",
33603
33693
  style: {
33604
33694
  fill: "transparent"
33605
- }
33695
+ },
33696
+ // Use aria-hidden to hide the line from screen readers
33697
+ // so it doesn't read as "image" with no context.
33698
+ // This is okay because the graph has its own aria-label.
33699
+ "aria-hidden": true
33606
33700
  }
33607
33701
  }), /*#__PURE__*/React__namespace.createElement("rect", {
33702
+ // Use aria-hidden to hide the line from screen readers
33703
+ // so it doesn't read as "image" with no context.
33704
+ // This is okay because the graph has its own aria-label.
33705
+ "aria-hidden": true,
33608
33706
  style: {
33609
33707
  fill: "rgba(0,0,0,0)",
33610
33708
  cursor: "crosshair"
@@ -33758,7 +33856,7 @@ function getKeyboardMovementConstraintForPoint(points, index, range, snapStep, s
33758
33856
  case "sides":
33759
33857
  return getSideSnapConstraint(points, index, range);
33760
33858
  case "angles":
33761
- return p => p;
33859
+ return getAngleSnapConstraint(points, index, range);
33762
33860
  default:
33763
33861
  throw new wonderStuffCore.UnreachableCaseError(snapTo);
33764
33862
  }
@@ -33812,6 +33910,49 @@ function getSideSnapConstraint(points, index, range) {
33812
33910
  right: movePointWithConstraint(coord => mafs.vec.add(coord, [1, 0]))
33813
33911
  };
33814
33912
  }
33913
+ function getAngleSnapConstraint(points, index, range) {
33914
+ // Make newPoints mutable.
33915
+ const newPoints = [...points];
33916
+
33917
+ // Get the point that is being moved.
33918
+ const pointToBeMoved = newPoints[index];
33919
+
33920
+ // Create a helper function that moves the point to a valid location
33921
+ // for angle snapping.
33922
+ const movePointWithConstraint = moveFunc => {
33923
+ // The direction the user is attempting to move the point in.
33924
+ let destinationAttempt = bound$1({
33925
+ snapStep: [0, 0],
33926
+ range,
33927
+ point: moveFunc(pointToBeMoved)
33928
+ });
33929
+ // The new point we're moving to.
33930
+ let newPoint = pointToBeMoved;
33931
+
33932
+ // Move the point and keep trying until we are at the boarder
33933
+ // of the graph.
33934
+ while (newPoint[0] === pointToBeMoved[0] && newPoint[1] === pointToBeMoved[1] && isInBound({
33935
+ range,
33936
+ point: destinationAttempt
33937
+ })) {
33938
+ newPoint = calculateAngleSnap(destinationAttempt, range, newPoints, index, pointToBeMoved);
33939
+
33940
+ // Increment the destinationAttempt.
33941
+ // For every time it does not work increment the direction for x and y.
33942
+ destinationAttempt = moveFunc(destinationAttempt);
33943
+ }
33944
+ return newPoint;
33945
+ };
33946
+
33947
+ // For each direction look for the next movable point by a small step to increase changes
33948
+ // of finding the next angle value.
33949
+ return {
33950
+ up: movePointWithConstraint(coord => mafs.vec.add(coord, [0, 0.1])),
33951
+ down: movePointWithConstraint(coord => mafs.vec.sub(coord, [0, 0.1])),
33952
+ left: movePointWithConstraint(coord => mafs.vec.sub(coord, [0.1, 0])),
33953
+ right: movePointWithConstraint(coord => mafs.vec.add(coord, [0.1, 0]))
33954
+ };
33955
+ }
33815
33956
 
33816
33957
  function renderQuadraticGraph(state, dispatch, i18n) {
33817
33958
  return {
@@ -33831,6 +33972,9 @@ function QuadraticGraph(props) {
33831
33972
  coords,
33832
33973
  snapStep
33833
33974
  } = graphState;
33975
+ const {
33976
+ interactiveColor
33977
+ } = useGraphConfig();
33834
33978
  const {
33835
33979
  strings,
33836
33980
  locale
@@ -33872,7 +34016,13 @@ function QuadraticGraph(props) {
33872
34016
  "aria-describedby": `${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`
33873
34017
  }, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
33874
34018
  y: y,
33875
- color: wonderBlocksTokens.color.blue
34019
+ color: interactiveColor,
34020
+ svgPathProps: {
34021
+ // Use aria-hidden to hide the line from screen readers
34022
+ // so it doesn't read as "image" with no context.
34023
+ // This is okay because the graph has its own aria-label.
34024
+ "aria-hidden": true
34025
+ }
33876
34026
  }), coords.map((coord, i) => {
33877
34027
  const srQuadraticPoint = getQuadraticPointString(i + 1, coord, strings, locale);
33878
34028
  // Vertex might not exist if the quadratic graph is a line
@@ -34272,6 +34422,9 @@ function SinusoidGraph(props) {
34272
34422
  dispatch,
34273
34423
  graphState
34274
34424
  } = props;
34425
+ const {
34426
+ interactiveColor
34427
+ } = useGraphConfig();
34275
34428
  const i18n = usePerseusI18n();
34276
34429
  const id = React__namespace.useId();
34277
34430
  const descriptionId = id + "-description";
@@ -34314,7 +34467,13 @@ function SinusoidGraph(props) {
34314
34467
  "aria-describedby": descriptionId
34315
34468
  }, /*#__PURE__*/React__namespace.createElement(mafs.Plot.OfX, {
34316
34469
  y: x => computeSine(x, coeffRef.current),
34317
- color: wonderBlocksTokens.color.blue
34470
+ color: interactiveColor,
34471
+ svgPathProps: {
34472
+ // Use aria-hidden to hide the line from screen readers
34473
+ // so it doesn't read as "image" with no context.
34474
+ // This is okay because the graph has its own aria-label.
34475
+ "aria-hidden": true
34476
+ }
34318
34477
  }), coords.map((coord, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
34319
34478
  ariaLabel: i === 0 ? srSinusoidRootPoint : srSinusoidPeakPoint,
34320
34479
  key: "point-" + i,
@@ -34663,6 +34822,7 @@ const MafsGraph = props => {
34663
34822
  i18n,
34664
34823
  markings: props.markings
34665
34824
  });
34825
+ const disableInteraction = readOnly || !!props.static;
34666
34826
  return /*#__PURE__*/React__namespace.createElement(GraphConfigContext.Provider, {
34667
34827
  value: {
34668
34828
  range: state.range,
@@ -34675,7 +34835,12 @@ const MafsGraph = props => {
34675
34835
  width,
34676
34836
  height,
34677
34837
  labels,
34678
- disableKeyboardInteraction: readOnly || !!props.static
34838
+ disableKeyboardInteraction: disableInteraction,
34839
+ // If the graph is read-only or static, we want to make it
34840
+ // visually clear that the graph is no longer interactive.
34841
+ // We do this by changing the color of the interactive elements
34842
+ // to a static gray color rather than our standard blue.
34843
+ interactiveColor: disableInteraction ? "var(--static-gray)" : "var(--mafs-blue)"
34679
34844
  }
34680
34845
  }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
34681
34846
  className: "mafs-graph-container"
@@ -35701,7 +35866,8 @@ class InteractiveGraph extends React__namespace.Component {
35701
35866
  const staticTransform$3 = ___default["default"].identity;
35702
35867
  var InteractiveGraph$1 = {
35703
35868
  name: "interactive-graph",
35704
- displayName: "Interactive graph (Assessments only)",
35869
+ displayName: "Interactive graph",
35870
+ accessible: true,
35705
35871
  widget: InteractiveGraph,
35706
35872
  staticTransform: staticTransform$3
35707
35873
  };
@@ -41796,7 +41962,7 @@ const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
41796
41962
 
41797
41963
  // This file is processed by a Rollup plugin (replace) to inject the production
41798
41964
  const libName = "@khanacademy/perseus";
41799
- const libVersion = "56.0.0";
41965
+ const libVersion = "56.1.0";
41800
41966
  addLibraryVersionToPerseusDebug(libName, libVersion);
41801
41967
 
41802
41968
  const apiVersion = {