@khanacademy/perseus 49.0.0 → 49.0.2

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.js CHANGED
@@ -365,11 +365,11 @@ const mockStrings = {
365
365
  tan: "tan",
366
366
  simulationLoadFail: "Sorry, this simulation cannot load.",
367
367
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
368
- addPoint: "Add Point",
369
- removePoint: "Remove Point",
370
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
371
368
  selectAnAnswer: "Select an answer",
372
369
  // The following strings are used for interactive graph SR descriptions.
370
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
371
+ addPoint: "Add Point",
372
+ removePoint: "Remove Point",
373
373
  closePolygon: "Close shape",
374
374
  openPolygon: "Re-open shape",
375
375
  srPointAtCoordinates: _ref27 => {
@@ -387,28 +387,28 @@ const mockStrings = {
387
387
  return `Interactive elements: ${elements}`;
388
388
  },
389
389
  srNoInteractiveElements: "No interactive elements",
390
- circleGraphAriaLabel: "A circle on a coordinate plane.",
391
- circleShapeAriaLabel: _ref29 => {
390
+ srCircleGraph: "A circle on a coordinate plane.",
391
+ srCircleShape: _ref29 => {
392
392
  let {
393
393
  centerX,
394
394
  centerY
395
395
  } = _ref29;
396
396
  return `Circle. The center point is at ${centerX} comma ${centerY}.`;
397
397
  },
398
- circleRadiusPointAriaLabel: _ref30 => {
398
+ srCircleRadiusPoint: _ref30 => {
399
399
  let {
400
400
  radiusPointX,
401
401
  radiusPointY
402
402
  } = _ref30;
403
403
  return `Radius point at ${radiusPointX} comma ${radiusPointY}.`;
404
404
  },
405
- circleRadiusDescription: _ref31 => {
405
+ srCircleRadius: _ref31 => {
406
406
  let {
407
407
  radius
408
408
  } = _ref31;
409
409
  return `Circle radius is ${radius}.`;
410
410
  },
411
- circleOuterPointsDescription: _ref32 => {
411
+ srCircleOuterPoints: _ref32 => {
412
412
  let {
413
413
  point1X,
414
414
  point1Y,
@@ -8993,13 +8993,10 @@ const reactRootCache = new WeakMap();
8993
8993
  */
8994
8994
  function render(element, container) {
8995
8995
  /**
8996
- * `RenderStateRoot` is responsible for tracking whether it's
8997
- * the initial render or subsequent renders. It's used by
8998
- * `useUniqueId` and will be used by `UniqueIDProvider` and
8999
- * `WithSSRPlaceholder` in the future. We're placing it as
9000
- * high up in the render tree as possible to ensure that any
9001
- * components using that hook or those components will function
9002
- * correctly.
8996
+ * `RenderStateRoot` is responsible for tracking whether it's the initial
8997
+ * render or subsequent renders. It's used by `WithSSRPlaceholder`. We're
8998
+ * placing it as high up in the render tree as possible to ensure that any
8999
+ * components using that hook or those components will function correctly.
9003
9000
  */
9004
9001
  const children = /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.RenderStateRoot, {
9005
9002
  throwIfNested: false
@@ -27638,41 +27635,33 @@ class Dropdown extends React__namespace.Component {
27638
27635
  value: String(i + 1),
27639
27636
  label: choice
27640
27637
  }))];
27641
- return (
27642
- /*#__PURE__*/
27643
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
27644
- // eslint-disable-next-line no-restricted-syntax
27645
- React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
27646
- scope: "dropdown-widget",
27647
- mockOnFirstRender: true
27648
- }, ids => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
27649
- // NOTE(jared): These are required to prevent weird behavior
27650
- // When there's a dropdown in a zoomable table.
27651
- , {
27652
- onClick: e => {
27653
- e.stopPropagation();
27654
- },
27655
- onTouchStart: e => {
27656
- e.stopPropagation();
27657
- }
27658
- }, this.props.visibleLabel && /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelLarge, {
27659
- tag: "label",
27660
- htmlFor: ids.get("dropdown")
27661
- }, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27662
- id: ids.get("dropdown"),
27663
- placeholder: "",
27664
- onChange: value => this._handleChange(parseInt(value)),
27665
- selectedValue: String(this.props.selected),
27666
- disabled: this.props.apiOptions.readOnly,
27667
- "aria-label": this.props.ariaLabel || this.props.visibleLabel || this.context.strings.selectAnAnswer
27668
- // This is currently necessary for SRs to read the labels properly.
27669
- // However, WB is working on a change to add the "combobox" role to
27670
- // all dropdowns.
27671
- // See https://khanacademy.atlassian.net/browse/WB-1671
27672
- ,
27673
- role: "combobox"
27674
- }, children)))
27675
- );
27638
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Id, null, dropdownId => /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View
27639
+ // NOTE(jared): These are required to prevent weird behavior
27640
+ // When there's a dropdown in a zoomable table.
27641
+ , {
27642
+ onClick: e => {
27643
+ e.stopPropagation();
27644
+ },
27645
+ onTouchStart: e => {
27646
+ e.stopPropagation();
27647
+ }
27648
+ }, this.props.visibleLabel && /*#__PURE__*/React__namespace.createElement(wonderBlocksTypography.LabelLarge, {
27649
+ tag: "label",
27650
+ htmlFor: dropdownId
27651
+ }, this.props.visibleLabel), /*#__PURE__*/React__namespace.createElement(wonderBlocksDropdown.SingleSelect, {
27652
+ id: dropdownId,
27653
+ placeholder: "",
27654
+ onChange: value => this._handleChange(parseInt(value)),
27655
+ selectedValue: String(this.props.selected),
27656
+ disabled: this.props.apiOptions.readOnly,
27657
+ "aria-label": this.props.ariaLabel || this.props.visibleLabel || this.context.strings.selectAnAnswer
27658
+ // This is currently necessary for SRs to read the labels properly.
27659
+ // However, WB is working on a change to add the "combobox" role to
27660
+ // all dropdowns.
27661
+ // See https://khanacademy.atlassian.net/browse/WB-1671
27662
+ ,
27663
+ role: "combobox"
27664
+ }, children)));
27676
27665
  }
