@khanacademy/perseus 56.0.0 → 56.2.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.js CHANGED
@@ -18020,17 +18020,6 @@ class WidgetContainer extends React.Component {
18020
18020
  widget_id: this.props.id
18021
18021
  },
18022
18022
  onError: error => {
18023
- // TODO(LEMS-2826): Remove analytics event in LEMS-2826 in favor of ti below.
18024
- analytics.onAnalyticsEvent({
18025
- type: "perseus:widget-rendering-error",
18026
- payload: {
18027
- widgetSubType: subType,
18028
- widgetType: type,
18029
- widgetId: this.props.id,
18030
- message: error.message,
18031
- userAgent: userAgent
18032
- }
18033
- });
18034
18023
  analytics.onAnalyticsEvent({
18035
18024
  type: "perseus:widget-rendering-error:ti",
18036
18025
  payload: {
@@ -27696,7 +27685,8 @@ const defaultGraphConfig = {
27696
27685
  width: 0,
27697
27686
  height: 0,
27698
27687
  labels: [],
27699
- disableKeyboardInteraction: false
27688
+ disableKeyboardInteraction: false,
27689
+ interactiveColor: "var(--mafs-blue)"
27700
27690
  };
27701
27691
  const GraphConfigContext = /*#__PURE__*/createContext(defaultGraphConfig);
27702
27692
  function useGraphConfig() {
@@ -27767,6 +27757,11 @@ function Arrowhead(props) {
27767
27757
  var _props$color;
27768
27758
  const [point] = useTransformVectorsToPixels(props.tip);
27769
27759
  return /*#__PURE__*/React.createElement("g", {
27760
+ // Use aria-hidden to hide the line from screen readers
27761
+ // so it doesn't read as "image" with no context.
27762
+ // The elements using this should have their own aria-labels,
27763
+ // so this is okay.
27764
+ "aria-hidden": true,
27770
27765
  className: "interactive-graph-arrowhead",
27771
27766
  transform: `translate(${point[X]} ${point[Y]}) rotate(${props.angle})`
27772
27767
  }, /*#__PURE__*/React.createElement("g", {
@@ -28931,6 +28926,11 @@ function SVGLine(props) {
28931
28926
  testId
28932
28927
  } = props;
28933
28928
  return /*#__PURE__*/React.createElement("line", {
28929
+ // Use aria-hidden to hide the line from screen readers
28930
+ // so it doesn't read as "image" with no context.
28931
+ // The elements using this should have their own aria-labels,
28932
+ // so this is okay.
28933
+ "aria-hidden": true,
28934
28934
  x1: start[X],
28935
28935
  y1: start[Y],
28936
28936
  x2: end[X],
@@ -28941,15 +28941,23 @@ function SVGLine(props) {
28941
28941
  });
28942
28942
  }
28943
28943
 
28944
+ /**
28945
+ * Used to visualize vectors for both interative elements
28946
+ * as well as locked figures within an Interactive Graph widget.
28947
+ */
28944
28948
  const {
28945
28949
  calculateAngleInDegrees: calculateAngleInDegrees$3
28946
28950
  } = angles;
28947
28951
  function Vector(props) {
28952
+ const {
28953
+ interactiveColor
28954
+ } = useGraphConfig();
28948
28955
  const {
28949
28956
  tail,
28950
28957
  tip,
28951
- color,
28952
- style
28958
+ color = interactiveColor,
28959
+ style,
28960
+ testId
28953
28961
  } = props;
28954
28962
  const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
28955
28963
  const direction = vec.sub(tipPx, tailPx);
@@ -28958,7 +28966,8 @@ function Vector(props) {
28958
28966
  style: {
28959
28967
  stroke: color,
28960
28968
  strokeWidth: 2
28961
- }
28969
+ },
28970
+ "data-testid": testId
28962
28971
  }, /*#__PURE__*/React.createElement(SVGLine, {
28963
28972
  start: tailPx,
28964
28973
  end: tipPx,
@@ -29253,9 +29262,17 @@ function getAngleFromPoints(points, i) {
29253
29262
  const a = vec.dist(point, pt1);
29254
29263
  const b = vec.dist(point, pt2);
29255
29264
  const c = vec.dist(pt1, pt2);
29265
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
29266
+
29267
+ // If the equation results in a number greater than 1 or less than -1.
29268
+ // Correct to ensure a valid angle.
29269
+ // This ensures we are not producing NaN results from Math.acos.
29270
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
29271
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
29272
+ }
29256
29273
 
29257
29274
  // Law of cosines
29258
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29275
+ const angle = Math.acos(lawOfCosinesRadicand);
29259
29276
  return angle;
29260
29277
  }
29261
29278
  function getSideLengthsFromPoints(points, i, isPolygonOpen) {
@@ -29779,9 +29796,17 @@ const PolygonAngle = ({
29779
29796
  const a = vec.dist(centerPoint, endPoints[0]);
29780
29797
  const b = vec.dist(centerPoint, endPoints[1]);
29781
29798
  const c = vec.dist(endPoints[0], endPoints[1]);
29799
+ let lawOfCosinesRadicand = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
29800
+
29801
+ // If the equation results in a number greater than 1 or less than -1.
29802
+ // Correct to ensure a valid angle.
29803
+ // This ensures we are not producing NaN results from Math.acos.
29804
+ if (lawOfCosinesRadicand < -1 || lawOfCosinesRadicand > 1) {
29805
+ lawOfCosinesRadicand = Math.round(lawOfCosinesRadicand);
29806
+ }
29782
29807
 
29783
29808
  // Law of cosines
29784
- const angle = Math.acos((a ** 2 + b ** 2 - c ** 2) / (2 * a * b));
29809
+ const angle = Math.acos(lawOfCosinesRadicand);
29785
29810
  const y1 = centerY + (startY - centerY) / a * radius;
29786
29811
  const x2 = centerX + (endX - centerX) / b * radius;
29787
29812
  const x1 = centerX + (startX - centerX) / a * radius;
@@ -29824,7 +29849,7 @@ const PolygonAngle = ({
29824
29849
  }), /*#__PURE__*/React.createElement("feComposite", {
29825
29850
  operator: "over",
29826
29851
  in: "SourceGraphic"
29827
- }))), !isOutside && isRightAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
29852
+ }))), !isOutside && isRightPolygonAngle(angle) ? /*#__PURE__*/React.createElement(RightAngleSquare, {
29828
29853
  start: [x1, y1],
29829
29854
  vertex: [x2, y2],
29830
29855
  end: [x3, y3]
@@ -29897,6 +29922,12 @@ const Angle = ({
29897
29922
  // Calculate the text position based on the angle and whether we allow reflex angles
29898
29923
  // Let's try the angle bisector method to find the midpoint of the arc
29899
29924
  const [textX, textY] = calculateBisectorPoint(point1, point2, vertex, isReflexive, allowReflexAngles, radius);
29925
+
29926
+ // Determine the color style for the arc when interactive vs disabled
29927
+ const {
29928
+ disableKeyboardInteraction
29929
+ } = useGraphConfig();
29930
+ const arcClassName = disableKeyboardInteraction ? "angle-arc-static" : "angle-arc-interactive";
29900
29931
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("defs", null, /*#__PURE__*/React.createElement("filter", {
29901
29932
  id: "background",
29902
29933
  x: "-5%",
@@ -29913,10 +29944,10 @@ const Angle = ({
29913
29944
  start: [x1, y1],
29914
29945
  vertex: [x2, y2],
29915
29946
  end: [x3, y3],
29916
- className: "arc-right-angle"
29947
+ className: arcClassName
29917
29948
  }) : /*#__PURE__*/React.createElement(Arc, {
29918
29949
  arc: arc,
29919
- className: "angle-arc"
29950
+ className: arcClassName
29920
29951
  }), showAngles && /*#__PURE__*/React.createElement(TextLabel, {
29921
29952
  x: textX,
29922
29953
  y: textY,
@@ -29934,10 +29965,16 @@ const RightAngleSquare = ({
29934
29965
  end: [x3, y3],
29935
29966
  className
29936
29967
  }) => /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29968
+ // Use aria-hidden to hide the line from screen readers
29969
+ // so it doesn't read as "image" with no context.
29970
+ // The elements using this should have their own aria-labels,
29971
+ // so this is okay.
29972
+ "aria-hidden": true,
29937
29973
  d: `M ${x1} ${y1} L ${x3} ${y3} M ${x3} ${y3} L ${x2} ${y2}`,
29938
29974
  strokeWidth: 0.02,
29939
29975
  fill: "none",
29940
- className: className
29976
+ className: className,
29977
+ "data-testid": "angle-indicators__right-angle"
29941
29978
  }));
29942
29979
 
29943
29980
  // We're conditionally adding the class name here so that we can style the arc differently
@@ -29947,10 +29984,16 @@ const Arc = ({
29947
29984
  className
29948
29985
  }) => {
29949
29986
  return /*#__PURE__*/React.createElement(MafsCssTransformWrapper, null, /*#__PURE__*/React.createElement("path", {
29987
+ // Use aria-hidden to hide the line from screen readers
29988
+ // so it doesn't read as "image" with no context.
29989
+ // The elements using this should have their own aria-labels,
29990
+ // so this is okay.
29991
+ "aria-hidden": true,
29950
29992
  d: arc,
29951
29993
  strokeWidth: 0.02,
29952
29994
  fill: "none",
29953
- className: className
29995
+ className: className,
29996
+ "data-testid": "angle-indicators__arc"
29954
29997
  }));
29955
29998
  };
29956
29999
  const isRightPolygonAngle = angle => Math.abs(angle - Math.PI / 2) < 0.01;
@@ -30222,7 +30265,13 @@ function Hairlines(props) {
30222
30265
  const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
30223
30266
  const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
30224
30267
  const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
30225
- return /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
30268
+ return /*#__PURE__*/React.createElement("g", {
30269
+ // Use aria-hidden to hide the line from screen readers
30270
+ // so it doesn't read as "image" with no context.
30271
+ // The elements using this should have their own aria-labels,
30272
+ // so this is okay.
30273
+ "aria-hidden": true
30274
+ }, /*#__PURE__*/React.createElement("line", {
30226
30275
  x1: verticalStartX,
30227
30276
  y1: y,
30228
30277
  x2: verticalEndX,
@@ -30248,11 +30297,12 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30248
30297
  var _Object$entries$find$, _Object$entries$find;
30249
30298
  const {
30250
30299
  markings,
30251
- showTooltips
30300
+ showTooltips,
30301
+ interactiveColor
30252
30302
  } = useGraphConfig();
30253
30303
  const {
30254
30304
  point,
30255
- color: color$1 = color.blue,
30305
+ color: color$1 = interactiveColor,
30256
30306
  dragging,
30257
30307
  focused,
30258
30308
  cursor,
@@ -30267,6 +30317,11 @@ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
30267
30317
  const [[x, y]] = useTransformVectorsToPixels(point);
30268
30318
  const showHairlines = (dragging || focused) && markings !== "none";
30269
30319
  const svgForPoint = /*#__PURE__*/React.createElement("g", {
30320
+ // Use aria-hidden to hide the line from screen readers
30321
+ // so it doesn't read as "image" with no context.
30322
+ // The elements using this should have their own aria-labels,
30323
+ // so this is okay.
30324
+ "aria-hidden": true,
30270
30325
  ref: hitboxRef,
30271
30326
  className: pointClasses,
30272
30327
  style: {
@@ -30367,9 +30422,22 @@ function useControlPoint(params) {
30367
30422
  x: srFormatNumber(point[X], locale),
30368
30423
  y: srFormatNumber(point[Y], locale)
30369
30424
  });
30425
+
30426
+ // Set the forwarded ref to the focusable handle element when it changes.
30370
30427
  useLayoutEffect(() => {
30371
30428
  setForwardedRef(forwardedRef, focusableHandleRef.current);
30372
30429
  }, [forwardedRef]);
30430
+
30431
+ // If the point is being dragged and is not focused, focus the focusable handle.
30432
+ useLayoutEffect(() => {
30433
+ if (dragging && !focused) {
30434
+ var _focusableHandleRef$c;
30435
+ // If the point is being dragged, focus the focusable handle so that
30436
+ // users can continue to interact with the point using the keyboard or buttons.
30437
+ // This particular focus call ensures that the focus ring and hairlines are visible.
30438
+ (_focusableHandleRef$c = focusableHandleRef.current) == null || _focusableHandleRef$c.focus();
30439
+ }
30440
+ }, [dragging, focused]);
30373
30441
  const focusableHandle = /*#__PURE__*/React.createElement("g", {
30374
30442
  "data-testid": "movable-point__focusable-handle",
30375
30443
  className: "movable-point__focusable-handle",
@@ -30391,9 +30459,9 @@ function useControlPoint(params) {
30391
30459
  const visiblePoint = /*#__PURE__*/React.createElement(MovablePointView, {
30392
30460
  cursor: cursor,
30393
30461
  onClick: () => {
30394
- var _focusableHandleRef$c;
30462
+ var _focusableHandleRef$c2;
30395
30463
  onClick();
30396
- (_focusableHandleRef$c = focusableHandleRef.current) == null || _focusableHandleRef$c.focus();
30464
+ (_focusableHandleRef$c2 = focusableHandleRef.current) == null || _focusableHandleRef$c2.focus();
30397
30465
  },
30398
30466
  point: point,
30399
30467
  dragging: dragging,
@@ -30431,7 +30499,6 @@ const MovableLine = props => {
30431
30499
  points: [start, end],
30432
30500
  ariaLabels,
30433
30501
  ariaDescribedBy,
30434
- color,
30435
30502
  extend,
30436
30503
  onMoveLine = () => {},
30437
30504
  onMovePoint = () => {}
@@ -30466,7 +30533,6 @@ const MovableLine = props => {
30466
30533
  ariaLive: ariaLives[0],
30467
30534
  point: start,
30468
30535
  sequenceNumber: 1,
30469
- color,
30470
30536
  onMove: p => {
30471
30537
  setAriaLives(["polite", "off", "off"]);
30472
30538
  onMovePoint(0, p);
@@ -30482,7 +30548,6 @@ const MovableLine = props => {
30482
30548
  ariaLive: ariaLives[1],
30483
30549
  point: end,
30484
30550
  sequenceNumber: 2,
30485
- color,
30486
30551
  onMove: p => {
30487
30552
  setAriaLives(["off", "polite", "off"]);
30488
30553
  onMovePoint(1, p);
@@ -30495,7 +30560,6 @@ const MovableLine = props => {
30495
30560
  ariaLive: ariaLives[2],
30496
30561
  start: start,
30497
30562
  end: end,
30498
- stroke: color,
30499
30563
  extend: extend,
30500
30564
  onMove: delta => {
30501
30565
  setAriaLives(["off", "off", "polite"]);
@@ -30504,7 +30568,6 @@ const MovableLine = props => {
30504
30568
  });
30505
30569
  return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle1, line, focusableHandle2, visiblePoint1, visiblePoint2);
30506
30570
  };
30507
- const defaultStroke = "var(--movable-line-stroke-color)";
30508
30571
  const Line$1 = props => {
30509
30572
  const {
30510
30573
  start,
@@ -30513,7 +30576,6 @@ const Line$1 = props => {
30513
30576
  ariaDescribedBy,
30514
30577
  ariaLive,
30515
30578
  extend,
30516
- stroke = defaultStroke,
30517
30579
  onMove
30518
30580
  } = props;
30519
30581
  const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
@@ -30521,7 +30583,8 @@ const Line$1 = props => {
30521
30583
  range,
30522
30584
  graphDimensionsInPixels,
30523
30585
  snapStep,
30524
- disableKeyboardInteraction
30586
+ disableKeyboardInteraction,
30587
+ interactiveColor
30525
30588
  } = useGraphConfig();
30526
30589
  let startExtend = undefined;
30527
30590
  let endExtend = undefined;
@@ -30578,7 +30641,7 @@ const Line$1 = props => {
30578
30641
  start: startPtPx,
30579
30642
  end: endPtPx,
30580
30643
  style: {
30581
- stroke,
30644
+ stroke: interactiveColor,
30582
30645
  strokeWidth: "var(--movable-line-stroke-weight)"
30583
30646
  },
30584
30647
  className: dragging ? "movable-dragging" : "",
@@ -30586,11 +30649,11 @@ const Line$1 = props => {
30586
30649
  })), startExtend && /*#__PURE__*/React.createElement(Vector, {
30587
30650
  tail: start,
30588
30651
  tip: startExtend,
30589
- color: stroke
30652
+ testId: "movable-line__vector"
30590
30653
  }), endExtend && /*#__PURE__*/React.createElement(Vector, {
30591
30654
  tail: end,
30592
30655
  tip: endExtend,
30593
- color: stroke
30656
+ testId: "movable-line__vector"
30594
30657
  }));
30595
30658
  };
30596
30659
  const getMovableLineKeyboardConstraint = (line, snapStep, pointIndex) => {
@@ -30662,7 +30725,8 @@ function AngleGraph(props) {
30662
30725
  graphState
30663
30726
  } = props;
30664
30727
  const {
30665
- graphDimensionsInPixels
30728
+ graphDimensionsInPixels,
30729
+ interactiveColor
30666
30730
  } = useGraphConfig();
30667
30731
  const i18n = usePerseusI18n();
30668
30732
  const id = React.useId();
@@ -30693,13 +30757,14 @@ function AngleGraph(props) {
30693
30757
  start: startPtPx,
30694
30758
  end: endPtPx,
30695
30759
  style: {
30696
- stroke: "var(--movable-line-stroke-color)",
30760
+ stroke: interactiveColor,
30697
30761
  strokeWidth: "var(--movable-line-stroke-weight)"
30698
- }
30762
+ },
30763
+ testId: "angle-graph__line"
30699
30764
  }), /*#__PURE__*/React.createElement(Vector, {
30700
30765
  tail: angleLines[i][1],
30701
30766
  tip: endExtend,
30702
- color: "var(--movable-line-stroke-color)"
30767
+ testId: "angle-graph__vector"
30703
30768
  }));
30704
30769
  });
30705
30770
 
@@ -31043,7 +31108,8 @@ function MovableCircle(props) {
31043
31108
  } = props;
31044
31109
  const {
31045
31110
  snapStep,
31046
- disableKeyboardInteraction
31111
+ disableKeyboardInteraction,
31112
+ interactiveColor
31047
31113
  } = useGraphConfig();
31048
31114
  const [focused, setFocused] = React.useState(false);
31049
31115
  const draggableRef = useRef(null);
@@ -31079,7 +31145,9 @@ function MovableCircle(props) {
31079
31145
  cx: centerPx[X],
31080
31146
  cy: centerPx[Y],
31081
31147
  rx: radiiPx[X],
31082
- ry: radiiPx[Y]
31148
+ ry: radiiPx[Y],
31149
+ stroke: interactiveColor,
31150
+ "data-testid": "movable-circle__circle"
31083
31151
  }), /*#__PURE__*/React.createElement(DragHandle, {
31084
31152
  center: center,
31085
31153
  dragging: dragging,
@@ -31096,7 +31164,8 @@ function DragHandle(props) {
31096
31164
  } = props;
31097
31165
  const [centerPx] = useTransformVectorsToPixels(center);
31098
31166
  const {
31099
- markings
31167
+ markings,
31168
+ interactiveColor
31100
31169
  } = useGraphConfig();
31101
31170
  const cornerRadius = Math.min(...dragHandleDimensions) / 2;
31102
31171
  const topLeft = vec.sub(centerPx, vec.scale(dragHandleDimensions, 0.5));
@@ -31110,7 +31179,9 @@ function DragHandle(props) {
31110
31179
  width: dragHandleDimensions[X],
31111
31180
  height: dragHandleDimensions[Y],
31112
31181
  rx: cornerRadius,
31113
- ry: cornerRadius
31182
+ ry: cornerRadius,
31183
+ fill: interactiveColor,
31184
+ "data-testid": "movable-circle__handle"
31114
31185
  }), dragHandleDotPositions.map(offsetPx => {
31115
31186
  const [xPx, yPx] = vec.add(offsetPx, centerPx);
31116
31187
  return /*#__PURE__*/React.createElement("circle", {
@@ -31269,8 +31340,7 @@ const LinearGraph = (props, key) => {
31269
31340
  start: true,
31270
31341
  end: true
31271
31342
  },
31272
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination)),
31273
- color: "var(--movable-line-stroke-color)"
31343
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linear.movePoint(endpointIndex, destination))
31274
31344
  }), /*#__PURE__*/React.createElement("g", {
31275
31345
  id: pointsDescriptionId,
31276
31346
  style: a11y.srOnly
@@ -31409,8 +31479,7 @@ const LinearSystemGraph = props => {
31409
31479
  start: true,
31410
31480
  end: true
31411
31481
  },
31412
- onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination)),
31413
- color: "var(--movable-line-stroke-color)"
31482
+ onMovePoint: (endpointIndex, destination) => dispatch(actions.linearSystem.movePointInFigure(i, endpointIndex, destination))
31414
31483
  })), linesAriaInfo.map(({
31415
31484
  pointsDescriptionId,
31416
31485
  interceptDescriptionId,
@@ -32226,6 +32295,7 @@ function doMovePoint(state, action) {
32226
32295
  {
32227
32296
  return _extends({}, state, {
32228
32297
  hasBeenInteractedWith: true,
32298
+ focusedPointIndex: action.index,
32229
32299
  coords: setAtIndex({
32230
32300
  array: state.coords,
32231
32301
  index: action.index,
@@ -32545,7 +32615,9 @@ function boundAndSnapToPolygonAngle(destinationPoint, {
32545
32615
  coords
32546
32616
  }, index) {
32547
32617
  const startingPoint = coords[index];
32548
-
32618
+ return calculateAngleSnap(destinationPoint, range, coords, index, startingPoint);
32619
+ }
32620
+ function calculateAngleSnap(destinationPoint, range, coords, index, startingPoint) {
32549
32621
  // Needed to prevent updating the original coords before the checks for
32550
32622
  // degenerate triangles and overlapping sides
32551
32623
  const coordsCopy = [...coords];
@@ -32834,7 +32906,8 @@ const LimitedPolygonGraph = statefulProps => {
32834
32906
  snapStep
32835
32907
  } = statefulProps.graphState;
32836
32908
  const {
32837
- disableKeyboardInteraction
32909
+ disableKeyboardInteraction,
32910
+ interactiveColor
32838
32911
  } = graphConfig;
32839
32912
  const {
32840
32913
  strings,
@@ -32870,12 +32943,16 @@ const LimitedPolygonGraph = statefulProps => {
32870
32943
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
32871
32944
  }, /*#__PURE__*/React.createElement(Polygon, {
32872
32945
  points: [...points],
32873
- color: "var(--movable-line-stroke-color)",
32946
+ color: interactiveColor,
32874
32947
  svgPolygonProps: {
32875
32948
  strokeWidth: focusVisible ? "var(--movable-line-stroke-weight-active)" : "var(--movable-line-stroke-weight)",
32876
32949
  style: {
32877
32950
  fill: "transparent"
32878
- }
32951
+ },
32952
+ // Use aria-hidden to hide the line from screen readers
32953
+ // so it doesn't read as "image" with no context.
32954
+ // This is okay because the graph has its own aria-label.
32955
+ "aria-hidden": true
32879
32956
  }
32880
32957
  }), points.map((point, i) => {
32881
32958
  const pt1 = points.at(i - 1);
@@ -33015,6 +33092,9 @@ const UnlimitedPolygonGraph = statefulProps => {
33015
33092
  strings,
33016
33093
  locale
33017
33094
  } = usePerseusI18n();
33095
+ const {
33096
+ interactiveColor
33097
+ } = useGraphConfig();
33018
33098
  const id = React.useId();
33019
33099
  const polygonPointsNumId = id + "-points-num";
33020
33100
  const polygonPointsId = id + "-points";
@@ -33054,14 +33134,22 @@ const UnlimitedPolygonGraph = statefulProps => {
33054
33134
  "aria-describedby": `${polygonPointsNumId} ${polygonPointsId}`
33055
33135
  }, /*#__PURE__*/React.createElement(Polyline, {
33056
33136
  points: [...points],
33057
- color: "var(--movable-line-stroke-color)",
33137
+ color: interactiveColor,
33058
33138
  svgPolylineProps: {
33059
33139
  strokeWidth: "var(--movable-line-stroke-weight)",
33060
33140
  style: {
33061
33141
  fill: "transparent"
33062
- }
33142
+ },
33143
+ // Use aria-hidden to hide the line from screen readers
33144
+ // so it doesn't read as "image" with no context.
33145
+ // This is okay because the graph has its own aria-label.
33146
+ "aria-hidden": true
33063
33147
  }
33064
33148
  }), /*#__PURE__*/React.createElement("rect", {
33149
+ // Use aria-hidden to hide the line from screen readers
33150
+ // so it doesn't read as "image" with no context.
33151
+ // This is okay because the graph has its own aria-label.
33152
+ "aria-hidden": true,
33065
33153
  style: {
33066
33154
  fill: "rgba(0,0,0,0)",
33067
33155
  cursor: "crosshair"
@@ -33215,7 +33303,7 @@ function getKeyboardMovementConstraintForPoint(points, index, range, snapStep, s
33215
33303
  case "sides":
33216
33304
  return getSideSnapConstraint(points, index, range);
33217
33305
  case "angles":
33218
- return p => p;
33306
+ return getAngleSnapConstraint(points, index, range);
33219
33307
  default:
33220
33308
  throw new UnreachableCaseError(snapTo);
33221
33309
  }
@@ -33269,6 +33357,49 @@ function getSideSnapConstraint(points, index, range) {
33269
33357
  right: movePointWithConstraint(coord => vec.add(coord, [1, 0]))
33270
33358
  };
33271
33359
  }
33360
+ function getAngleSnapConstraint(points, index, range) {
33361
+ // Make newPoints mutable.
33362
+ const newPoints = [...points];
33363
+
33364
+ // Get the point that is being moved.
33365
+ const pointToBeMoved = newPoints[index];
33366
+
33367
+ // Create a helper function that moves the point to a valid location
33368
+ // for angle snapping.
33369
+ const movePointWithConstraint = moveFunc => {
33370
+ // The direction the user is attempting to move the point in.
33371
+ let destinationAttempt = bound$1({
33372
+ snapStep: [0, 0],
33373
+ range,
33374
+ point: moveFunc(pointToBeMoved)
33375
+ });
33376
+ // The new point we're moving to.
33377
+ let newPoint = pointToBeMoved;
33378
+
33379
+ // Move the point and keep trying until we are at the boarder
33380
+ // of the graph.
33381
+ while (newPoint[0] === pointToBeMoved[0] && newPoint[1] === pointToBeMoved[1] && isInBound({
33382
+ range,
33383
+ point: destinationAttempt
33384
+ })) {
33385
+ newPoint = calculateAngleSnap(destinationAttempt, range, newPoints, index, pointToBeMoved);
33386
+
33387
+ // Increment the destinationAttempt.
33388
+ // For every time it does not work increment the direction for x and y.
33389
+ destinationAttempt = moveFunc(destinationAttempt);
33390
+ }
33391
+ return newPoint;
33392
+ };
33393
+
33394
+ // For each direction look for the next movable point by a small step to increase changes
33395
+ // of finding the next angle value.
33396
+ return {
33397
+ up: movePointWithConstraint(coord => vec.add(coord, [0, 0.1])),
33398
+ down: movePointWithConstraint(coord => vec.sub(coord, [0, 0.1])),
33399
+ left: movePointWithConstraint(coord => vec.sub(coord, [0.1, 0])),
33400
+ right: movePointWithConstraint(coord => vec.add(coord, [0.1, 0]))
33401
+ };
33402
+ }
33272
33403
 
33273
33404
  function renderQuadraticGraph(state, dispatch, i18n) {
33274
33405
  return {
@@ -33288,6 +33419,9 @@ function QuadraticGraph(props) {
33288
33419
  coords,
33289
33420
  snapStep
33290
33421
  } = graphState;
33422
+ const {
33423
+ interactiveColor
33424
+ } = useGraphConfig();
33291
33425
  const {
33292
33426
  strings,
33293
33427
  locale
@@ -33329,7 +33463,13 @@ function QuadraticGraph(props) {
33329
33463
  "aria-describedby": `${quadraticDirectionId} ${quadraticVertexId} ${quadraticInterceptsId}`
33330
33464
  }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
33331
33465
  y: y,
33332
- color: color.blue
33466
+ color: interactiveColor,
33467
+ svgPathProps: {
33468
+ // Use aria-hidden to hide the line from screen readers
33469
+ // so it doesn't read as "image" with no context.
33470
+ // This is okay because the graph has its own aria-label.
33471
+ "aria-hidden": true
33472
+ }
33333
33473
  }), coords.map((coord, i) => {
33334
33474
  const srQuadraticPoint = getQuadraticPointString(i + 1, coord, strings, locale);
33335
33475
  // Vertex might not exist if the quadratic graph is a line
@@ -33724,6 +33864,9 @@ function SinusoidGraph(props) {
33724
33864
  dispatch,
33725
33865
  graphState
33726
33866
  } = props;
33867
+ const {
33868
+ interactiveColor
33869
+ } = useGraphConfig();
33727
33870
  const i18n = usePerseusI18n();
33728
33871
  const id = React.useId();
33729
33872
  const descriptionId = id + "-description";
@@ -33766,7 +33909,13 @@ function SinusoidGraph(props) {
33766
33909
  "aria-describedby": descriptionId
33767
33910
  }, /*#__PURE__*/React.createElement(Plot$2.OfX, {
33768
33911
  y: x => computeSine(x, coeffRef.current),
33769
- color: color.blue
33912
+ color: interactiveColor,
33913
+ svgPathProps: {
33914
+ // Use aria-hidden to hide the line from screen readers
33915
+ // so it doesn't read as "image" with no context.
33916
+ // This is okay because the graph has its own aria-label.
33917
+ "aria-hidden": true
33918
+ }
33770
33919
  }), coords.map((coord, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
33771
33920
  ariaLabel: i === 0 ? srSinusoidRootPoint : srSinusoidPeakPoint,
33772
33921
  key: "point-" + i,
@@ -34115,6 +34264,7 @@ const MafsGraph = props => {
34115
34264
  i18n,
34116
34265
  markings: props.markings
34117
34266
  });
34267
+ const disableInteraction = readOnly || !!props.static;
34118
34268
  return /*#__PURE__*/React.createElement(GraphConfigContext.Provider, {
34119
34269
  value: {
34120
34270
  range: state.range,
@@ -34127,7 +34277,12 @@ const MafsGraph = props => {
34127
34277
  width,
34128
34278
  height,
34129
34279
  labels,
34130
- disableKeyboardInteraction: readOnly || !!props.static
34280
+ disableKeyboardInteraction: disableInteraction,
34281
+ // If the graph is read-only or static, we want to make it
34282
+ // visually clear that the graph is no longer interactive.
34283
+ // We do this by changing the color of the interactive elements
34284
+ // to a static gray color rather than our standard blue.
34285
+ interactiveColor: disableInteraction ? "var(--static-gray)" : "var(--mafs-blue)"
34131
34286
  }
34132
34287
  }, /*#__PURE__*/React.createElement(View, {
34133
34288
  className: "mafs-graph-container"
@@ -35143,7 +35298,8 @@ InteractiveGraph.defaultProps = {
35143
35298
  const staticTransform$3 = _.identity;
35144
35299
  var InteractiveGraph$1 = {
35145
35300
  name: "interactive-graph",
35146
- displayName: "Interactive graph (Assessments only)",
35301
+ displayName: "Interactive graph",
35302
+ accessible: true,
35147
35303
  widget: InteractiveGraph,
35148
35304
  staticTransform: staticTransform$3
35149
35305
  };
@@ -41232,7 +41388,7 @@ const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
41232
41388
 
41233
41389
  // This file is processed by a Rollup plugin (replace) to inject the production
41234
41390
  const libName = "@khanacademy/perseus";
41235
- const libVersion = "56.0.0";
41391
+ const libVersion = "56.2.0";
41236
41392
  addLibraryVersionToPerseusDebug(libName, libVersion);
41237
41393
 
41238
41394
  const apiVersion = {