@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/es/index.js CHANGED
@@ -2,13 +2,13 @@ import * as KAS from '@khanacademy/kas';
2
2
  import { MathInputI18nContext, CursorContext, getKeyTranslator, createMathField, getCursorContext, DesktopKeypad, convertDotToTimesByLocale, mathQuillInstance, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
3
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
4
  import { linterContextDefault, Rule } from '@khanacademy/perseus-linter';
5
- import { View, RenderStateRoot, UniqueIDProvider, useOnMountEffect, useLatestRef, useUniqueIdWithMock, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
5
+ import { View, RenderStateRoot, Id, useOnMountEffect, useLatestRef, Text as Text$2 } from '@khanacademy/wonder-blocks-core';
6
6
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
7
7
  import { HeadingMedium, LabelSmall, LabelLarge, Body, LabelMedium } from '@khanacademy/wonder-blocks-typography';
8
8
  import { StyleSheet, css } from 'aphrodite';
9
9
  import classNames$1 from 'classnames';
10
10
  import * as React from 'react';
11
- import React__default, { useContext, useState, useEffect, useRef, createContext, forwardRef, useLayoutEffect, useImperativeHandle } from 'react';
11
+ import React__default, { useContext, useState, useEffect, useRef, createContext, forwardRef, useLayoutEffect, useImperativeHandle, useId } from 'react';
12
12
  import * as ReactDOM from 'react-dom';
13
13
  import ReactDOM__default from 'react-dom';
14
14
  import _, { debounce as debounce$1 } from 'underscore';
@@ -244,11 +244,11 @@ const mockStrings = {
244
244
  tan: "tan",
245
245
  simulationLoadFail: "Sorry, this simulation cannot load.",
246
246
  simulationLocaleWarning: "Sorry, this simulation isn't available in your language.",
247
- addPoint: "Add Point",
248
- removePoint: "Remove Point",
249
- graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
250
247
  selectAnAnswer: "Select an answer",
251
248
  // The following strings are used for interactive graph SR descriptions.
249
+ graphKeyboardPrompt: "Press Shift + Enter to interact with the graph",
250
+ addPoint: "Add Point",
251
+ removePoint: "Remove Point",
252
252
  closePolygon: "Close shape",
253
253
  openPolygon: "Re-open shape",
254
254
  srPointAtCoordinates: ({
@@ -260,19 +260,19 @@ const mockStrings = {
260
260
  elements
261
261
  }) => `Interactive elements: ${elements}`,
262
262
  srNoInteractiveElements: "No interactive elements",
263
- circleGraphAriaLabel: "A circle on a coordinate plane.",
264
- circleShapeAriaLabel: ({
263
+ srCircleGraph: "A circle on a coordinate plane.",
264
+ srCircleShape: ({
265
265
  centerX,
266
266
  centerY
267
267
  }) => `Circle. The center point is at ${centerX} comma ${centerY}.`,
268
- circleRadiusPointAriaLabel: ({
268
+ srCircleRadiusPoint: ({
269
269
  radiusPointX,
270
270
  radiusPointY
271
271
  }) => `Radius point at ${radiusPointX} comma ${radiusPointY}.`,
272
- circleRadiusDescription: ({
272
+ srCircleRadius: ({
273
273
  radius
274
274
  }) => `Circle radius is ${radius}.`,
275
- circleOuterPointsDescription: ({
275
+ srCircleOuterPoints: ({
276
276
  point1X,
277
277
  point1Y,
278
278
  point2X,
@@ -8870,13 +8870,10 @@ const reactRootCache = new WeakMap();
8870
8870
  */
8871
8871
  function render(element, container) {
8872
8872
  /**
8873
- * `RenderStateRoot` is responsible for tracking whether it's
8874
- * the initial render or subsequent renders. It's used by
8875
- * `useUniqueId` and will be used by `UniqueIDProvider` and
8876
- * `WithSSRPlaceholder` in the future. We're placing it as
8877
- * high up in the render tree as possible to ensure that any
8878
- * components using that hook or those components will function
8879
- * correctly.
8873
+ * `RenderStateRoot` is responsible for tracking whether it's the initial
8874
+ * render or subsequent renders. It's used by `WithSSRPlaceholder`. We're
8875
+ * placing it as high up in the render tree as possible to ensure that any
8876
+ * components using that hook or those components will function correctly.
8880
8877
  */
8881
8878
  const children = /*#__PURE__*/React.createElement(RenderStateRoot, {
8882
8879
  throwIfNested: false
@@ -27385,41 +27382,33 @@ class Dropdown extends React.Component {
27385
27382
  value: String(i + 1),
27386
27383
  label: choice
27387
27384
  }))];
27388
- return (
27389
- /*#__PURE__*/
27390
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
27391
- // eslint-disable-next-line no-restricted-syntax
27392
- React.createElement(UniqueIDProvider, {
27393
- scope: "dropdown-widget",
27394
- mockOnFirstRender: true
27395
- }, ids => /*#__PURE__*/React.createElement(View
27396
- // NOTE(jared): These are required to prevent weird behavior
27397
- // When there's a dropdown in a zoomable table.
27398
- , {
27399
- onClick: e => {
27400
- e.stopPropagation();
27401
- },
27402
- onTouchStart: e => {
27403
- e.stopPropagation();
27404
- }
27405
- }, this.props.visibleLabel && /*#__PURE__*/React.createElement(LabelLarge, {
27406
- tag: "label",
27407
- htmlFor: ids.get("dropdown")
27408
- }, this.props.visibleLabel), /*#__PURE__*/React.createElement(SingleSelect, {
27409
- id: ids.get("dropdown"),
27410
- placeholder: "",
27411
- onChange: value => this._handleChange(parseInt(value)),
27412
- selectedValue: String(this.props.selected),
27413
- disabled: this.props.apiOptions.readOnly,
27414
- "aria-label": this.props.ariaLabel || this.props.visibleLabel || this.context.strings.selectAnAnswer
27415
- // This is currently necessary for SRs to read the labels properly.
27416
- // However, WB is working on a change to add the "combobox" role to
27417
- // all dropdowns.
27418
- // See https://khanacademy.atlassian.net/browse/WB-1671
27419
- ,
27420
- role: "combobox"
27421
- }, children)))
27422
- );
27385
+ return /*#__PURE__*/React.createElement(Id, null, dropdownId => /*#__PURE__*/React.createElement(View
27386
+ // NOTE(jared): These are required to prevent weird behavior
27387
+ // When there's a dropdown in a zoomable table.
27388
+ , {
27389
+ onClick: e => {
27390
+ e.stopPropagation();
27391
+ },
27392
+ onTouchStart: e => {
27393
+ e.stopPropagation();
27394
+ }
27395
+ }, this.props.visibleLabel && /*#__PURE__*/React.createElement(LabelLarge, {
27396
+ tag: "label",
27397
+ htmlFor: dropdownId
27398
+ }, this.props.visibleLabel), /*#__PURE__*/React.createElement(SingleSelect, {
27399
+ id: dropdownId,
27400
+ placeholder: "",
27401
+ onChange: value => this._handleChange(parseInt(value)),
27402
+ selectedValue: String(this.props.selected),
27403
+ disabled: this.props.apiOptions.readOnly,
27404
+ "aria-label": this.props.ariaLabel || this.props.visibleLabel || this.context.strings.selectAnAnswer
27405
+ // This is currently necessary for SRs to read the labels properly.
27406
+ // However, WB is working on a change to add the "combobox" role to
27407
+ // all dropdowns.
27408
+ // See https://khanacademy.atlassian.net/browse/WB-1671
27409
+ ,
27410
+ role: "combobox"
27411
+ }, children)));
27423
27412
  }
27424
27413
  }
27425
27414
  Dropdown.contextType = PerseusI18nContext;
@@ -27522,37 +27511,29 @@ class Explanation extends React.Component {
27522
27511
  "white-space": "normal"
27523
27512
  };
27524
27513
  const contentStyling = [styles$k.content, this.state.expanded ? styles$k.contentExpanded : styles$k.contentCollapsed, allowTransition && (this.state.expanded ? styles$k.transitionExpanded : styles$k.transitionCollapsed)];
27525
- return (
27526
- /*#__PURE__*/
27527
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
27528
- // eslint-disable-next-line no-restricted-syntax
27529
- React.createElement(UniqueIDProvider, {
27530
- mockOnFirstRender: true,
27531
- scope: "explanation-widget"
27532
- }, ids => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, {
27533
- "aria-expanded": this.state.expanded,
27534
- "aria-controls": ids.get("content"),
27535
- endIcon: caretIcon,
27536
- kind: "tertiary",
27537
- labelStyle: labelStyle,
27538
- onClick: this._onClick,
27539
- size: "small",
27540
- style: buttonStyleOverrides
27541
- }, promptText), /*#__PURE__*/React.createElement(View, {
27542
- id: ids.get("content"),
27543
- style: contentStyling,
27544
- "aria-hidden": !this.state.expanded,
27545
- testId: "content-container"
27546
- }, /*#__PURE__*/React.createElement(View, {
27547
- style: styles$k.contentWrapper
27548
- }, /*#__PURE__*/React.createElement(Renderer, {
27549
- apiOptions: this.props.apiOptions,
27550
- content: this.props.explanation,
27551
- widgets: this.props.widgets,
27552
- linterContext: this.props.linterContext,
27553
- strings: this.context.strings
27554
- })))))
27555
- );
27514
+ return /*#__PURE__*/React.createElement(Id, null, contentId => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, {
27515
+ "aria-expanded": this.state.expanded,
27516
+ "aria-controls": contentId,
27517
+ endIcon: caretIcon,
27518
+ kind: "tertiary",
27519
+ labelStyle: labelStyle,
27520
+ onClick: this._onClick,
27521
+ size: "small",
27522
+ style: buttonStyleOverrides
27523
+ }, promptText), /*#__PURE__*/React.createElement(View, {
27524
+ id: contentId,
27525
+ style: contentStyling,
27526
+ "aria-hidden": !this.state.expanded,
27527
+ testId: "content-container"
27528
+ }, /*#__PURE__*/React.createElement(View, {
27529
+ style: styles$k.contentWrapper
27530
+ }, /*#__PURE__*/React.createElement(Renderer, {
27531
+ apiOptions: this.props.apiOptions,
27532
+ content: this.props.explanation,
27533
+ widgets: this.props.widgets,
27534
+ linterContext: this.props.linterContext,
27535
+ strings: this.context.strings
27536
+ })))));
27556
27537
  }
27557
27538
  }
27558
27539
  Explanation.contextType = PerseusI18nContext;
@@ -32708,6 +32689,17 @@ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
32708
32689
  function isBetween(x, low, high) {
32709
32690
  return x >= low && x <= high;
32710
32691
  }
32692
+ function getArrayWithoutDuplicates(array) {
32693
+ const returnArray = [];
32694
+ for (const coordPair of array) {
32695
+ if (
32696
+ // Check if the coordPair is not already in the returnArray
32697
+ !returnArray.some(([x, y]) => x === coordPair[0] && y === coordPair[1])) {
32698
+ returnArray.push(coordPair);
32699
+ }
32700
+ }
32701
+ return returnArray;
32702
+ }
32711
32703
 
32712
32704
  const LockedLine = props => {
32713
32705
  var _flags$mafs;
@@ -34118,11 +34110,23 @@ function getGradableGraph(state, initialGraph) {
34118
34110
  });
34119
34111
  }
34120
34112
  if (state.type === "polygon" && initialGraph.type === "polygon") {
34113
+ // Unless the polygon is closed it is not considered score-able.
34114
+ if (state.numSides === "unlimited" && !state.closedPolygon) {
34115
+ return _extends({}, initialGraph, {
34116
+ coords: null
34117
+ });
34118
+ }
34121
34119
  return _extends({}, initialGraph, {
34122
34120
  coords: state.coords
34123
34121
  });
34124
34122
  }
34125
34123
  if (state.type === "point" && initialGraph.type === "point") {
34124
+ // The unlimited point graph must have at least 1 coordinate or else it is not considered score-able.
34125
+ if (state.numPoints === "unlimited" && state.coords.length === 0) {
34126
+ return _extends({}, initialGraph, {
34127
+ coords: null
34128
+ });
34129
+ }
34126
34130
  return _extends({}, initialGraph, {
34127
34131
  coords: state.coords
34128
34132
  });
@@ -34196,7 +34200,8 @@ function CircleGraph(props) {
34196
34200
  radiusPoint
34197
34201
  } = graphState;
34198
34202
  const {
34199
- strings
34203
+ strings,
34204
+ locale
34200
34205
  } = usePerseusI18n();
34201
34206
  const [radiusPointAriaLive, setRadiusPointAriaLive] = React.useState("off");
34202
34207
  const radius = getRadius(graphState);
@@ -34206,27 +34211,27 @@ function CircleGraph(props) {
34206
34211
  const outerPointsId = id + "-outer-points";
34207
34212
 
34208
34213
  // Aria label strings
34209
- const circleGraphAriaLabel = strings.circleGraphAriaLabel;
34210
- const circleShapeAriaLabel = strings.circleShapeAriaLabel({
34211
- centerX: center[0],
34212
- centerY: center[1]
34214
+ const circleGraphAriaLabel = strings.srCircleGraph;
34215
+ const circleShapeAriaLabel = strings.srCircleShape({
34216
+ centerX: srFormatNumber(center[0], locale),
34217
+ centerY: srFormatNumber(center[1], locale)
34213
34218
  });
34214
- const circleRadiusPointAriaLabel = strings.circleRadiusPointAriaLabel({
34215
- radiusPointX: radiusPoint[0],
34216
- radiusPointY: radiusPoint[1]
34219
+ const circleRadiusPointAriaLabel = strings.srCircleRadiusPoint({
34220
+ radiusPointX: srFormatNumber(radiusPoint[0], locale),
34221
+ radiusPointY: srFormatNumber(radiusPoint[1], locale)
34217
34222
  });
34218
- const circleRadiusDescription = strings.circleRadiusDescription({
34223
+ const circleRadiusDescription = strings.srCircleRadius({
34219
34224
  radius
34220
34225
  });
34221
- const circleOuterPointsDescription = strings.circleOuterPointsDescription({
34222
- point1X: center[0] + radius,
34223
- point1Y: center[1],
34224
- point2X: center[0],
34225
- point2Y: center[1] + radius,
34226
- point3X: center[0] - radius,
34227
- point3Y: center[1],
34228
- point4X: center[0],
34229
- point4Y: center[1] - radius
34226
+ const circleOuterPointsDescription = strings.srCircleOuterPoints({
34227
+ point1X: srFormatNumber(center[0] + radius, locale),
34228
+ point1Y: srFormatNumber(center[1], locale),
34229
+ point2X: srFormatNumber(center[0], locale),
34230
+ point2Y: srFormatNumber(center[1] + radius, locale),
34231
+ point3X: srFormatNumber(center[0] - radius, locale),
34232
+ point3Y: srFormatNumber(center[1], locale),
34233
+ point4X: srFormatNumber(center[0], locale),
34234
+ point4Y: srFormatNumber(center[1] - radius, locale)
34230
34235
  });
34231
34236
  return /*#__PURE__*/React.createElement("g", {
34232
34237
  // Outer circle minimal description
@@ -34629,6 +34634,15 @@ const PolygonGraph = props => {
34629
34634
  (_pointsRef$current$fo = pointsRef.current[focusedIndex]) == null || _pointsRef$current$fo.focus();
34630
34635
  }
34631
34636
  }, [props.graphState.focusedPointIndex, pointsRef]);
34637
+
34638
+ // If the unlimited polygon is rendered with 3 or more coordinates
34639
+ // Close the polygon, but only on first render.
34640
+ React.useEffect(() => {
34641
+ if (numSides === "unlimited" && props.graphState.coords.length > 2) {
34642
+ dispatch(actions.polygon.closePolygon());
34643
+ }
34644
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34645
+ }, []);
34632
34646
  const statefulProps = _extends({}, props, {
34633
34647
  graphConfig,
34634
34648
  polygonRef,
@@ -34769,7 +34783,16 @@ const UnlimitedPolygonGraph = statefulProps => {
34769
34783
  } = graphConfig;
34770
34784
  const widthPx = graphDimensionsInPixels[0];
34771
34785
  const heightPx = graphDimensionsInPixels[1];
34772
- return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("rect", {
34786
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Polyline, {
34787
+ points: [...points],
34788
+ color: "var(--movable-line-stroke-color)",
34789
+ svgPolylineProps: {
34790
+ strokeWidth: "var(--movable-line-stroke-weight)",
34791
+ style: {
34792
+ fill: "transparent"
34793
+ }
34794
+ }
34795
+ }), /*#__PURE__*/React.createElement("rect", {
34773
34796
  style: {
34774
34797
  fill: "rgba(0,0,0,0)",
34775
34798
  cursor: "crosshair"
@@ -34785,15 +34808,6 @@ const UnlimitedPolygonGraph = statefulProps => {
34785
34808
  const graphCoordinates = pixelsToVectors([[x, y]], graphConfig);
34786
34809
  dispatch(actions.polygon.addPoint(graphCoordinates[0]));
34787
34810
  }
34788
- }), /*#__PURE__*/React.createElement(Polyline, {
34789
- points: [...points],
34790
- color: "var(--movable-line-stroke-color)",
34791
- svgPolylineProps: {
34792
- strokeWidth: "var(--movable-line-stroke-weight)",
34793
- style: {
34794
- fill: "transparent"
34795
- }
34796
- }
34797
34811
  }), coords.map((point, i) => /*#__PURE__*/React.createElement(MovablePoint$1, {
34798
34812
  key: i,
34799
34813
  point: point,
@@ -34807,9 +34821,10 @@ const UnlimitedPolygonGraph = statefulProps => {
34807
34821
  },
34808
34822
  onClick: () => {
34809
34823
  // If the point being clicked is the first point and
34810
- // there's enough points to form a polygon (3 or more)
34811
- // Close the shape before setting focus.
34812
- if (i === 0 && coords.length >= 3) {
34824
+ // there's enough non-duplicated points to form
34825
+ // a polygon (3 or more), close the shape before
34826
+ // setting focus.
34827
+ if (i === 0 && getArrayWithoutDuplicates(coords).length >= 3) {
34813
34828
  dispatch(actions.polygon.closePolygon());
34814
34829
  }
34815
34830
  dispatch(actions.polygon.clickPoint(i));
@@ -35432,7 +35447,7 @@ const renderPolygonGraphControls = props => {
35432
35447
 
35433
35448
  // We want to disable the closePolygon button when
35434
35449
  // there are not enough coords to make a polygon
35435
- const disableCloseButton = coords.length < 3;
35450
+ const disableCloseButton = getArrayWithoutDuplicates(coords).length < 3;
35436
35451
 
35437
35452
  // If polygon is closed, show open button.
35438
35453
  // If polygon is open, show close button.
@@ -36132,7 +36147,13 @@ function doClickPoint(state, action) {
36132
36147
  }
36133
36148
  function doClosePolygon(state) {
36134
36149
  if (isUnlimitedGraphState(state) && state.type === "polygon") {
36150
+ // We want to remove any duplicate points when closing the polygon to
36151
+ // (1) prevent the polygon from sides with length zero, and
36152
+ // (2) make sure the question is can be marked correct if the polygon
36153
+ // LOOKS correct, even if two of the points are at the same coords.
36154
+ const noDupedPoints = getArrayWithoutDuplicates(state.coords);
36135
36155
  return _extends({}, state, {
36156
+ coords: noDupedPoints,
36136
36157
  closedPolygon: true
36137
36158
  });
36138
36159
  }
@@ -38914,11 +38935,8 @@ const AnswerChoices = props => {
38914
38935
  };
38915
38936
 
38916
38937
  const HideAnswersToggle = props => {
38917
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
38918
- // eslint-disable-next-line no-restricted-syntax
38919
- const idFactory = useUniqueIdWithMock();
38920
- const switchId = idFactory.get("hide-answers-toggle");
38921
- const labelId = idFactory.get("hide-answers-label");
38938
+ const switchId = useId();
38939
+ const labelId = useId();
38922
38940
  const {
38923
38941
  strings
38924
38942
  } = usePerseusI18n();
@@ -38961,10 +38979,7 @@ const AnswerPill = props => {
38961
38979
  focused,
38962
38980
  hovered
38963
38981
  } = props;
38964
-
38965
- // TODO(WB-1812, somewhatabstract): Migrate to Id or useId
38966
- // eslint-disable-next-line no-restricted-syntax
38967
- const idFactory = useUniqueIdWithMock();
38982
+ const pillId = useId();
38968
38983
  const {
38969
38984
  strings
38970
38985
  } = usePerseusI18n();
@@ -38991,7 +39006,7 @@ const AnswerPill = props => {
38991
39006
  }) => /*#__PURE__*/React.createElement(Pill, {
38992
39007
  size: "large",
38993
39008
  kind: "accent",
38994
- id: idFactory.get("perseus-label-image-widget-answer-pill"),
39009
+ id: pillId,
38995
39010
  onClick: correct ? undefined : onClick,
38996
39011
  ref: ref,
38997
39012
  style: [style, popperStyle, styles$b.pill, correct && styles$b.correct, incorrect && styles$b.incorrect, (focused || hovered) && BringToFront]
@@ -45307,7 +45322,7 @@ const init = function init() {
45307
45322
 
45308
45323
  // This file is processed by a Rollup plugin (replace) to inject the production
45309
45324
  const libName = "@khanacademy/perseus";
45310
- const libVersion = "49.0.0";
45325
+ const libVersion = "49.0.2";
45311
45326
  addLibraryVersionToPerseusDebug(libName, libVersion);
45312
45327
 
45313
45328
  const apiVersion = {