27677
27666
  }
27678
27667
  const optionsTransform = widgetOptions => {
@@ -27780,37 +27769,29 @@ class Explanation extends React__namespace.Component {
27780
27769
  "white-space": "normal"
27781
27770
  };
27782
27771
  const contentStyling = [styles$k.content, this.state.expanded ? styles$k.contentExpanded : styles$k.contentCollapsed, allowTransition && (this.state.expanded ? styles$k.transitionExpanded : styles$k.transitionCollapsed)];
27783
- return (
27784
- /*#__PURE__*/
27785
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
27786
- // eslint-disable-next-line no-restricted-syntax
27787
- React__namespace.createElement(wonderBlocksCore.UniqueIDProvider, {
27788
- mockOnFirstRender: true,
27789
- scope: "explanation-widget"
27790
- }, ids => /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
27791
- "aria-expanded": this.state.expanded,
27792
- "aria-controls": ids.get("content"),
27793
- endIcon: caretIcon,
27794
- kind: "tertiary",
27795
- labelStyle: labelStyle,
27796
- onClick: this._onClick,
27797
- size: "small",
27798
- style: buttonStyleOverrides
27799
- }, promptText), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
27800
- id: ids.get("content"),
27801
- style: contentStyling,
27802
- "aria-hidden": !this.state.expanded,
27803
- testId: "content-container"
27804
- }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
27805
- style: styles$k.contentWrapper
27806
- }, /*#__PURE__*/React__namespace.createElement(Renderer, {
27807
- apiOptions: this.props.apiOptions,
27808
- content: this.props.explanation,
27809
- widgets: this.props.widgets,
27810
- linterContext: this.props.linterContext,
27811
- strings: this.context.strings
27812
- })))))
27813
- );
27772
+ return /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.Id, null, contentId => /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(Button__default["default"], {
27773
+ "aria-expanded": this.state.expanded,
27774
+ "aria-controls": contentId,
27775
+ endIcon: caretIcon,
27776
+ kind: "tertiary",
27777
+ labelStyle: labelStyle,
27778
+ onClick: this._onClick,
27779
+ size: "small",
27780
+ style: buttonStyleOverrides
27781
+ }, promptText), /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
27782
+ id: contentId,
27783
+ style: contentStyling,
27784
+ "aria-hidden": !this.state.expanded,
27785
+ testId: "content-container"
27786
+ }, /*#__PURE__*/React__namespace.createElement(wonderBlocksCore.View, {
27787
+ style: styles$k.contentWrapper
27788
+ }, /*#__PURE__*/React__namespace.createElement(Renderer, {
27789
+ apiOptions: this.props.apiOptions,
27790
+ content: this.props.explanation,
27791
+ widgets: this.props.widgets,
27792
+ linterContext: this.props.linterContext,
27793
+ strings: this.context.strings
27794
+ })))));
27814
27795
  }
