@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/es/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/es/index.js CHANGED
@@ -6,7 +6,7 @@ import { View, RenderStateRoot, useUniqueIdWithMock, UniqueIDProvider, Text as T
6
6
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
7
7
  import classNames from 'classnames';
8
8
  import * as React from 'react';
9
- import React__default, { useContext, useState, useEffect, useRef, createContext, useImperativeHandle } from 'react';
9
+ import React__default, { useContext, useState, useEffect, useRef, createContext, forwardRef, useImperativeHandle } from 'react';
10
10
  import _, { debounce as debounce$1 } from 'underscore';
11
11
  import Clickable from '@khanacademy/wonder-blocks-clickable';
12
12
  import { Popover, PopoverContentCore, PopoverContent } from '@khanacademy/wonder-blocks-popover';
@@ -341,7 +341,7 @@ class InnerMathInput extends React.Component {
341
341
  locale
342
342
  } = this.context;
343
343
  // Initialize MathQuill.MathField instance
344
- this.__mathField = createMathField(this.__mathFieldWrapperRef, this.props.mathInputStrings, baseConfig => _extends({}, baseConfig, {
344
+ this.__mathField = createMathField(this.__mathFieldWrapperRef, locale, this.props.mathInputStrings, baseConfig => _extends({}, baseConfig, {
345
345
  handlers: {
346
346
  edit: debounce(mathField => {
347
347
  // This handler is guaranteed to be called on change, but
@@ -29046,6 +29046,192 @@ function snap(snapStep, point) {
29046
29046
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29047
29047
  }
29048
29048
 
29049
+ // The hitbox size of 48px by 48px is preserved from the legacy interactive
29050
+ // graph.
29051
+ const hitboxSizePx = 48;
29052
+
29053
+ // MovablePointView is a purely presentational component (i.e. it is a pure
29054
+ // function with no state or effects) that renders the SVG for a movable point
29055
+ // on an interactive graph.
29056
+ //
29057
+ // It has two modes for managing tabbing / focus: "controlled" (where the caller
29058
+ // manages the display of the focus ring, and the point itself cannot be
29059
+ // focused) and "uncontrolled" (where the point is focusable, and the browser
29060
+ // manages the focus state and styling). For context on why we did this, see
29061
+ // the description of https://github.com/Khan/perseus/pull/1240
29062
+ const MovablePointView = /*#__PURE__*/forwardRef((props, hitboxRef) => {
29063
+ var _Object$entries$find$, _Object$entries$find;
29064
+ const {
29065
+ range,
29066
+ markings,
29067
+ showTooltips
29068
+ } = useGraphConfig();
29069
+ const {
29070
+ point,
29071
+ color: color$1 = color.blue,
29072
+ dragging,
29073
+ focusBehavior
29074
+ } = props;
29075
+
29076
+ // WB Tooltip requires a color name for the background color.
29077
+ // Since the color in props is a hex value, a reverse lookup is needed.
29078
+ const wbColorName = (_Object$entries$find$ = (_Object$entries$find = Object.entries(color).find(([_, value]) => value === color$1)) == null ? void 0 : _Object$entries$find[0]) != null ? _Object$entries$find$ : "blue";
29079
+ const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""} ${focusClass(focusBehavior)}`;
29080
+ const [[x, y]] = useTransformVectorsToPixels(point);
29081
+ const [xMin, xMax] = range[0];
29082
+ const [yMin, yMax] = range[1];
29083
+ const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29084
+ const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29085
+ const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29086
+ const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29087
+ const showHairlines = dragging && markings !== "none";
29088
+ const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
29089
+ x1: verticalStartX,
29090
+ y1: y,
29091
+ x2: verticalEndX,
29092
+ y2: y,
29093
+ stroke: color$1
29094
+ }), /*#__PURE__*/React.createElement("line", {
29095
+ x1: x,
29096
+ y1: horizontalStartY,
29097
+ x2: x,
29098
+ y2: horizontalEndY,
29099
+ stroke: color$1
29100
+ }));
29101
+ const svgForPoint = /*#__PURE__*/React.createElement("g", {
29102
+ ref: hitboxRef,
29103
+ className: pointClasses,
29104
+ style: {
29105
+ "--movable-point-color": color$1
29106
+ },
29107
+ "data-testid": "movable-point",
29108
+ tabIndex: tabIndex(focusBehavior)
29109
+ }, /*#__PURE__*/React.createElement("circle", {
29110
+ className: "movable-point-hitbox",
29111
+ r: hitboxSizePx / 2,
29112
+ cx: x,
29113
+ cy: y
29114
+ }), /*#__PURE__*/React.createElement("circle", {
29115
+ className: "movable-point-halo",
29116
+ cx: x,
29117
+ cy: y
29118
+ }), /*#__PURE__*/React.createElement("circle", {
29119
+ className: "movable-point-ring",
29120
+ cx: x,
29121
+ cy: y
29122
+ }), /*#__PURE__*/React.createElement("circle", {
29123
+ className: "movable-point-focus-outline",
29124
+ cx: x,
29125
+ cy: y
29126
+ }), /*#__PURE__*/React.createElement("circle", {
29127
+ className: "movable-point-center",
29128
+ cx: x,
29129
+ cy: y,
29130
+ style: {
29131
+ fill: color$1
29132
+ },
29133
+ "data-testid": "movable-point__center"
29134
+ }));
29135
+ return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
29136
+ autoUpdate: true,
29137
+ backgroundColor: wbColorName,
29138
+ content: `(${point[0]}, ${point[1]})`,
29139
+ contentStyle: {
29140
+ color: "white"
29141
+ }
29142
+ }, svgForPoint) : svgForPoint);
29143
+ });
29144
+ function focusClass(config) {
29145
+ if (config.type === "controlled" && config.showFocusRing) {
29146
+ return "movable-point--focus";
29147
+ }
29148
+ return "";
29149
+ }
29150
+ function tabIndex(config) {
29151
+ if (config.type === "uncontrolled") {
29152
+ return config.tabIndex;
29153
+ }
29154
+ return undefined;
29155
+ }
29156
+
29157
+ const Segment = props => {
29158
+ const {
29159
+ onMoveLine,
29160
+ color,
29161
+ points: [start, end],
29162
+ extend
29163
+ } = props;
29164
+
29165
+ // We use separate focusableHandle elements, instead of letting the movable
29166
+ // points themselves be focusable, to allow the tab order of the points to
29167
+ // be different from the rendering order. We had to solve for the following
29168
+ // constraints:
29169
+ // - SVG has no equivalent of z-index, so the order of elements in the
29170
+ // document determines the order in which they're painted. We want the
29171
+ // movable line segment to render behind its endpoints (it looks weird
29172
+ // otherwise) so we have to render the line first.
29173
+ // - There isn't a browser-native way to customize tab order, other than
29174
+ // setting tabindex > 0. But that bumps elements to the front of the
29175
+ // tab order for the entire page, which is not what we want.
29176
+ const {
29177
+ visiblePoint: visiblePoint1,
29178
+ focusableHandle: focusableHandle1
29179
+ } = useControlPoint(start, color, p => props.onMovePoint(0, p));
29180
+ const {
29181
+ visiblePoint: visiblePoint2,
29182
+ focusableHandle: focusableHandle2
29183
+ } = useControlPoint(end, color, p => props.onMovePoint(1, p));
29184
+ return /*#__PURE__*/React.createElement(React.Fragment, null, focusableHandle1, /*#__PURE__*/React.createElement(MovableLine, {
29185
+ start: start,
29186
+ end: end,
29187
+ stroke: color,
29188
+ extend: extend,
29189
+ onMove: onMoveLine
29190
+ }), focusableHandle2, visiblePoint1, visiblePoint2);
29191
+ };
29192
+ function useControlPoint(point, color, onMovePoint) {
29193
+ const {
29194
+ snapStep
29195
+ } = useGraphConfig();
29196
+ const [focused, setFocused] = useState(false);
29197
+ const keyboardHandleRef = useRef(null);
29198
+ useMovable({
29199
+ gestureTarget: keyboardHandleRef,
29200
+ point,
29201
+ onMove: onMovePoint,
29202
+ constrain: p => snap(snapStep, p)
29203
+ });
29204
+ const visiblePointRef = useRef(null);
29205
+ const {
29206
+ dragging
29207
+ } = useMovable({
29208
+ gestureTarget: visiblePointRef,
29209
+ point,
29210
+ onMove: onMovePoint,
29211
+ constrain: p => snap(snapStep, p)
29212
+ });
29213
+ const focusableHandle = /*#__PURE__*/React.createElement("g", {
29214
+ "data-testid": "movable-point__focusable-handle",
29215
+ tabIndex: 0,
29216
+ ref: keyboardHandleRef,
29217
+ onFocus: () => setFocused(true),
29218
+ onBlur: () => setFocused(false)
29219
+ });
29220
+ const visiblePoint = /*#__PURE__*/React.createElement(MovablePointView, {
29221
+ point: point,
29222
+ dragging: dragging,
29223
+ color: color,
29224
+ ref: visiblePointRef,
29225
+ focusBehavior: {
29226
+ type: "controlled",
29227
+ showFocusRing: focused
29228
+ }
29229
+ });
29230
+ return {
29231
+ focusableHandle,
29232
+ visiblePoint
29233
+ };
29234
+ }
29049
29235
  const defaultStroke = "var(--movable-line-stroke-color)";
29050
29236
  const MovableLine = props => {
29051
29237
  const {
@@ -29058,12 +29244,13 @@ const MovableLine = props => {
29058
29244
  const midpoint = vec.midpoint(start, end);
29059
29245
  const [startPtPx, endPtPx] = useTransformVectorsToPixels(start, end);
29060
29246
  const {
29247
+ range,
29061
29248
  graphDimensionsInPixels
29062
29249
  } = useGraphConfig();
29063
29250
  let startExtend = undefined;
29064
29251
  let endExtend = undefined;
29065
29252
  if (extend) {
29066
- const trimmedRange = trimRange(extend.range, graphDimensionsInPixels);
29253
+ const trimmedRange = trimRange(range, graphDimensionsInPixels);
29067
29254
  startExtend = extend.start ? getIntersectionOfRayWithBox(start, end, trimmedRange) : undefined;
29068
29255
  endExtend = extend.end ? getIntersectionOfRayWithBox(end, start, trimmedRange) : undefined;
29069
29256
  }
@@ -29142,101 +29329,6 @@ function size(interval) {
29142
29329
  return interval[1] - interval[0];
29143
29330
  }
29144
29331
 
29145
- // The hitbox size of 48px by 48px is preserved from the legacy interactive
29146
- // graph.
29147
- const hitboxSizePx = 48;
29148
- const StyledMovablePoint = props => {
29149
- var _Object$entries$find$, _Object$entries$find;
29150
- const {
29151
- range,
29152
- snapStep,
29153
- markings,
29154
- showTooltips
29155
- } = useGraphConfig();
29156
- const hitboxRef = useRef(null);
29157
- const {
29158
- point,
29159
- onMove,
29160
- color: color$1 = color.blue
29161
- } = props;
29162
-
29163
- // WB Tooltip requires a color name for the background color.
29164
- // Since the color in props is a hex value, a reverse lookup is needed.
29165
- const wbColorName = (_Object$entries$find$ = (_Object$entries$find = Object.entries(color).find(([_, value]) => value === color$1)) == null ? void 0 : _Object$entries$find[0]) != null ? _Object$entries$find$ : "blue";
29166
- const {
29167
- dragging
29168
- } = useMovable({
29169
- gestureTarget: hitboxRef,
29170
- point,
29171
- onMove,
29172
- constrain: p => snap(snapStep, p)
29173
- });
29174
- const pointClasses = `movable-point ${dragging ? "movable-point--dragging" : ""}`;
29175
- const [[x, y]] = useTransformVectorsToPixels(point);
29176
- const [xMin, xMax] = range[0];
29177
- const [yMin, yMax] = range[1];
29178
- const [[verticalStartX]] = useTransformVectorsToPixels([xMin, 0]);
29179
- const [[verticalEndX]] = useTransformVectorsToPixels([xMax, 0]);
29180
- const [[_, horizontalStartY]] = useTransformVectorsToPixels([0, yMin]);
29181
- const [[__, horizontalEndY]] = useTransformVectorsToPixels([0, yMax]);
29182
- const showHairlines = dragging && markings !== "none";
29183
- const hairlines = /*#__PURE__*/React.createElement("g", null, /*#__PURE__*/React.createElement("line", {
29184
- x1: verticalStartX,
29185
- y1: y,
29186
- x2: verticalEndX,
29187
- y2: y,
29188
- stroke: color$1
29189
- }), /*#__PURE__*/React.createElement("line", {
29190
- x1: x,
29191
- y1: horizontalStartY,
29192
- x2: x,
29193
- y2: horizontalEndY,
29194
- stroke: color$1
29195
- }));
29196
- const svgForPoint = /*#__PURE__*/React.createElement("g", {
29197
- ref: hitboxRef,
29198
- className: pointClasses,
29199
- tabIndex: 0,
29200
- style: {
29201
- "--movable-point-color": color$1
29202
- },
29203
- "data-testid": "movable-point"
29204
- }, /*#__PURE__*/React.createElement("circle", {
29205
- className: "movable-point-hitbox",
29206
- r: hitboxSizePx / 2,
29207
- cx: x,
29208
- cy: y
29209
- }), /*#__PURE__*/React.createElement("circle", {
29210
- className: "movable-point-halo",
29211
- cx: x,
29212
- cy: y
29213
- }), /*#__PURE__*/React.createElement("circle", {
29214
- className: "movable-point-ring",
29215
- cx: x,
29216
- cy: y
29217
- }), /*#__PURE__*/React.createElement("circle", {
29218
- className: "movable-point-focus-outline",
29219
- cx: x,
29220
- cy: y
29221
- }), /*#__PURE__*/React.createElement("circle", {
29222
- className: "movable-point-center",
29223
- cx: x,
29224
- cy: y,
29225
- style: {
29226
- fill: color$1
29227
- },
29228
- "data-testid": "movable-point__center"
29229
- }));
29230
- return /*#__PURE__*/React.createElement(React.Fragment, null, showHairlines && hairlines, showTooltips ? /*#__PURE__*/React.createElement(Tooltip$1, {
29231
- autoUpdate: true,
29232
- backgroundColor: wbColorName,
29233
- content: `(${point[0]}, ${point[1]})`,
29234
- contentStyle: {
29235
- color: "white"
29236
- }
29237
- }, svgForPoint) : svgForPoint);
29238
- };
29239
-
29240
29332
  const SegmentGraph = props => {
29241
29333
  const {
29242
29334
  dispatch
@@ -29244,7 +29336,7 @@ const SegmentGraph = props => {
29244
29336
  const {
29245
29337
  coords: segments
29246
29338
  } = props.graphState;
29247
- return /*#__PURE__*/React.createElement(React.Fragment, null, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement(SegmentView, {
29339
+ return /*#__PURE__*/React.createElement(React.Fragment, null, segments == null ? void 0 : segments.map((segment, i) => /*#__PURE__*/React.createElement(Segment, {
29248
29340
  key: i,
29249
29341
  points: segment,
29250
29342
  onMoveLine: delta => {
@@ -29255,27 +29347,6 @@ const SegmentGraph = props => {
29255
29347
  }
29256
29348
  })));
29257
29349
  };
29258
- const SegmentView = props => {
29259
- const {
29260
- onMoveLine: onMoveSegment,
29261
- points: [start, end]
29262
- } = props;
29263
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MovableLine, {
29264
- start: start,
29265
- end: end,
29266
- onMove: onMoveSegment
29267
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29268
- point: start,
29269
- onMove: newPoint => {
29270
- props.onMovePoint(0, newPoint);
29271
- }
29272
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29273
- point: end,
29274
- onMove: newPoint => {
29275
- props.onMovePoint(1, newPoint);
29276
- }
29277
- }));
29278
- };
29279
29350
 
29280
29351
  const LinearGraph = props => {
29281
29352
  const {
@@ -29285,49 +29356,19 @@ const LinearGraph = props => {
29285
29356
  coords: lines
29286
29357
  } = props.graphState;
29287
29358
  const colors = ["var(--movable-line-stroke-color)", "var(--mafs-violet)"];
29288
- return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(LineView, {
29359
+ return /*#__PURE__*/React.createElement(React.Fragment, null, lines == null ? void 0 : lines.map((line, i) => /*#__PURE__*/React.createElement(Segment, {
29289
29360
  key: i,
29290
29361
  points: line,
29291
29362
  onMoveLine: delta => {
29292
29363
  dispatch(moveLine(i, delta));
29293
29364
  },
29294
- onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29295
- stroke: colors[i]
29296
- })));
29297
- };
29298
- const LineView = props => {
29299
- const {
29300
- onMoveLine,
29301
- onMovePoint,
29302
- points: [start, end],
29303
- stroke
29304
- } = props;
29305
- const {
29306
- range
29307
- } = useGraphConfig();
29308
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MovableLine, {
29309
- start: start,
29310
- end: end,
29311
- onMove: onMoveLine,
29312
29365
  extend: {
29313
29366
  start: true,
29314
- end: true,
29315
- range
29367
+ end: true
29316
29368
  },
29317
- stroke: stroke
29318
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29319
- point: start,
29320
- onMove: newPoint => {
29321
- onMovePoint(0, newPoint);
29322
- },
29323
- color: stroke
29324
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29325
- point: end,
29326
- onMove: newPoint => {
29327
- onMovePoint(1, newPoint);
29328
- },
29329
- color: stroke
29330
- }));
29369
+ onMovePoint: (endpointIndex, destination) => dispatch(moveControlPoint(endpointIndex, destination, i)),
29370
+ color: colors[i]
29371
+ })));
29331
29372
  };
29332
29373
 
29333
29374
  const RayGraph = props => {
@@ -29335,30 +29376,21 @@ const RayGraph = props => {
29335
29376
  dispatch
29336
29377
  } = props;
29337
29378
  const {
29338
- coords: lines,
29339
- range
29379
+ coords: lines
29340
29380
  } = props.graphState;
29341
-
29342
- // a ray only has one line
29343
- const [start, end] = lines[0];
29344
29381
  const handleMoveLine = delta => dispatch(moveLine(0, delta));
29345
- const handleMovePoint = (newPoint, pointIndex) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29346
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MovableLine, {
29347
- start: start,
29348
- end: end,
29349
- onMove: handleMoveLine,
29382
+ const handleMovePoint = (pointIndex, newPoint) => dispatch(moveControlPoint(pointIndex, newPoint, 0));
29383
+ return /*#__PURE__*/React.createElement(Segment
29384
+ // a ray only has one line
29385
+ , {
29386
+ points: lines[0],
29387
+ onMoveLine: handleMoveLine,
29388
+ onMovePoint: handleMovePoint,
29350
29389
  extend: {
29351
- end: true,
29352
29390
  start: false,
29353
- range
29391
+ end: true
29354
29392
  }
29355
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29356
- point: start,
29357
- onMove: newPoint => handleMovePoint(newPoint, 0)
29358
- }), /*#__PURE__*/React.createElement(StyledMovablePoint, {
29359
- point: end,
29360
- onMove: newPoint => handleMovePoint(newPoint, 1)
29361
- }));
29393
+ });
29362
29394
  };
29363
29395
 
29364
29396
  /**
@@ -29522,8 +29554,37 @@ const linesIntersect = ([[a, b], [c, d]], [[p, q], [r, s]]) => {
29522
29554
  };
29523
29555
  const isEven = n => n % 2 === 0;
29524
29556
 
29557
+ const StyledMovablePoint = props => {
29558
+ const {
29559
+ snapStep
29560
+ } = useGraphConfig();
29561
+ const elementRef = useRef(null);
29562
+ const {
29563
+ point,
29564
+ onMove,
29565
+ color: color$1 = color.blue
29566
+ } = props;
29567
+ const {
29568
+ dragging
29569
+ } = useMovable({
29570
+ gestureTarget: elementRef,
29571
+ point,
29572
+ onMove,
29573
+ constrain: p => snap(snapStep, p)
29574
+ });
29575
+ return /*#__PURE__*/React.createElement(MovablePointView, {
29576
+ ref: elementRef,
29577
+ point: point,
29578
+ color: color$1,
29579
+ dragging: dragging,
29580
+ focusBehavior: {
29581
+ type: "uncontrolled",
29582
+ tabIndex: 0
29583
+ }
29584
+ });
29585
+ };
29586
+
29525
29587
  const PolygonGraph = props => {
29526
- var _points$length;
29527
29588
  const [focused, setFocused] = React.useState(false);
29528
29589
  const [hovered, setHovered] = React.useState(false);
29529
29590
  const {
@@ -29536,17 +29597,19 @@ const PolygonGraph = props => {
29536
29597
  range,
29537
29598
  snapStep
29538
29599
  } = props.graphState;
29600
+
29601
+ // TODO(benchristel): can the default set of points be removed here? I don't
29602
+ // think coords can be null.
29539
29603
  const points = coords != null ? coords : [[0, 0]];
29540
- const pointsSum = points.reduce((acc, point) => vec.add(acc, point), [0, 0]);
29541
- const midpoint = pointsSum && vec.scale(pointsSum, 1 / ((_points$length = points.length) != null ? _points$length : 1));
29542
29604
  const ref = React.useRef(null);
29605
+ const dragReferencePoint = points[0];
29543
29606
  const {
29544
29607
  dragging
29545
29608
  } = useMovable({
29546
29609
  gestureTarget: ref,
29547
- point: midpoint,
29610
+ point: dragReferencePoint,
29548
29611
  onMove: newPoint => {
29549
- const delta = vec.sub(newPoint, midpoint);
29612
+ const delta = vec.sub(newPoint, dragReferencePoint);
29550
29613
  dispatch(moveAll(delta));
29551
29614
  },
29552
29615
  constrain: p => snap(snapStep, p)
@@ -43231,7 +43294,7 @@ const init = function init(options) {
43231
43294
 
43232
43295
  // This file is processed by a Rollup plugin (replace) to inject the production
43233
43296
  const libName = "@khanacademy/perseus";
43234
- const libVersion = "22.4.0";
43297
+ const libVersion = "22.4.1";
43235
43298
  addLibraryVersionToPerseusDebug(libName, libVersion);
43236
43299
 
43237
43300
  const apiVersion = {