@khanacademy/perseus 22.4.0 → 22.4.1

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
@@ -4419,7 +4419,9 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4419
4419
  );
4420
4420
  }
4421
4421
 
4422
- .MafsView .movable-point:focus-visible .movable-point-focus-outline {
4422
+ .MafsView
4423
+ .movable-point:is(:focus-visible, .movable-point--focus)
4424
+ .movable-point-focus-outline {
4423
4425
  visibility: visible;
4424
4426
  }
4425
4427
 
package/dist/index.js CHANGED
@@ -477,7 +477,7 @@ class InnerMathInput extends React__namespace.Component {
477
477
  locale
478
478
  } = this.context;
479
479
  // Initialize MathQuill.MathField instance
480
- this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, this.props.mathInputStrings, baseConfig => ({
480
+ this.__mathField = mathInput.createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings, baseConfig => ({
481
481
  ...baseConfig,
482
482
  handlers: {
483
483
  edit: debounce(mathField => {
@@ -29333,6 +29333,194 @@ function snap(snapStep, point) {
29333
29333
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29334
29334
  }
29335
29335
 
29336
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
29337
+ // graph.
29338
+ const hitboxSizePx = 48;
29339
+
29340
+ // MovablePointView is a purely presentational component (i.e. it is a pure
29341
+ // function with no state or effects) that renders the SVG for a movable point
29342
+ // on an interactive graph.
29343
+ //
29344
+ // It has two modes for managing tabbing / focus: "controlled" (where the caller
29345
+ // manages the display of the focus ring, and the point itself cannot be
29346
+ // focused) and "uncontrolled" (where the point is focusable, and the browser
29347
+ // manages the focus state and styling). For context on why we did this, see
29348
+ // the description of https://github.com/Khan/perseus/pull/1240
29349
+ const MovablePointView = /*#__PURE__*/React.forwardRef((props, hitboxRef) => {
29350
+ const {
29351
+ range,
29352
+ markings,
29353
+ showTooltips
29354
+ } = useGraphConfig();
29355
+ const {
29356
+ point,
29357
+ color = wonderBlocksTokens.color.blue,
29358
+ dragging,
29359
+ focusBehavior
29360
+ } = props;
29361
+
29362
+ // WB Tooltip requires a color name for the background color.
29363
+ // Since the color in props is a hex value, a reverse lookup is needed.
29364
+ const wbColorName = Object.entries(wonderBlocksTokens.color).find(_ref => {
29365
+ let [_, value] = _ref;
29366
+ return value === color;
29367
+ })?.[0] ?? "blue";
29368
+ const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""} ${focusClass(focusBehavior)}`;
29369
+ const [[x, y]] = useTransformVectorsToPixels(point);
29370
+ const [xMin, xMax] = range[0];
29371
+ const [yMin, yMax] = range[1];
29372
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29373
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29374
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29375
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29376
+ const showHairlines = dragging && markings !== "none";
29377
+ const hairlines = /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
29378
+ x1: verticalStartX,
29379
+ y1: y,
29380
+ x2: verticalEndX,
29381
+ y2: y,
29382
+ stroke: color
29383
+ }), /*#__PURE__*/React__namespace.createElement("line", {
29384
+ x1: x,
29385
+ y1: horizontalStartY,
29386
+ x2: x,
29387
+ y2: horizontalEndY,
29388
+ stroke: color
29389
+ }));
29390
+ const svgForPoint = /*#__PURE__*/React__namespace.createElement("g", {
29391
+ ref: hitboxRef,
29392
+ className: pointClasses,
29393
+ style: {
29394
+ "--movable-point-color": color
29395
+ },
29396
+ "data-testid": "movable-point",
29397
+ tabIndex: tabIndex(focusBehavior)
29398
+ }, /*#__PURE__*/React__namespace.createElement("circle", {
29399
+ className: "movable-point-hitbox",
29400
+ r: hitboxSizePx / 2,
29401
+ cx: x,
29402
+ cy: y
29403
+ }), /*#__PURE__*/React__namespace.createElement("circle", {
29404
+ className: "movable-point-halo",
29405
+ cx: x,
29406
+ cy: y
29407
+ }), /*#__PURE__*/React__namespace.createElement("circle", {
29408
+ className: "movable-point-ring",
29409
+ cx: x,
29410
+ cy: y
29411
+ }), /*#__PURE__*/React__namespace.createElement("circle", {
29412
+ className: "movable-point-focus-outline",
29413
+ cx: x,
29414
+ cy: y
29415
+ }), /*#__PURE__*/React__namespace.createElement("circle", {
29416
+ className: "movable-point-center",
29417
+ cx: x,
29418
+ cy: y,
29419
+ style: {
29420
+ fill: color
29421
+ },
29422
+ "data-testid": "movable-point__center"
29423
+ }));
29424
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
29425
+ autoUpdate: true,
29426
+ backgroundColor: wbColorName,
29427
+ content: `(${point[0]}, ${point[1]})`,
29428
+ contentStyle: {
29429
+ color: "white"
29430
+ }
29431
+ }, svgForPoint) : svgForPoint);
29432
+ });
29433
+ function focusClass(config) {
29434
+ if (config.type === "controlled" && config.showFocusRing) {
29435
+ return "movable-point--focus";
29436
+ }
29437
+ return "";
29438
+ }
29439
+ function tabIndex(config) {
29440
+ if (config.type === "uncontrolled") {
29441
+ return config.tabIndex;
29442
+ }
29443
+ return undefined;
29444
+ }
29445
+
29446
+ const Segment = props => {
29447
+ const {
29448
+ onMoveLine,
29449
+ color,
29450
+ points: [start, end],
29451
+ extend
29452
+ } = props;
29453
+
29454
+ // We use separate focusableHandle elements, instead of letting the movable
29455
+ // points themselves be focusable, to allow the tab order of the points to
29456
+ // be different from the rendering order. We had to solve for the following
29457
+ // constraints:
29458
+ // - SVG has no equivalent of z-index, so the order of elements in the
29459
+ // document determines the order in which they're painted. We want the
29460
+ // movable line segment to render behind its endpoints (it looks weird
29461
+ // otherwise) so we have to render the line first.
29462
+ // - There isn't a browser-native way to customize tab order, other than
29463
+ // setting tabindex > 0. But that bumps elements to the front of the
29464
+ // tab order for the entire page, which is not what we want.
29465
+ const {
29466
+ visiblePoint: visiblePoint1,
29467
+ focusableHandle: focusableHandle1
29468
+ } = useControlPoint(start, color, p => props.onMovePoint(0, p));
29469
+ const {
29470
+ visiblePoint: visiblePoint2,
29471
+ focusableHandle: focusableHandle2
29472
+ } = useControlPoint(end, color, p => props.onMovePoint(1, p));
29473
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, focusableHandle1, /*#__PURE__*/React__namespace.createElement(MovableLine, {
29474
+ start: start,
29475
+ end: end,
29476
+ stroke: color,
29477
+ extend: extend,
29478
+ onMove: onMoveLine
29479
+ }), focusableHandle2, visiblePoint1, visiblePoint2);
29480
+ };
29481
+ function useControlPoint(point, color, onMovePoint) {
29482
+ const {
29483
+ snapStep
29484
+ } = useGraphConfig();
29485
+ const [focused, setFocused] = React.useState(false);
29486
+ const keyboardHandleRef = React.useRef(null);
29487
+ mafs.useMovable({
29488
+ gestureTarget: keyboardHandleRef,
29489
+ point,
29490
+ onMove: onMovePoint,
29491
+ constrain: p => snap(snapStep, p)
29492
+ });
29493
+ const visiblePointRef = React.useRef(null);
29494
+ const {
29495
+ dragging
29496
+ } = mafs.useMovable({
29497
+ gestureTarget: visiblePointRef,
29498
+ point,
29499
+ onMove: onMovePoint,
29500
+ constrain: p => snap(snapStep, p)
29501
+ });
29502
+ const focusableHandle = /*#__PURE__*/React__namespace.createElement("g", {
29503
+ "data-testid": "movable-point__focusable-handle",
29504
+ tabIndex: 0,
29505
+ ref: keyboardHandleRef,
29506
+ onFocus: () => setFocused(true),
29507
+ onBlur: () => setFocused(false)
29508
+ });
29509
+ const visiblePoint = /*#__PURE__*/React__namespace.createElement(MovablePointView, {
29510
+ point: point,
29511
+ dragging: dragging,
29512
+ color: color,
29513
+ ref: visiblePointRef,
29514
+ focusBehavior: {
29515
+ type: "controlled",
29516
+ showFocusRing: focused
29517
+ }
29518
+ });
29519
+ return {
29520
+ focusableHandle,
29521
+ visiblePoint
29522
+ };
29523
+ }
29336
29524
  const defaultStroke = "var(--movable-line-stroke-color)";
29337
29525
  const MovableLine = props => {
29338
29526
  const {
@@ -29345,12 +29533,13 @@ const MovableLine = props => {
29345
29533
  const midpoint = mafs.vec.midpoint(start, end);
29346
29534
  const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
29347
29535
  const {
29536
+ range,
29348
29537
  graphDimensionsInPixels
29349
29538
  } = useGraphConfig();
29350
29539
  let startExtend = undefined;
29351
29540
  let endExtend = undefined;
29352
29541
  if (extend) {
29353
- const trimmedRange = trimRange(extend.range, graphDimensionsInPixels);
29542
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
29354
29543
  startExtend = extend.start ? getIntersectionOfRayWithBox(start, end, trimmedRange) : undefined;
29355
29544
  endExtend = extend.end ? getIntersectionOfRayWithBox(end, start, trimmedRange) : undefined;
29356
29545
  }
@@ -29429,103 +29618,6 @@ function size(interval) {
29429
29618
  return interval[1] - interval[0];
29430
29619
  }
29431
29620
 
29432
- // The hitbox size of 48px by 48px is preserved from the legacy interactive
29433
- // graph.
29434
- const hitboxSizePx = 48;
29435
- const StyledMovablePoint = props => {
29436
- const {
29437
- range,
29438
- snapStep,
29439
- markings,
29440
- showTooltips
29441
- } = useGraphConfig();
29442
- const hitboxRef = React.useRef(null);
29443
- const {
29444
- point,
29445
- onMove,
29446
- color = wonderBlocksTokens.color.blue
29447
- } = props;
29448
-
29449
- // WB Tooltip requires a color name for the background color.
29450
- // Since the color in props is a hex value, a reverse lookup is needed.
29451
- const wbColorName = Object.entries(wonderBlocksTokens.color).find(_ref => {
29452
- let [_, value] = _ref;
29453
- return value === color;
29454
- })?.[0] ?? "blue";
29455
- const {
29456
- dragging
29457
- } = mafs.useMovable({
29458
- gestureTarget: hitboxRef,
29459
- point,
29460
- onMove,
29461
- constrain: p => snap(snapStep, p)
29462
- });
29463
- const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""}`;
29464
- const [[x, y]] = useTransformVectorsToPixels(point);
29465
- const [xMin, xMax] = range[0];
29466
- const [yMin, yMax] = range[1];
29467
- const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29468
- const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29469
- const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29470
- const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29471
- const showHairlines = dragging && markings !== "none";
29472
- const hairlines = /*#__PURE__*/React__namespace.createElement("g", null, /*#__PURE__*/React__namespace.createElement("line", {
29473
- x1: verticalStartX,
29474
- y1: y,
29475
- x2: verticalEndX,
29476
- y2: y,
29477
- stroke: color
29478
- }), /*#__PURE__*/React__namespace.createElement("line", {
29479
- x1: x,
29480
- y1: horizontalStartY,
29481
- x2: x,
29482
- y2: horizontalEndY,
29483
- stroke: color
29484
- }));
29485
- const svgForPoint = /*#__PURE__*/React__namespace.createElement("g", {
29486
- ref: hitboxRef,
29487
- className: pointClasses,
29488
- tabIndex: 0,
29489
- style: {
29490
- "--movable-point-color": color
29491
- },
29492
- "data-testid": "movable-point"
29493
- }, /*#__PURE__*/React__namespace.createElement("circle", {
29494
- className: "movable-point-hitbox",
29495
- r: hitboxSizePx / 2,
29496
- cx: x,
29497
- cy: y
29498
- }), /*#__PURE__*/React__namespace.createElement("circle", {
29499
- className: "movable-point-halo",
29500
- cx: x,
29501
- cy: y
29502
- }), /*#__PURE__*/React__namespace.createElement("circle", {
29503
- className: "movable-point-ring",
29504
- cx: x,
29505
- cy: y
29506
- }), /*#__PURE__*/React__namespace.createElement("circle", {
29507
- className: "movable-point-focus-outline",
29508
- cx: x,
29509
- cy: y
29510
- }), /*#__PURE__*/React__namespace.createElement("circle", {
29511
- className: "movable-point-center",
29512
- cx: x,
29513
- cy: y,
29514
- style: {
29515
- fill: color
29516
- },
29517
- "data-testid": "movable-point__center"
29518
- }));
29519
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React__namespace.createElement(Tooltip__default["default"], {
29520
- autoUpdate: true,
29521
- backgroundColor: wbColorName,
29522
- content: `(${point[0]}, ${point[1]})`,
29523
- contentStyle: {
29524
- color: "white"
29525
- }
29526
- }, svgForPoint) : svgForPoint);
29527
- };
29528
-
29529
29621
  const SegmentGraph = props => {
29530
29622
  const {
29531
29623
  dispatch
@@ -29533,7 +29625,7 @@ const SegmentGraph = props => {
29533
29625
  const {
29534
29626
  coords: segments
29535
29627
  } = props.graphState;
29536
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement(SegmentView, {
29628
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, segments?.map((segment, i) => /*#__PURE__*/React__namespace.createElement(Segment, {
29537
29629
  key: i,
29538
29630
  points: segment,
29539
29631
  onMoveLine: delta => {
@@ -29544,27 +29636,6 @@ const SegmentGraph = props => {
29544
29636
  }
29545
29637
  })));
29546
29638
  };
29547
- const SegmentView = props => {
29548
- const {
29549
- onMoveLine: onMoveSegment,
29550
- points: [start, end]
29551
- } = props;
29552
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableLine, {
29553
- start: start,
29554
- end: end,
29555
- onMove: onMoveSegment
29556
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29557
- point: start,
29558
- onMove: newPoint => {
29559
- props.onMovePoint(0, newPoint);
29560
- }
29561
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29562
- point: end,
29563
- onMove: newPoint => {
29564
- props.onMovePoint(1, newPoint);
29565
- }
29566
- }));
29567
- };
29568
29639
 
29569
29640
  const LinearGraph = props => {
29570
29641
  const {
@@ -29574,49 +29645,19 @@ const LinearGraph = props => {
29574
29645
  coords: lines
29575
29646
  } = props.graphState;
29576
29647
  const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29577
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(LineView, {
29648
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lines?.map((line, i) => /*#__PURE__*/React__namespace.createElement(Segment, {
29578
29649
  key: i,
29579
29650
  points: line,
29580
29651
  onMoveLine: delta => {
29581
29652
  dispatch(moveLine(i, delta));
29582
29653
  },
29583
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29584
- stroke: colors[i]
29585
- })));
29586
- };
29587
- const LineView = props => {
29588
- const {
29589
- onMoveLine,
29590
- onMovePoint,
29591
- points: [start, end],
29592
- stroke
29593
- } = props;
29594
- const {
29595
- range
29596
- } = useGraphConfig();
29597
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableLine, {
29598
- start: start,
29599
- end: end,
29600
- onMove: onMoveLine,
29601
29654
  extend: {
29602
29655
  start: true,
29603
- end: true,
29604
- range
29605
- },
29606
- stroke: stroke
29607
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29608
- point: start,
29609
- onMove: newPoint => {
29610
- onMovePoint(0, newPoint);
29611
- },
29612
- color: stroke
29613
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29614
- point: end,
29615
- onMove: newPoint => {
29616
- onMovePoint(1, newPoint);
29656
+ end: true
29617
29657
  },
29618
- color: stroke
29619
- }));
29658
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29659
+ color: colors[i]
29660
+ })));
29620
29661
  };
29621
29662
 
29622
29663
  const RayGraph = props => {
@@ -29624,30 +29665,21 @@ const RayGraph = props => {
29624
29665
  dispatch
29625
29666
  } = props;
29626
29667
  const {
29627
- coords: lines,
29628
- range
29668
+ coords: lines
29629
29669
  } = props.graphState;
29630
-
29631
- // a ray only has one line
29632
- const [start, end] = lines[0];
29633
29670
  const handleMoveLine = delta => dispatch(moveLine(0, delta));
29634
- const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29635
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(MovableLine, {
29636
- start: start,
29637
- end: end,
29638
- onMove: handleMoveLine,
29671
+ const handleMovePoint = (pointIndex, newPoint) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29672
+ return /*#__PURE__*/React__namespace.createElement(Segment
29673
+ // a ray only has one line
29674
+ , {
29675
+ points: lines[0],
29676
+ onMoveLine: handleMoveLine,
29677
+ onMovePoint: handleMovePoint,
29639
29678
  extend: {
29640
- end: true,
29641
29679
  start: false,
29642
- range
29680
+ end: true
29643
29681
  }
29644
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29645
- point: start,
29646
- onMove: newPoint => handleMovePoint(newPoint, 0)
29647
- }), /*#__PURE__*/React__namespace.createElement(StyledMovablePoint, {
29648
- point: end,
29649
- onMove: newPoint => handleMovePoint(newPoint, 1)
29650
- }));
29682
+ });
29651
29683
  };
29652
29684
 
29653
29685
  /**
@@ -29819,6 +29851,36 @@ const linesIntersect = (_ref3, _ref4) => {
29819
29851
  };
29820
29852
  const isEven = n => n % 2 === 0;
29821
29853
 
29854
+ const StyledMovablePoint = props => {
29855
+ const {
29856
+ snapStep
29857
+ } = useGraphConfig();
29858
+ const elementRef = React.useRef(null);
29859
+ const {
29860
+ point,
29861
+ onMove,
29862
+ color = wonderBlocksTokens.color.blue
29863
+ } = props;
29864
+ const {
29865
+ dragging
29866
+ } = mafs.useMovable({
29867
+ gestureTarget: elementRef,
29868
+ point,
29869
+ onMove,
29870
+ constrain: p => snap(snapStep, p)
29871
+ });
29872
+ return /*#__PURE__*/React__namespace.createElement(MovablePointView, {
29873
+ ref: elementRef,
29874
+ point: point,
29875
+ color: color,
29876
+ dragging: dragging,
29877
+ focusBehavior: {
29878
+ type: "uncontrolled",
29879
+ tabIndex: 0
29880
+ }
29881
+ });
29882
+ };
29883
+
29822
29884
  const PolygonGraph = props => {
29823
29885
  const [focused, setFocused] = React__namespace.useState(false);
29824
29886
  const [hovered, setHovered] = React__namespace.useState(false);
@@ -29832,17 +29894,19 @@ const PolygonGraph = props => {
29832
29894
  range,
29833
29895
  snapStep
29834
29896
  } = props.graphState;
29897
+
29898
+ // TODO(benchristel): can the default set of points be removed here? I don't
29899
+ // think coords can be null.
29835
29900
  const points = coords ?? [[0, 0]];
29836
- const pointsSum = points.reduce((acc, point) => mafs.vec.add(acc, point), [0, 0]);
29837
- const midpoint = pointsSum && mafs.vec.scale(pointsSum, 1 / (points.length ?? 1));
29838
29901
  const ref = React__namespace.useRef(null);
29902
+ const dragReferencePoint = points[0];
29839
29903
  const {
29840
29904
  dragging
29841
29905
  } = mafs.useMovable({
29842
29906
  gestureTarget: ref,
29843
- point: midpoint,
29907
+ point: dragReferencePoint,
29844
29908
  onMove: newPoint => {
29845
- const delta = mafs.vec.sub(newPoint, midpoint);
29909
+ const delta = mafs.vec.sub(newPoint, dragReferencePoint);
29846
29910
  dispatch(moveAll(delta));
29847
29911
  },
29848
29912
  constrain: p => snap(snapStep, p)
@@ -43559,7 +43623,7 @@ const init = function (options) {
43559
43623
 
43560
43624
  // This file is processed by a Rollup plugin (replace) to inject the production
43561
43625
  const libName = "@khanacademy/perseus";
43562
- const libVersion = "22.4.0";
43626
+ const libVersion = "22.4.1";
43563
43627
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
43564
43628
 
43565
43629
  const apiVersion = {