27815
27796
  }
27816
27797
  const leftBorderSpacing = 23;
@@ -32998,6 +32979,20 @@ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
32998
32979
  function isBetween(x, low, high) {
32999
32980
  return x >= low && x <= high;
33000
32981
  }
32982
+ function getArrayWithoutDuplicates(array) {
32983
+ const returnArray = [];
32984
+ for (const coordPair of array) {
32985
+ if (
32986
+ // Check if the coordPair is not already in the returnArray
32987
+ !returnArray.some(_ref => {
32988
+ let [x, y] = _ref;
32989
+ return x === coordPair[0] && y === coordPair[1];
32990
+ })) {
32991
+ returnArray.push(coordPair);
32992
+ }
32993
+ }
32994
+ return returnArray;
32995
+ }
33001
32996
 
33002
32997
  const LockedLine = props => {
33003
32998
  const {
@@ -34424,12 +34419,26 @@ function getGradableGraph(state, initialGraph) {
34424
34419
  };
34425
34420
  }
34426
34421
  if (state.type === "polygon" && initialGraph.type === "polygon") {
34422
+ // Unless the polygon is closed it is not considered score-able.
34423
+ if (state.numSides === "unlimited" && !state.closedPolygon) {
34424
+ return {
34425
+ ...initialGraph,
34426
+ coords: null
34427
+ };
34428
+ }
34427
34429
  return {
34428
34430
  ...initialGraph,
34429
34431
  coords: state.coords
34430
34432
  };
34431
34433
  }
34432
34434
  if (state.type === "point" && initialGraph.type === "point") {
34435
+ // The unlimited point graph must have at least 1 coordinate or else it is not considered score-able.
34436
+ if (state.numPoints === "unlimited" && state.coords.length === 0) {
34437
+ return {
34438
+ ...initialGraph,
34439
+ coords: null
34440
+ };
34441
+ }
34433
34442
  return {
34434
34443
  ...initialGraph,
34435
34444
  coords: state.coords
@@ -34508,7 +34517,8 @@ function CircleGraph(props) {
34508
34517
  radiusPoint
34509
34518
  } = graphState;
34510
34519
  const {
34511
- strings
34520
+ strings,
34521
+ locale
34512
34522
  } = usePerseusI18n();
34513
34523
  const [radiusPointAriaLive, setRadiusPointAriaLive] = React__namespace.useState("off");
34514
34524
  const radius = getRadius(graphState);
@@ -34518,27 +34528,27 @@ function CircleGraph(props) {
34518
34528
  const outerPointsId = id + "-outer-points";
34519
34529
 
34520
34530
  // Aria label strings
34521
- const circleGraphAriaLabel = strings.circleGraphAriaLabel;
34522
- const circleShapeAriaLabel = strings.circleShapeAriaLabel({
34523
- centerX: center[0],
34524
- centerY: center[1]
34531
+ const circleGraphAriaLabel = strings.srCircleGraph;
34532
+ const circleShapeAriaLabel = strings.srCircleShape({
34533
+ centerX: srFormatNumber(center[0], locale),
34534
+ centerY: srFormatNumber(center[1], locale)
34525
34535
  });
34526
- const circleRadiusPointAriaLabel = strings.circleRadiusPointAriaLabel({
34527
- radiusPointX: radiusPoint[0],
34528
- radiusPointY: radiusPoint[1]
34536
+ const circleRadiusPointAriaLabel = strings.srCircleRadiusPoint({
34537
+ radiusPointX: srFormatNumber(radiusPoint[0], locale),
34538
+ radiusPointY: srFormatNumber(radiusPoint[1], locale)
34529
34539
  });
34530
- const circleRadiusDescription = strings.circleRadiusDescription({
34540
+ const circleRadiusDescription = strings.srCircleRadius({
34531
34541
  radius
34532
34542
  });
34533
- const circleOuterPointsDescription = strings.circleOuterPointsDescription({
34534
- point1X: center[0] + radius,
34535
- point1Y: center[1],
34536
- point2X: center[0],
34537
- point2Y: center[1] + radius,
34538
- point3X: center[0] - radius,
34539
- point3Y: center[1],
34540
- point4X: center[0],
34541
- point4Y: center[1] - radius
34543
+ const circleOuterPointsDescription = strings.srCircleOuterPoints({
34544
+ point1X: srFormatNumber(center[0] + radius, locale),
34545
+ point1Y: srFormatNumber(center[1], locale),
34546
+ point2X: srFormatNumber(center[0], locale),
34547
+ point2Y: srFormatNumber(center[1] + radius, locale),
34548
+ point3X: srFormatNumber(center[0] - radius, locale),
34549
+ point3Y: srFormatNumber(center[1], locale),
34550
+ point4X: srFormatNumber(center[0], locale),
34551
+ point4Y: srFormatNumber(center[1] - radius, locale)
34542
34552
  });
34543
34553
  return /*#__PURE__*/React__namespace.createElement("g", {
34544
34554
  // Outer circle minimal description
@@ -34944,6 +34954,15 @@ const PolygonGraph = props => {
34944
34954
  pointsRef.current[focusedIndex]?.focus();
34945
34955
  }
34946
34956
  }, [props.graphState.focusedPointIndex, pointsRef]);
34957
+
34958
+ // If the unlimited polygon is rendered with 3 or more coordinates
34959
+ // Close the polygon, but only on first render.
34960
+ React__namespace.useEffect(() => {
34961
+ if (numSides === "unlimited" && props.graphState.coords.length > 2) {
34962
+ dispatch(actions.polygon.closePolygon());
34963
+ }
34964
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34965
+ }, []);
34947
34966
  const statefulProps = {
34948
34967
  ...props,
34949
34968
  graphConfig,
@@ -35087,7 +35106,16 @@ const UnlimitedPolygonGraph = statefulProps => {
35087
35106
  } = graphConfig;
35088
35107
  const widthPx = graphDimensionsInPixels[0];
35089
35108
  const heightPx = graphDimensionsInPixels[1];
35090
- return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement("rect", {
35109
+ return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
35110
+ points: [...points],
35111
+ color: "var(--movable-line-stroke-color)",
35112
+ svgPolylineProps: {
35113
+ strokeWidth: "var(--movable-line-stroke-weight)",
35114
+ style: {
35115
+ fill: "transparent"
35116
+ }
35117
+ }
35118
+ }), /*#__PURE__*/React__namespace.createElement("rect", {
35091
35119
  style: {
35092
35120
  fill: "rgba(0,0,0,0)",
35093
35121
  cursor: "crosshair"
@@ -35103,15 +35131,6 @@ const UnlimitedPolygonGraph = statefulProps => {
35103
35131
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
35104
35132
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
35105
35133
  }
35106
- }), /*#__PURE__*/React__namespace.createElement(mafs.Polyline, {
35107
- points: [...points],
35108
- color: "var(--movable-line-stroke-color)",
35109
- svgPolylineProps: {
35110
- strokeWidth: "var(--movable-line-stroke-weight)",
35111
- style: {
35112
- fill: "transparent"
35113
- }
35114
- }
35115
35134
  }), coords.map((point, i) => /*#__PURE__*/React__namespace.createElement(MovablePoint$1, {
35116
35135
  key: i,
35117
35136
  point: point,
@@ -35125,9 +35144,10 @@ const UnlimitedPolygonGraph = statefulProps => {
35125
35144
  },
35126
35145
  onClick: () => {
35127
35146
  // If the point being clicked is the first point and
35128
- // there's enough points to form a polygon (3 or more)
35129
- // Close the shape before setting focus.
35130
- if (i === 0 && coords.length >= 3) {
35147
+ // there's enough non-duplicated points to form
35148
+ // a polygon (3 or more), close the shape before
35149
+ // setting focus.
35150
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
35131
35151
  dispatch(actions.polygon.closePolygon());
35132
35152
  }
35133
35153
  dispatch(actions.polygon.clickPoint(i));
@@ -35746,7 +35766,7 @@ const renderPolygonGraphControls = props => {
35746
35766
 
35747
35767
  // We want to disable the closePolygon button when
35748
35768
  // there are not enough coords to make a polygon
35749
- const disableCloseButton = coords.length < 3;
35769
+ const disableCloseButton = getArrayWithoutDuplicates(coords).length < 3;
35750
35770
 
35751
35771
  // If polygon is closed, show open button.
35752
35772
  // If polygon is open, show close button.
@@ -36471,8 +36491,14 @@ function doClickPoint(state, action) {
36471
36491
  }
36472
36492
  function doClosePolygon(state) {
36473
36493
  if (isUnlimitedGraphState(state) && state.type === "polygon") {
36494
+ // We want to remove any duplicate points when closing the polygon to
36495
+ // (1) prevent the polygon from sides with length zero, and
36496
+ // (2) make sure the question is can be marked correct if the polygon
36497
+ // LOOKS correct, even if two of the points are at the same coords.
36498
+ const noDupedPoints = getArrayWithoutDuplicates(state.coords);
36474
36499
  return {
36475
36500
  ...state,
36501
+ coords: noDupedPoints,
36476
36502
  closedPolygon: true
36477
36503
  };
36478
36504
  }
@@ -39261,11 +39287,8 @@ const AnswerChoices = props => {
39261
39287
  };
39262
39288
 
39263
39289
  const HideAnswersToggle = props => {
39264
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
39265
- // eslint-disable-next-line no-restricted-syntax
39266
- const idFactory = wonderBlocksCore.useUniqueIdWithMock();
39267
- const switchId = idFactory.get("hide-answers-toggle");
39268
- const labelId = idFactory.get("hide-answers-label");
39290
+ const switchId = React.useId();
39291
+ const labelId = React.useId();
39269
39292
  const {
39270
39293
  strings
39271
39294
  } = usePerseusI18n();
@@ -39308,10 +39331,7 @@ const AnswerPill = props => {
39308
39331
  focused,
39309
39332
  hovered
39310
39333
  } = props;
39311
-
39312
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
39313
- // eslint-disable-next-line no-restricted-syntax
39314
- const idFactory = wonderBlocksCore.useUniqueIdWithMock();
39334
+ const pillId = React.useId();
39315
39335
  const {
39316
39336
  strings
39317
39337
  } = usePerseusI18n();
@@ -39340,7 +39360,7 @@ const AnswerPill = props => {
39340
39360
  return /*#__PURE__*/React__namespace.createElement(Pill__default["default"], {
39341
39361
  size: "large",
39342
39362
  kind: "accent",
39343
- id: idFactory.get("perseus-label-image-widget-answer-pill"),
39363
+ id: pillId,
39344
39364
  onClick: correct ? undefined : onClick,
39345
39365
  ref: ref,
39346
39366
  style: [style, popperStyle, styles$b.pill, correct && styles$b.correct, incorrect && styles$b.incorrect, (focused || hovered) && BringToFront]
@@ -45658,7 +45678,7 @@ const init = function () {
45658
45678
 
45659
45679
  // This file is processed by a Rollup plugin (replace) to inject the production
45660
45680
  const libName = "@khanacademy/perseus";
45661
- const libVersion = "49.0.0";
45681
+ const libVersion = "49.0.2";
45662
45682
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
45663
45683
 
45664
45684
  const apiVersion = {