@khanacademy/perseus 17.6.0 → 17.6.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/es/index.js +402 -606
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.js +411 -603
  5. package/dist/index.js.map +1 -1
  6. package/dist/perseus-types.d.ts +2 -1
  7. package/dist/strings.js +1 -0
  8. package/dist/widgets/deprecated-standin/index.d.ts +32 -0
  9. package/dist/widgets/label-image/answer-choices.d.ts +8 -40
  10. package/dist/widgets/label-image/answer-pill.d.ts +3 -1
  11. package/dist/widgets/label-image/marker.d.ts +10 -5
  12. package/dist/widgets/label-image.d.ts +1 -8
  13. package/dist/widgets.d.ts +9 -0
  14. package/package.json +34 -33
  15. package/src/__tests__/widgets.test.ts +20 -0
  16. package/src/extra-widgets.ts +2 -0
  17. package/src/init.ts +5 -0
  18. package/src/perseus-types.ts +4 -1
  19. package/src/util/register-all-widgets-for-testing.ts +4 -0
  20. package/src/widgets/__stories__/orderer.stories.tsx +5 -1
  21. package/src/widgets/__testdata__/orderer.testdata.ts +66 -0
  22. package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +34 -0
  23. package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +71 -0
  24. package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +93 -0
  25. package/src/widgets/deprecated-standin/index.tsx +57 -0
  26. package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +29 -34
  27. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +2 -10
  28. package/src/widgets/label-image/answer-choices.tsx +60 -278
  29. package/src/widgets/label-image/answer-pill.tsx +22 -13
  30. package/src/widgets/label-image/marker.tsx +103 -146
  31. package/src/widgets/label-image.test.ts +0 -2
  32. package/src/widgets/label-image.tsx +94 -195
  33. package/src/widgets.ts +24 -0
  34. package/tsconfig-build.tsbuildinfo +1 -1
package/dist/es/index.js CHANGED
@@ -7,7 +7,7 @@ import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
7
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
8
  import classNames from 'classnames';
9
9
  import * as React from 'react';
10
- import { useState, useEffect, useRef } from 'react';
10
+ import React__default, { useState, useEffect, useRef } from 'react';
11
11
  import _, { debounce } from 'underscore';
12
12
  import Clickable from '@khanacademy/wonder-blocks-clickable';
13
13
  import Color, { fade } from '@khanacademy/wonder-blocks-color';
@@ -25,12 +25,13 @@ import { pureMarkdownRules } from '@khanacademy/pure-markdown';
25
25
  import { TextField } from '@khanacademy/wonder-blocks-form';
26
26
  import Button from '@khanacademy/wonder-blocks-button';
27
27
  import { Strut } from '@khanacademy/wonder-blocks-layout';
28
- import { OptionItem, SingleSelect } from '@khanacademy/wonder-blocks-dropdown';
28
+ import Banner from '@khanacademy/wonder-blocks-banner';
29
+ import { OptionItem, SingleSelect, MultiSelect } from '@khanacademy/wonder-blocks-dropdown';
29
30
  import debounce$1 from 'lodash.debounce';
30
- import Pill from '@khanacademy/wonder-blocks-pill';
31
- import { Popper } from 'react-popper';
32
31
  import Switch from '@khanacademy/wonder-blocks-switch';
33
32
  import { LabelMedium } from '@khanacademy/wonder-blocks-typography';
33
+ import Pill from '@khanacademy/wonder-blocks-pill';
34
+ import { Popper } from 'react-popper';
34
35
  import createReactClass from 'create-react-class';
35
36
  import Link from '@khanacademy/wonder-blocks-link';
36
37
  import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
@@ -242,7 +243,7 @@ class MathInput extends React.Component {
242
243
  className = className + " " + this.props.className;
243
244
  }
244
245
  return /*#__PURE__*/React.createElement(View, {
245
- style: [styles$C.outerWrapper, this.state.focused && styles$C.wrapperFocused, this.props.hasError && styles$C.wrapperError]
246
+ style: [styles$B.outerWrapper, this.state.focused && styles$B.wrapperFocused, this.props.hasError && styles$B.wrapperError]
246
247
  }, /*#__PURE__*/React.createElement("div", {
247
248
  style: {
248
249
  display: "flex",
@@ -271,7 +272,7 @@ class MathInput extends React.Component {
271
272
  var _this$props$keypadBut, _this$props;
272
273
  return /*#__PURE__*/React.createElement(PopoverContentCore, {
273
274
  closeButtonVisible: true,
274
- style: styles$C.popoverContent
275
+ style: styles$B.popoverContent
275
276
  }, /*#__PURE__*/React.createElement(DesktopKeypad, _extends({
276
277
  onAnalyticsEvent: this.props.analytics.onAnalyticsEvent,
277
278
  extraKeys: this.props.extraKeys,
@@ -315,9 +316,9 @@ const MathInputIcon = ({
315
316
  color = Color.offBlack;
316
317
  break;
317
318
  }
318
- const dynamicClass = active || focused ? styles$C.iconActive : styles$C.iconInactive;
319
+ const dynamicClass = active || focused ? styles$B.iconActive : styles$B.iconInactive;
319
320
  return /*#__PURE__*/React.createElement(View, {
320
- style: [styles$C.iconContainer, dynamicClass]
321
+ style: [styles$B.iconContainer, dynamicClass]
321
322
  }, /*#__PURE__*/React.createElement("svg", {
322
323
  xmlns: "http://www.w3.org/2000/svg",
323
324
  width: "16",
@@ -362,7 +363,7 @@ const inputFocused = {
362
363
  borderColor: Color.blue,
363
364
  margin: -1
364
365
  };
365
- const styles$C = StyleSheet.create({
366
+ const styles$B = StyleSheet.create({
366
367
  iconContainer: {
367
368
  display: "flex",
368
369
  justifyContent: "center",
@@ -17566,33 +17567,33 @@ class Lint extends React.Component {
17566
17567
  let warningText;
17567
17568
  let warningTextStyle;
17568
17569
  if (this.props.severity === Severity.Error) {
17569
- severityStyle = styles$B.indicatorError;
17570
+ severityStyle = styles$A.indicatorError;
17570
17571
  warningText = "Error";
17571
- warningTextStyle = styles$B.publishBlockingError;
17572
+ warningTextStyle = styles$A.publishBlockingError;
17572
17573
  } else if (this.props.severity === Severity.Warning) {
17573
- severityStyle = styles$B.indicatorWarning;
17574
+ severityStyle = styles$A.indicatorWarning;
17574
17575
  warningText = "Warning";
17575
- warningTextStyle = styles$B.warning;
17576
+ warningTextStyle = styles$A.warning;
17576
17577
  } else {
17577
- severityStyle = styles$B.indicatorGuideline;
17578
+ severityStyle = styles$A.indicatorGuideline;
17578
17579
  warningText = "Recommendation";
17579
- warningTextStyle = styles$B.warning;
17580
+ warningTextStyle = styles$A.warning;
17580
17581
  }
17581
17582
  return /*#__PURE__*/React.createElement("a", {
17582
17583
  href: `https://khanacademy.org/r/linter-rules#${this.props.ruleName}`,
17583
17584
  target: "lint-help-window",
17584
17585
  className: css(style)
17585
17586
  }, /*#__PURE__*/React.createElement("span", {
17586
- className: css(styles$B.indicator, severityStyle)
17587
+ className: css(styles$A.indicator, severityStyle)
17587
17588
  }, this.props.severity === 1 && /*#__PURE__*/React.createElement(InlineIcon, exclamationIcon)), /*#__PURE__*/React.createElement("div", {
17588
- className: css(styles$B.tooltip, tooltipAbove && styles$B.tooltipAbove)
17589
+ className: css(styles$A.tooltip, tooltipAbove && styles$A.tooltipAbove)
17589
17590
  }, this.props.message.split("\n\n").map((m, i) => /*#__PURE__*/React.createElement("p", {
17590
17591
  key: i,
17591
- className: css(styles$B.tooltipParagraph)
17592
+ className: css(styles$A.tooltipParagraph)
17592
17593
  }, /*#__PURE__*/React.createElement("span", {
17593
17594
  className: css(warningTextStyle)
17594
17595
  }, warningText, ":", " "), m)), /*#__PURE__*/React.createElement("div", {
17595
- className: css(styles$B.tail, tooltipAbove && styles$B.tailAbove)
17596
+ className: css(styles$A.tail, tooltipAbove && styles$A.tailAbove)
17596
17597
  })));
17597
17598
  };
17598
17599
  }
@@ -17630,20 +17631,20 @@ class Lint extends React.Component {
17630
17631
  }
17631
17632
  if (blockHighlight) {
17632
17633
  return /*#__PURE__*/React.createElement("span", {
17633
- className: css(styles$B.lintContainer, styles$B.lintContainerBlock)
17634
- }, this.renderLink(styles$B.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17634
+ className: css(styles$A.lintContainer, styles$A.lintContainerBlock)
17635
+ }, this.renderLink(styles$A.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17635
17636
  }
17636
17637
  if (inline) {
17637
17638
  return /*#__PURE__*/React.createElement("span", {
17638
- className: css(styles$B.lintContainer)
17639
- }, this.renderLink(styles$B.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17639
+ className: css(styles$A.lintContainer)
17640
+ }, this.renderLink(styles$A.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17640
17641
  }
17641
17642
  return /*#__PURE__*/React.createElement("div", {
17642
- className: css(styles$B.lintContainer)
17643
- }, this.renderLink(styles$B.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
17643
+ className: css(styles$A.lintContainer)
17644
+ }, this.renderLink(styles$A.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
17644
17645
  }
17645
17646
  }
17646
- const styles$B = StyleSheet.create({
17647
+ const styles$A = StyleSheet.create({
17647
17648
  // This is the class of the outermost element.
17648
17649
  // We use relative positioning so that the lint indicator can be
17649
17650
  // positioned absolutely relative to the position of the linty container.
@@ -18397,6 +18398,27 @@ const registerWidgets = widgets => {
18397
18398
  });
18398
18399
  validateAlignments();
18399
18400
  };
18401
+
18402
+ /**
18403
+ *
18404
+ * @param name - the widget that you are trying to replace
18405
+ * @param replacementName - the name of the widget that takes its place
18406
+ *
18407
+ * e.g. replaceWidget("transformer", "always-correct") will make it so the
18408
+ * transformer widget is replaced by the always correct widget
18409
+ */
18410
+ const replaceWidget = (name, replacementName) => {
18411
+ const substituteWidget = widgets[replacementName];
18412
+ if (!substituteWidget && Log) {
18413
+ const errorMsg = `Failed to replace ${name} with ${replacementName}`;
18414
+ Log.error(errorMsg, Errors.Internal);
18415
+ return;
18416
+ }
18417
+ registerWidget(name, substituteWidget);
18418
+ if (Log) {
18419
+ Log.log(`INFO: Replacing widget ${name} with ${replacementName}`);
18420
+ }
18421
+ };
18400
18422
  const registerEditors = editorsToRegister => {
18401
18423
  editorsToRegister.forEach(editor => {
18402
18424
  if (!editor.widgetName) {
@@ -18712,6 +18734,7 @@ var widgets$1 = /*#__PURE__*/Object.freeze({
18712
18734
  __proto__: null,
18713
18735
  registerWidget: registerWidget,
18714
18736
  registerWidgets: registerWidgets,
18737
+ replaceWidget: replaceWidget,
18715
18738
  registerEditors: registerEditors,
18716
18739
  getWidget: getWidget,
18717
18740
  getEditor: getEditor,
@@ -19494,7 +19517,7 @@ class Renderer extends React.Component {
19494
19517
  paddingLeft: margin,
19495
19518
  paddingRight: margin
19496
19519
  };
19497
- const mobileInnerStyle = _extends({}, innerStyle, styles$A.mobileZoomableParentFix);
19520
+ const mobileInnerStyle = _extends({}, innerStyle, styles$z.mobileZoomableParentFix);
19498
19521
  return /*#__PURE__*/React.createElement("div", {
19499
19522
  key: state.key,
19500
19523
  className: "perseus-block-math",
@@ -19615,7 +19638,7 @@ class Renderer extends React.Component {
19615
19638
  paddingLeft: 0,
19616
19639
  paddingRight: 0
19617
19640
  };
19618
- const mobileInnerStyle = _extends({}, innerStyle, styles$A.mobileZoomableParentFix);
19641
+ const mobileInnerStyle = _extends({}, innerStyle, styles$z.mobileZoomableParentFix);
19619
19642
  const wrappedOutput = /*#__PURE__*/React.createElement("div", {
19620
19643
  style: _extends({}, mobileInnerStyle, {
19621
19644
  overflowX: "auto"
@@ -20262,7 +20285,7 @@ Renderer.defaultProps = {
20262
20285
  onSerializedStateUpdated: () => {},
20263
20286
  linterContext: PerseusLinter.linterContextDefault
20264
20287
  };
20265
- const styles$A = {
20288
+ const styles$z = {
20266
20289
  mobileZoomableParentFix: {
20267
20290
  // NOTE(abdul): There is an issue where transform animations will
20268
20291
  // cause the Zoomable component to disappear when running on the
@@ -21279,10 +21302,10 @@ class InputNumber extends React.Component {
21279
21302
  // Note: This is _very_ similar to what `numeric-input.jsx` does. If
21280
21303
  // you modify this, double-check if you also need to modify that
21281
21304
  // component.
21282
- const inputStyles = [styles$z.default, this.props.size === "small" ? styles$z.small : null, this.props.rightAlign ? styles$z.rightAlign : styles$z.leftAlign];
21305
+ const inputStyles = [styles$y.default, this.props.size === "small" ? styles$y.small : null, this.props.rightAlign ? styles$y.rightAlign : styles$y.leftAlign];
21283
21306
  // Unanswered
21284
21307
  if (rubric && !this.props.currentValue) {
21285
- inputStyles.push(styles$z.answerStateUnanswered);
21308
+ inputStyles.push(styles$y.answerStateUnanswered);
21286
21309
  }
21287
21310
  return /*#__PURE__*/React.createElement(InputWithExamples
21288
21311
  // eslint-disable-next-line react/no-string-refs
@@ -21367,7 +21390,7 @@ InputNumber.defaultProps = {
21367
21390
  apiOptions: ApiOptions.defaults,
21368
21391
  linterContext: linterContextDefault
21369
21392
  };
21370
- const styles$z = StyleSheet.create({
21393
+ const styles$y = StyleSheet.create({
21371
21394
  default: {
21372
21395
  width: 80,
21373
21396
  height: "auto"
@@ -22368,7 +22391,7 @@ const FocusRing = function FocusRing(props) {
22368
22391
  };
22369
22392
  return /*#__PURE__*/React.createElement("span", {
22370
22393
  "data-test-id": "focus-ring",
22371
- className: css(styles$y.ring),
22394
+ className: css(styles$x.ring),
22372
22395
  style: style
22373
22396
  }, children);
22374
22397
  };
@@ -22377,7 +22400,7 @@ FocusRing.defaultProps = {
22377
22400
  color: kaGreen$1,
22378
22401
  multipleSelect: false
22379
22402
  };
22380
- const styles$y = StyleSheet.create({
22403
+ const styles$x = StyleSheet.create({
22381
22404
  ring: {
22382
22405
  margin: "auto",
22383
22406
  display: "inline-block",
@@ -22414,7 +22437,7 @@ function getChoiceLetter(pos) {
22414
22437
  }
22415
22438
 
22416
22439
  const CHOICE_ICON_SIZE = 24;
22417
- const styles$x = StyleSheet.create({
22440
+ const styles$w = StyleSheet.create({
22418
22441
  iconWrapper: {
22419
22442
  display: "inline-block",
22420
22443
  position: "relative"
@@ -22437,7 +22460,7 @@ const CrossOutLine = function CrossOutLine(props) {
22437
22460
  width: CROSS_OUT_LINE_SIZE,
22438
22461
  height: CROSS_OUT_LINE_SIZE,
22439
22462
  viewBox: `0 0 ${CROSS_OUT_LINE_SIZE} ${CROSS_OUT_LINE_SIZE}`,
22440
- className: css(styles$w.crossOutLine),
22463
+ className: css(styles$v.crossOutLine),
22441
22464
  "data-test-id": "choice-icon__cross-out-line"
22442
22465
  }, /*#__PURE__*/React.createElement("line", {
22443
22466
  // The line stretches from the bottom-left to top-right.
@@ -22452,7 +22475,7 @@ const CrossOutLine = function CrossOutLine(props) {
22452
22475
  strokeLinecap: "round"
22453
22476
  }));
22454
22477
  };
22455
- const styles$w = StyleSheet.create({
22478
+ const styles$v = StyleSheet.create({
22456
22479
  crossOutLine: {
22457
22480
  // Center the icon within the container.
22458
22481
  position: "absolute",
@@ -22533,7 +22556,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22533
22556
  } = props;
22534
22557
  const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
22535
22558
  return /*#__PURE__*/React.createElement("div", {
22536
- className: css(styles$x.iconWrapper)
22559
+ className: css(styles$w.iconWrapper)
22537
22560
  }, /*#__PURE__*/React.createElement(FocusRing, {
22538
22561
  color: Color.blue,
22539
22562
  visible: focused || hovered,
@@ -22542,13 +22565,13 @@ const ChoiceIcon = function ChoiceIcon(props) {
22542
22565
  // @ts-expect-error - TS2322 - Type '{ backgroundColor: string | null | undefined; borderColor: string; color: string; borderRadius: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
22543
22566
  style: dynamicStyles,
22544
22567
  "data-test-id": "choice-icon__library-choice-icon",
22545
- className: css(styles$v.circle, showCorrectness && correct && styles$v.circleCorrect, showCorrectness && !correct && styles$v.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$v.circleIncorrectAnswered)
22568
+ className: css(styles$u.circle, showCorrectness && correct && styles$u.circleCorrect, showCorrectness && !correct && styles$u.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$u.circleIncorrectAnswered)
22546
22569
  // used in BaseRadio editMode to check
22547
22570
  // if we actually clicked on the radio icon
22548
22571
  ,
22549
22572
  "data-is-radio-icon": true
22550
22573
  }, /*#__PURE__*/React.createElement("div", {
22551
- className: css(styles$v.innerWrapper)
22574
+ className: css(styles$u.innerWrapper)
22552
22575
  }, /*#__PURE__*/React.createElement(ChoiceInner, {
22553
22576
  pos: pos,
22554
22577
  showCorrectness: showCorrectness,
@@ -22557,7 +22580,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22557
22580
  color: dynamicStyles.borderColor
22558
22581
  }));
22559
22582
  };
22560
- const styles$v = StyleSheet.create({
22583
+ const styles$u = StyleSheet.create({
22561
22584
  innerWrapper: {
22562
22585
  width: CHOICE_ICON_SIZE,
22563
22586
  height: CHOICE_ICON_SIZE,
@@ -22649,19 +22672,19 @@ const OptionStatus = function OptionStatus(props) {
22649
22672
  }
22650
22673
  let textStyle;
22651
22674
  if (correct) {
22652
- textStyle = styles$u.correct;
22675
+ textStyle = styles$t.correct;
22653
22676
  } else {
22654
22677
  if (checked || previouslyAnswered) {
22655
- textStyle = styles$u.incorrectAnswered;
22678
+ textStyle = styles$t.incorrectAnswered;
22656
22679
  } else {
22657
- textStyle = styles$u.incorrect;
22680
+ textStyle = styles$t.incorrect;
22658
22681
  }
22659
22682
  }
22660
22683
  return /*#__PURE__*/React.createElement("div", {
22661
- className: css(styles$u.text, textStyle)
22684
+ className: css(styles$t.text, textStyle)
22662
22685
  }, renderText(checked, correct, crossedOut));
22663
22686
  };
22664
- const styles$u = StyleSheet.create({
22687
+ const styles$t = StyleSheet.create({
22665
22688
  text: {
22666
22689
  alignItems: "center",
22667
22690
  display: "flex",
@@ -22736,8 +22759,8 @@ const Choice = function Choice(props) {
22736
22759
  crossedOut: updatedCrossedOut
22737
22760
  });
22738
22761
  }
22739
- const descriptionClassName = classNames("description", css(styles$t.description));
22740
- const rationaleClassName = classNames("perseus-radio-rationale-content", css(styles$t.rationale));
22762
+ const descriptionClassName = classNames("description", css(styles$s.description));
22763
+ const rationaleClassName = classNames("perseus-radio-rationale-content", css(styles$s.rationale));
22741
22764
 
22742
22765
  // We want to show the choices as dimmed out when the choices are disabled.
22743
22766
  // However, we don't want to do this when we're in review mode in the
@@ -22897,7 +22920,7 @@ const Choice = function Choice(props) {
22897
22920
  "data-test-id": `perseus-radio-rationale-content-${pos}`
22898
22921
  }, rationale));
22899
22922
  };
22900
- const styles$t = StyleSheet.create({
22923
+ const styles$s = StyleSheet.create({
22901
22924
  description: {
22902
22925
  display: "inline-block",
22903
22926
  width: "100%"
@@ -23089,10 +23112,10 @@ const BaseRadio = function BaseRadio(props) {
23089
23112
  const isMobile = apiOptions.isMobile;
23090
23113
  const firstChoiceHighlighted = choices[0].highlighted;
23091
23114
  const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
23092
- const className = classNames("perseus-widget-radio", !editMode && "perseus-rendered-radio", css(styles$s.radio, styles$s.responsiveRadioContainer, firstChoiceHighlighted && isMobile && styles$s.radioContainerFirstHighlighted, lastChoiceHighlighted && isMobile && styles$s.radioContainerLastHighlighted));
23093
- const instructionsClassName = classNames("instructions", css(styles$s.instructions, isMobile && styles$s.instructionsMobile));
23115
+ const className = classNames("perseus-widget-radio", !editMode && "perseus-rendered-radio", css(styles$r.radio, styles$r.responsiveRadioContainer, firstChoiceHighlighted && isMobile && styles$r.radioContainerFirstHighlighted, lastChoiceHighlighted && isMobile && styles$r.radioContainerLastHighlighted));
23116
+ const instructionsClassName = classNames("instructions", css(styles$r.instructions, isMobile && styles$r.instructionsMobile));
23094
23117
  const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
23095
- const responsiveClassName = css(styles$s.responsiveFieldset);
23118
+ const responsiveClassName = css(styles$r.responsiveFieldset);
23096
23119
  const fieldset = /*#__PURE__*/React.createElement("fieldset", {
23097
23120
  className: `perseus-widget-radio-fieldset ${responsiveClassName}`
23098
23121
  }, /*#__PURE__*/React.createElement("legend", {
@@ -23143,7 +23166,7 @@ const BaseRadio = function BaseRadio(props) {
23143
23166
  const aphroditeClassName = checked => {
23144
23167
  // Whether or not to show correctness borders
23145
23168
  // for this choice and the next choice.
23146
- return css(sharedStyles.aboveScratchpad, styles$s.item, styles$s.responsiveItem, checked && styles$s.selectedItem, checked && choice.highlighted && styles$s.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$s.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$s.nextHighlighted);
23169
+ return css(sharedStyles.aboveScratchpad, styles$r.item, styles$r.responsiveItem, checked && styles$r.selectedItem, checked && choice.highlighted && styles$r.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$r.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$r.nextHighlighted);
23147
23170
  };
23148
23171
 
23149
23172
  // HACK(abdulrahman): Preloads the selection-state
@@ -23217,14 +23240,14 @@ const BaseRadio = function BaseRadio(props) {
23217
23240
  // Allow for horizontal scrolling if content is too wide, which may be
23218
23241
  // an issue especially on phones.
23219
23242
  return /*#__PURE__*/React.createElement("div", {
23220
- className: css(styles$s.responsiveContainer)
23243
+ className: css(styles$r.responsiveContainer)
23221
23244
  }, fieldset);
23222
23245
  };
23223
23246
  BaseRadio.defaultProps = {
23224
23247
  editMode: false,
23225
23248
  multipleSelect: false
23226
23249
  };
23227
- const styles$s = StyleSheet.create({
23250
+ const styles$r = StyleSheet.create({
23228
23251
  instructions: {
23229
23252
  display: "block",
23230
23253
  color: gray17,
@@ -23872,9 +23895,9 @@ class Categorizer extends React.Component {
23872
23895
  indexedItems = Util.shuffle(indexedItems, this.props.problemNum);
23873
23896
  }
23874
23897
  const table = /*#__PURE__*/React.createElement("table", {
23875
- className: "categorizer-table " + css(styles$r.mobileTable)
23898
+ className: "categorizer-table " + css(styles$q.mobileTable)
23876
23899
  }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", {
23877
- className: css(styles$r.emptyHeaderCell)
23900
+ className: css(styles$q.emptyHeaderCell)
23878
23901
  }), this.props.categories.map((category, i) => {
23879
23902
  // Array index is the correct key here, as that's
23880
23903
  // how category grading actually works -- no way
@@ -23882,7 +23905,7 @@ class Categorizer extends React.Component {
23882
23905
  // middle. (If we later add that, this should be
23883
23906
  // fixed.)
23884
23907
  return /*#__PURE__*/React.createElement("th", {
23885
- className: css(styles$r.header),
23908
+ className: css(styles$q.header),
23886
23909
  key: i
23887
23910
  }, /*#__PURE__*/React.createElement(Renderer, {
23888
23911
  content: category,
@@ -23902,7 +23925,7 @@ class Categorizer extends React.Component {
23902
23925
  })), self.props.categories.map((catName, catNum) => {
23903
23926
  const selected = self.props.values[itemNum] === catNum;
23904
23927
  return /*#__PURE__*/React.createElement("td", {
23905
- className: "category " + css(styles$r.cell),
23928
+ className: "category " + css(styles$q.cell),
23906
23929
  key: catNum
23907
23930
  }, /*#__PURE__*/React.createElement("div", {
23908
23931
  className: ClassNames.INTERACTIVE,
@@ -23921,7 +23944,7 @@ class Categorizer extends React.Component {
23921
23944
  onChange: this.onChange.bind(this, itemNum, catNum),
23922
23945
  onClick: e => e.stopPropagation()
23923
23946
  }), !isMobile && /*#__PURE__*/React.createElement("span", {
23924
- className: css(styles$r.radioSpan, selected && styles$r.checkedRadioSpan, this.props.static && selected && styles$r.staticCheckedRadioSpan)
23947
+ className: css(styles$q.radioSpan, selected && styles$q.checkedRadioSpan, this.props.static && selected && styles$q.staticCheckedRadioSpan)
23925
23948
  }, selected ? /*#__PURE__*/React.createElement(InlineIcon, iconCircle) : /*#__PURE__*/React.createElement(InlineIcon, iconCircleThin))));
23926
23949
  }));
23927
23950
  /* eslint-enable max-len */
@@ -23933,7 +23956,7 @@ class Categorizer extends React.Component {
23933
23956
  "categorizer-container": true,
23934
23957
  "static-mode": this.props.static
23935
23958
  });
23936
- const inlineStyles = this.props.apiOptions.isMobile ? [styles$r.fullBleedContainer] : [];
23959
+ const inlineStyles = this.props.apiOptions.isMobile ? [styles$q.fullBleedContainer] : [];
23937
23960
  return /*#__PURE__*/React.createElement("div", {
23938
23961
  className: extraClassNames + " " + css(...inlineStyles)
23939
23962
  }, table);
@@ -23978,7 +24001,7 @@ const pageMargin = 16;
23978
24001
 
23979
24002
  // Stylesheets aren't directly testable
23980
24003
  /* c8 ignore next */
23981
- const styles$r = StyleSheet.create({
24004
+ const styles$q = StyleSheet.create({
23982
24005
  mobileTable: {
23983
24006
  [mediaQueries.smOrSmaller]: {
23984
24007
  minWidth: "auto"
@@ -24214,7 +24237,7 @@ class CSProgram extends React.Component {
24214
24237
  // creator "went wild".
24215
24238
  // http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
24216
24239
  return /*#__PURE__*/React.createElement("div", {
24217
- className: css(styles$q.widthOverride, styleContainer && styles$q.container)
24240
+ className: css(styles$p.widthOverride, styleContainer && styles$p.container)
24218
24241
  }, /*#__PURE__*/React.createElement("iframe", {
24219
24242
  sandbox: sandboxOptions,
24220
24243
  src: url,
@@ -24243,7 +24266,7 @@ CSProgram.defaultProps = {
24243
24266
  // optional message
24244
24267
  message: null
24245
24268
  };
24246
- const styles$q = StyleSheet.create({
24269
+ const styles$p = StyleSheet.create({
24247
24270
  // Override the inherited width from the perseus paragraph class
24248
24271
  // 820 is minimum width for 420px editor and 400px canvas
24249
24272
  widthOverride: {
@@ -24288,7 +24311,7 @@ class Definition extends React.Component {
24288
24311
  }) => /*#__PURE__*/React.createElement(Popover, {
24289
24312
  content: /*#__PURE__*/React.createElement(PopoverContentCore, {
24290
24313
  color: "white",
24291
- style: styles$p.tooltipBody,
24314
+ style: styles$o.tooltipBody,
24292
24315
  closeButtonVisible: true
24293
24316
  }, /*#__PURE__*/React.createElement(Renderer, {
24294
24317
  apiOptions: this.props.apiOptions,
@@ -24319,7 +24342,7 @@ Definition.defaultProps = {
24319
24342
  togglePrompt: "define me",
24320
24343
  definition: "definition goes here"
24321
24344
  };
24322
- const styles$p = {
24345
+ const styles$o = {
24323
24346
  tooltipBody: {
24324
24347
  color: Color.offBlack,
24325
24348
  fontSize: 20,
@@ -24336,6 +24359,47 @@ var Definition$1 = {
24336
24359
  transform: x => x
24337
24360
  };
24338
24361
 
24362
+ // The props are type `any` on purpose so that this can receive props
24363
+ // from any deprecated widget
24364
+ class AutoCorrect extends React__default.Component {
24365
+ constructor(...args) {
24366
+ super(...args);
24367
+ this.getUserInput = () => {
24368
+ return {};
24369
+ };
24370
+ this.simpleValidate = rubric => {
24371
+ return AutoCorrect.validate(this.getUserInput(), rubric);
24372
+ };
24373
+ }
24374
+ static validate(userInput, rubric) {
24375
+ // Since this mean to replace an existing widget the learner
24376
+ // WILL earn points for this widget
24377
+ return {
24378
+ type: "points",
24379
+ earned: 1,
24380
+ total: 1,
24381
+ message: null
24382
+ };
24383
+ }
24384
+ render() {
24385
+ return /*#__PURE__*/React__default.createElement("div", {
24386
+ style: {
24387
+ paddingTop: 8,
24388
+ paddingBottom: 8
24389
+ }
24390
+ }, /*#__PURE__*/React__default.createElement(Banner, {
24391
+ text: i18n._("This part of the question is no-longer available. You will not be graded on this part."),
24392
+ kind: "warning",
24393
+ layout: "full-width"
24394
+ }));
24395
+ }
24396
+ }
24397
+ var DeprecatedStandin = {
24398
+ name: "deprecated-standin",
24399
+ displayName: "Deprecated Standin",
24400
+ widget: AutoCorrect
24401
+ };
24402
+
24339
24403
  class Dropdown extends React.Component {
24340
24404
  constructor(...args) {
24341
24405
  super(...args);
@@ -24483,15 +24547,15 @@ class Explanation extends React.Component {
24483
24547
  const onClick = this._onClick;
24484
24548
  if (isMobile) {
24485
24549
  linkContainer = /*#__PURE__*/React.createElement("div", {
24486
- className: css(styles$o.linkContainer)
24550
+ className: css(styles$n.linkContainer)
24487
24551
  }, /*#__PURE__*/React.createElement("a", {
24488
- className: css(styles$o.mobileExplanationLink),
24552
+ className: css(styles$n.mobileExplanationLink),
24489
24553
  href: href,
24490
24554
  onClick: onClick,
24491
24555
  role: "button",
24492
24556
  "aria-expanded": this.state.expanded
24493
24557
  }, linkAnchor), this.state.expanded && /*#__PURE__*/React.createElement("svg", {
24494
- className: css(styles$o.disclosureArrow)
24558
+ className: css(styles$n.disclosureArrow)
24495
24559
  }, /*#__PURE__*/React.createElement("polygon", {
24496
24560
  style: {
24497
24561
  fill: backgroundColor
@@ -24504,13 +24568,13 @@ class Explanation extends React.Component {
24504
24568
  // an anchor tag (if rendered in a modal) or a Wonder Blocks
24505
24569
  // link component in lesson page, see: `article-page.jsx`.
24506
24570
  linkContainer = /*#__PURE__*/React.createElement("div", {
24507
- className: css(styles$o.linkContainer, styles$o.articleLink)
24571
+ className: css(styles$n.linkContainer, styles$n.articleLink)
24508
24572
  }, /*#__PURE__*/React.createElement(Link, {
24509
- className: css(styles$o.explanationLink)
24573
+ className: css(styles$n.explanationLink)
24510
24574
  // HACK(michaelpolyak): WB Link doesn't passthrough
24511
24575
  // class name.
24512
24576
  ,
24513
- style: styles$o.explanationLink,
24577
+ style: styles$n.explanationLink,
24514
24578
  href: href,
24515
24579
  onClick: onClick,
24516
24580
  role: "button",
@@ -24520,9 +24584,9 @@ class Explanation extends React.Component {
24520
24584
  // NOTE: For exercises, the baseElements `Link` is an
24521
24585
  // anchor tag, see: `perseus-api.jsx`.
24522
24586
  linkContainer = /*#__PURE__*/React.createElement("div", {
24523
- className: css(styles$o.linkContainer, styles$o.exerciseLink)
24587
+ className: css(styles$n.linkContainer, styles$n.exerciseLink)
24524
24588
  }, /*#__PURE__*/React.createElement(Link, {
24525
- className: css(styles$o.explanationLink),
24589
+ className: css(styles$n.explanationLink),
24526
24590
  href: href,
24527
24591
  onClick: onClick,
24528
24592
  role: "button",
@@ -24530,11 +24594,11 @@ class Explanation extends React.Component {
24530
24594
  }, `[${linkAnchor}]`));
24531
24595
  }
24532
24596
  }
24533
- const expandedStyle = isMobile ? styles$o.contentExpandedMobile : styles$o.contentExpanded;
24597
+ const expandedStyle = isMobile ? styles$n.contentExpandedMobile : styles$n.contentExpanded;
24534
24598
  return /*#__PURE__*/React.createElement("div", {
24535
- className: css(styles$o.container)
24599
+ className: css(styles$n.container)
24536
24600
  }, linkContainer, this.state.expanded && /*#__PURE__*/React.createElement("div", {
24537
- className: css(styles$o.content, isMobile && styles$o.contentMobile, this.state.expanded && expandedStyle)
24601
+ className: css(styles$n.content, isMobile && styles$n.contentMobile, this.state.expanded && expandedStyle)
24538
24602
  // eslint-disable-next-line react/no-string-refs
24539
24603
  ,
24540
24604
  ref: "content"
@@ -24558,7 +24622,7 @@ const verticalContentPadding = 10;
24558
24622
  const arrowWidth = 30;
24559
24623
  const arrowHeight = 14;
24560
24624
  const backgroundColor = gray95;
24561
- const styles$o = StyleSheet.create({
24625
+ const styles$n = StyleSheet.create({
24562
24626
  container: {
24563
24627
  display: "inline",
24564
24628
  position: "relative"
@@ -24663,15 +24727,15 @@ class GradedGroupAnswerBar extends React.Component {
24663
24727
  onCheckAnswer,
24664
24728
  onNextQuestion
24665
24729
  } = this.props;
24666
- const answerBarStyle = _extends({}, styles$n.answerBar, {
24730
+ const answerBarStyle = _extends({}, styles$m.answerBar, {
24667
24731
  backgroundColor: answerBarState === "CORRECT" ? Color.offWhite : "white",
24668
24732
  // Center the "Correct!" message only when there's no next question
24669
24733
  justifyContent: answerBarState === "CORRECT" && !onNextQuestion ? "center" : "space-between"
24670
24734
  });
24671
24735
  const message = answerBarState === "INCORRECT" ? /*#__PURE__*/React.createElement("span", {
24672
- style: styles$n.text
24736
+ style: styles$m.text
24673
24737
  }, /*#__PURE__*/React.createElement("span", {
24674
- style: styles$n.tryAgainIcon
24738
+ style: styles$m.tryAgainIcon
24675
24739
  }, /*#__PURE__*/React.createElement(InlineIcon, iconTryAgain)), /*#__PURE__*/React.createElement("span", {
24676
24740
  style: {
24677
24741
  marginLeft: 8
@@ -24690,7 +24754,7 @@ class GradedGroupAnswerBar extends React.Component {
24690
24754
  return /*#__PURE__*/React.createElement("div", {
24691
24755
  style: answerBarStyle
24692
24756
  }, /*#__PURE__*/React.createElement("span", {
24693
- style: styles$n.text
24757
+ style: styles$m.text
24694
24758
  }, /*#__PURE__*/React.createElement("span", {
24695
24759
  style: {
24696
24760
  fontSize: 28,
@@ -24712,7 +24776,7 @@ class GradedGroupAnswerBar extends React.Component {
24712
24776
  }
24713
24777
  }
24714
24778
  const fontSize = 17;
24715
- const styles$n = {
24779
+ const styles$m = {
24716
24780
  answerBar: {
24717
24781
  display: "flex",
24718
24782
  alignItems: "center",
@@ -24898,7 +24962,7 @@ class GradedGroup extends React.Component {
24898
24962
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
24899
24963
  gradeStatus = i18n._("Incorrect");
24900
24964
  }
24901
- const mobileClass = this.props.inGradedGroupSet ? css(styles$m.gradedGroupInSet) : css(styles$m.gradedGroup);
24965
+ const mobileClass = this.props.inGradedGroupSet ? css(styles$l.gradedGroupInSet) : css(styles$l.gradedGroup);
24902
24966
  const classes = classNames({
24903
24967
  [mobileClass]: apiOptions.isMobile,
24904
24968
  "perseus-graded-group": true,
@@ -24917,7 +24981,7 @@ class GradedGroup extends React.Component {
24917
24981
  return /*#__PURE__*/React.createElement("div", {
24918
24982
  className: classes
24919
24983
  }, !!this.props.title && /*#__PURE__*/React.createElement("div", {
24920
- className: css(styles$m.title)
24984
+ className: css(styles$l.title)
24921
24985
  }, this.props.title), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props, {
24922
24986
  // eslint-disable-next-line react/no-string-refs
24923
24987
  ref: "renderer",
@@ -24949,7 +25013,7 @@ class GradedGroup extends React.Component {
24949
25013
  }, i18n._("Next question")), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
24950
25014
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
24951
25015
  tabIndex: "0",
24952
- className: css(styles$m.explanationTitle),
25016
+ className: css(styles$l.explanationTitle),
24953
25017
  onClick: () => this.setState({
24954
25018
  showHint: false
24955
25019
  }),
@@ -24981,7 +25045,7 @@ class GradedGroup extends React.Component {
24981
25045
  showHint: true
24982
25046
  });
24983
25047
  },
24984
- className: css(styles$m.showHintLink)
25048
+ className: css(styles$l.showHintLink)
24985
25049
  }, i18n._("Explain"))), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
24986
25050
  apiOptions: apiOptions,
24987
25051
  answerBarState: answerBarState,
@@ -25002,7 +25066,7 @@ GradedGroup.defaultProps = {
25002
25066
  const traverseChildWidgets$3 = function traverseChildWidgets(props, traverseRenderer) {
25003
25067
  return _.extend({}, props, traverseRenderer(props));
25004
25068
  };
25005
- const styles$m = StyleSheet.create({
25069
+ const styles$l = StyleSheet.create({
25006
25070
  gradedGroupInSet: {
25007
25071
  // Reset a few desktop-only styles that come from graded-group.less
25008
25072
  marginLeft: 0,
@@ -25075,18 +25139,18 @@ class Indicators extends React.Component {
25075
25139
  return /*#__PURE__*/React.createElement("ul", {
25076
25140
  // reduntantly add class name for use in .less files--
25077
25141
  // the styles object key gets hashed
25078
- className: classNames(css(styles$l.indicatorContainer), "indicatorContainer")
25142
+ className: classNames(css(styles$k.indicatorContainer), "indicatorContainer")
25079
25143
  }, this.props.gradedGroups.map(({
25080
25144
  title
25081
25145
  }, i) => /*#__PURE__*/React.createElement("li", {
25082
- className: css(styles$l.indicator),
25146
+ className: css(styles$k.indicator),
25083
25147
  key: title
25084
25148
  }, /*#__PURE__*/React.createElement(Clickable, {
25085
25149
  role: "button",
25086
25150
  "aria-label": i18n._("Skip to %(title)s", {
25087
25151
  title
25088
25152
  }),
25089
- style: styles$l.indicatorButton,
25153
+ style: styles$k.indicatorButton,
25090
25154
  onClick: () => this.props.onChangeCurrentGroup(i),
25091
25155
  onKeyDown: e => this.handleKeyDown(e, i)
25092
25156
  }, ({
@@ -25094,9 +25158,9 @@ class Indicators extends React.Component {
25094
25158
  focused,
25095
25159
  pressed
25096
25160
  }) => /*#__PURE__*/React.createElement(View, {
25097
- style: [styles$l.indicatorDot, (hovered || focused || pressed) && styles$l.indicatorDotFocused]
25161
+ style: [styles$k.indicatorDot, (hovered || focused || pressed) && styles$k.indicatorDotFocused]
25098
25162
  }, i === this.props.currentGroup && /*#__PURE__*/React.createElement(View, {
25099
- style: styles$l.indicatorDotActive
25163
+ style: styles$k.indicatorDotActive
25100
25164
  }, /*#__PURE__*/React.createElement("span", {
25101
25165
  className: css(a11y.srOnly)
25102
25166
  }, i18n._("Current"))))))));
@@ -25162,7 +25226,7 @@ class GradedGroupSet extends React.Component {
25162
25226
  } = getDependencies();
25163
25227
  if (JIPT.useJIPT && this.props.gradedGroups.length > 1) {
25164
25228
  return /*#__PURE__*/React.createElement("div", {
25165
- className: css(styles$l.container)
25229
+ className: css(styles$k.container)
25166
25230
  }, this.props.gradedGroups.map((gradedGroup, i) => {
25167
25231
  return (
25168
25232
  /*#__PURE__*/
@@ -25185,13 +25249,13 @@ class GradedGroupSet extends React.Component {
25185
25249
  const numGroups = this.props.gradedGroups.length;
25186
25250
  const handleNextQuestion = this.state.currentGroup < numGroups - 1 ? this.handleNextQuestion : null;
25187
25251
  return /*#__PURE__*/React.createElement("div", {
25188
- className: css(styles$l.container)
25252
+ className: css(styles$k.container)
25189
25253
  }, /*#__PURE__*/React.createElement("div", {
25190
- className: css(styles$l.top)
25254
+ className: css(styles$k.top)
25191
25255
  }, /*#__PURE__*/React.createElement("div", {
25192
- className: css(styles$l.title)
25256
+ className: css(styles$k.title)
25193
25257
  }, currentGroup.title), /*#__PURE__*/React.createElement("div", {
25194
- className: css(styles$l.spacer)
25258
+ className: css(styles$k.spacer)
25195
25259
  }), /*#__PURE__*/React.createElement(Indicators, {
25196
25260
  currentGroup: this.state.currentGroup,
25197
25261
  gradedGroups: this.props.gradedGroups,
@@ -25233,7 +25297,7 @@ var GradedGroupSet$1 = {
25233
25297
  tracking: "all",
25234
25298
  isLintable: true
25235
25299
  };
25236
- const styles$l = StyleSheet.create({
25300
+ const styles$k = StyleSheet.create({
25237
25301
  top: {
25238
25302
  display: "flex",
25239
25303
  flexDirection: "row"
@@ -25338,7 +25402,7 @@ class ButtonGroup extends React.Component {
25338
25402
  id: "" + i,
25339
25403
  ref: "button" + i,
25340
25404
  key: "" + i,
25341
- className: css(styles$k.buttonStyle, button.value === value && styles$k.selectedStyle),
25405
+ className: css(styles$j.buttonStyle, button.value === value && styles$j.selectedStyle),
25342
25406
  onClick: () => this.toggleSelect(button.value)
25343
25407
  }, button.content || "" + button.value);
25344
25408
  });
@@ -25355,7 +25419,7 @@ ButtonGroup.defaultProps = {
25355
25419
  value: null,
25356
25420
  allowEmpty: true
25357
25421
  };
25358
- const styles$k = StyleSheet.create({
25422
+ const styles$j = StyleSheet.create({
25359
25423
  buttonStyle: {
25360
25424
  backgroundColor: "white",
25361
25425
  border: "1px solid #ccc",
@@ -30835,214 +30899,89 @@ var globalStyles = /*#__PURE__*/Object.freeze({
30835
30899
  typography: typography
30836
30900
  });
30837
30901
 
30838
- class AnswerChoice extends React.Component {
30839
- constructor(...args) {
30840
- super(...args);
30841
- // The radio button or checkbox input control.
30842
- this._input = void 0;
30843
- this.state = {
30844
- isInputFocused: false
30845
- };
30846
- }
30847
- focusInput() {
30848
- if (this._input) {
30849
- this._input.focus();
30850
- }
30851
- }
30852
- handleClick(e) {
30902
+ const AnswerChoices = props => {
30903
+ const onAnswerChange = selected => {
30853
30904
  const {
30854
- checked,
30905
+ choices,
30855
30906
  onChange
30856
- } = this.props;
30857
- e.preventDefault();
30858
-
30859
- // We manually handle clicks, setting focus on the input control.
30860
- this.focusInput();
30861
-
30862
- // And toggling the answer selection state.
30863
- onChange(!checked);
30864
- }
30865
- handleKeyDown(e) {
30866
- const {
30867
- onFocusPrevAnswer,
30868
- onFocusNextAnswer
30869
- } = this.props;
30907
+ } = props;
30908
+ onChange(choices.map(choice => selected.includes(choice.content)));
30909
+ };
30870
30910
 
30871
- // This may override built-in browser arrow key navigation for radio
30872
- // group. Checkbox fieldset does not have such built-in capability,
30873
- // thus we do this manually for both input types.
30874
- switch (e.key) {
30875
- case "ArrowUp":
30876
- e.preventDefault();
30877
- onFocusPrevAnswer();
30878
- break;
30879
- case "ArrowDown":
30880
- e.preventDefault();
30881
- onFocusNextAnswer();
30882
- break;
30883
- }
30884
- }
30885
- handleInputChange(e) {
30886
- // Prevent answer selection from double-toggling, as `handleClick` is
30887
- // handling changes to selection state.
30888
- e.preventDefault();
30889
- }
30890
- handleInputFocus() {
30891
- this.setState({
30892
- isInputFocused: true
30893
- });
30894
- }
30895
- handleInputBlur() {
30896
- this.setState({
30897
- isInputFocused: false
30898
- });
30899
- }
30900
- renderCheckmark() {
30901
- const {
30902
- checked
30903
- } = this.props;
30904
- return /*#__PURE__*/React.createElement("span", {
30905
- className: css(styles$j.checkmarkContainer)
30906
- }, checked && /*#__PURE__*/React.createElement(Icon, {
30907
- icon: iconCheckMedium,
30908
- size: 16,
30909
- color: Color.offBlack
30910
- }));
30911
- }
30912
- render() {
30913
- const {
30914
- content,
30915
- checked,
30916
- groupName,
30917
- index,
30918
- inputType
30919
- } = this.props;
30920
- const {
30921
- isInputFocused
30922
- } = this.state;
30923
- const answerId = `${groupName}-${index}`;
30924
- return /*#__PURE__*/React.createElement("div", {
30925
- className: css(styles$j.answerChoice, isInputFocused && styles$j.answerFocused),
30926
- onClick: e => this.handleClick(e),
30927
- onKeyDown: e => this.handleKeyDown(e)
30928
- }, /*#__PURE__*/React.createElement("span", {
30929
- className: css(styles$j.answerInputWrapper)
30930
- }, /*#__PURE__*/React.createElement("input", {
30931
- className: css(sharedStyles.perseusSrOnly),
30932
- checked: checked,
30933
- name: groupName,
30934
- id: answerId,
30935
- type: inputType,
30936
- onChange: e => this.handleInputChange(e),
30937
- onFocus: () => this.handleInputFocus(),
30938
- onBlur: () => this.handleInputBlur(),
30939
- ref: node => this._input = node
30940
- }), this.renderCheckmark()), /*#__PURE__*/React.createElement("label", {
30941
- htmlFor: answerId
30942
- }, /*#__PURE__*/React.createElement(Renderer, {
30943
- content: content
30944
- })));
30945
- }
30946
- }
30947
- class AnswerChoices extends React.Component {
30948
- constructor(props) {
30949
- super(props);
30950
- // The rendered answers elements.
30951
- this._choices = void 0;
30952
- this._choices = [];
30953
- this.state = {
30954
- groupName: _.uniqueId("answer_choices_")
30955
- };
30956
- }
30957
- focusAnswer(index = 0) {
30958
- const numChoices = this.props.choices.length;
30911
+ // WB Dropdown types only take an array of nodes, so we can't use a
30912
+ // functional component, which can only return a single node.
30913
+ const AnswerItems = choices => choices.map(({
30914
+ content
30915
+ }) => /*#__PURE__*/React.createElement(OptionItem, {
30916
+ key: content,
30917
+ value: content,
30918
+ label: /*#__PURE__*/React.createElement(Renderer, {
30919
+ content: content,
30920
+ inline: true
30921
+ })
30922
+ }));
30923
+ const selectedValues = props.choices.filter(choice => choice.checked).map(choice => choice.content);
30924
+ const {
30925
+ opener,
30926
+ onToggle,
30927
+ disabled
30928
+ } = props;
30929
+ const args = {
30930
+ // reset to allow child (answer pill) to control z-index
30931
+ style: {
30932
+ zIndex: "unset"
30933
+ },
30934
+ children: AnswerItems(props.choices),
30935
+ opener,
30936
+ onToggle,
30937
+ disabled
30938
+ };
30939
+ return props.multipleSelect ? /*#__PURE__*/React.createElement(MultiSelect, _extends({}, args, {
30940
+ selectedValues: selectedValues,
30941
+ onChange: selected => onAnswerChange(selected)
30942
+ })) : /*#__PURE__*/React.createElement(SingleSelect, _extends({}, args, {
30943
+ selectedValue: selectedValues[0],
30944
+ onChange: selected => onAnswerChange([selected]),
30945
+ placeholder: props.choices[0].content // not visible, but required
30946
+ }));
30947
+ };
30959
30948
 
30960
- // Wrap the index if necessary.
30961
- if (index < 0) {
30962
- index += numChoices;
30963
- } else if (index >= numChoices) {
30964
- index -= numChoices;
30965
- }
30966
- const choice = this._choices[index];
30967
- if (choice) {
30968
- choice.focusInput();
30969
- }
30970
- }
30971
- onAnswerChange(index, checked) {
30972
- const {
30973
- choices,
30974
- multipleSelect,
30975
- onChange
30976
- } = this.props;
30977
- const selection = [];
30949
+ const strings = {
30950
+ hideAnswersToggleLabel: i18n._("Hide answer choices")
30951
+ };
30978
30952
 
30979
- // Compile the selection state of answer choices. In single selection
30980
- // mode (radio input type), only 0 or 1 answer may be chosen.
30981
- for (let i = 0; i < choices.length; i++) {
30982
- selection.push(multipleSelect ? i === index ? checked : choices[i].checked : i === index ? checked : false);
30983
- }
30984
- onChange(selection);
30985
- }
30986
- onFocusAnswer(index) {
30987
- this.focusAnswer(index);
30988
- }
30989
- render() {
30990
- const {
30991
- choices,
30992
- multipleSelect
30993
- } = this.props;
30994
- const {
30995
- groupName
30996
- } = this.state;
30997
- return /*#__PURE__*/React.createElement("fieldset", {
30998
- style: {
30999
- border: "none",
31000
- margin: 0,
31001
- padding: 0
31002
- }
31003
- }, choices.map((choice, index) => /*#__PURE__*/React.createElement(AnswerChoice, _extends({}, choice, {
31004
- key: choice.content,
31005
- groupName: groupName,
31006
- index: index,
31007
- inputType: multipleSelect ? "checkbox" : "radio",
31008
- onChange: checked => this.onAnswerChange(index, checked),
31009
- onFocusPrevAnswer: () => this.onFocusAnswer(index - 1),
31010
- onFocusNextAnswer: () => this.onFocusAnswer(index + 1),
31011
- ref: node => this._choices[index] = node
31012
- }))));
31013
- }
31014
- }
31015
- const styles$j = StyleSheet.create({
31016
- answerChoice: {
30953
+ const HideAnswersToggle = props => {
30954
+ const idFactory = useUniqueIdWithMock();
30955
+ const switchId = idFactory.get("hide-answers-toggle");
30956
+ const labelId = idFactory.get("hide-answers-label");
30957
+ return /*#__PURE__*/React.createElement(View, {
30958
+ style: styles$i.switchWrapper
30959
+ }, /*#__PURE__*/React.createElement(Switch, {
30960
+ id: switchId,
30961
+ checked: props.areAnswersHidden,
30962
+ onChange: props.onChange,
30963
+ "aria-labelledby": labelId
30964
+ }), /*#__PURE__*/React.createElement(LabelMedium, {
30965
+ id: labelId,
30966
+ htmlFor: switchId,
30967
+ tag: "label"
30968
+ }, strings.hideAnswersToggleLabel));
30969
+ };
30970
+ const styles$i = StyleSheet.create({
30971
+ switchWrapper: {
31017
30972
  display: "flex",
30973
+ flexDirection: "row",
30974
+ flexWrap: "wrap-reverse",
31018
30975
  alignItems: "center",
31019
- userSelect: "none",
31020
- padding: "10px 16px 10px 0px",
31021
- fontWeight: "bold",
31022
- border: "solid 2px transparent",
31023
- borderRadius: 2,
31024
- ":active": {
31025
- backgroundColor: "rgba(33, 36, 44, 0.08)"
31026
- }
31027
- },
31028
- answerFocused: {
31029
- borderColor: "#1865f2",
31030
- ":active": {
31031
- borderColor: "transparent"
31032
- }
31033
- },
31034
- answerInputWrapper: {
31035
- marginRight: 8
31036
- },
31037
- checkmarkContainer: {
31038
- display: "flex",
31039
- position: "relative",
31040
- width: 16,
31041
- height: 16,
31042
- marginLeft: 8
30976
+ gap: "0.5em",
30977
+ marginTop: "1em"
31043
30978
  }
31044
30979
  });
31045
30980
 
30981
+ const BringToFront = {
30982
+ boxShadow: `0 8px 8px ${Color.offBlack64}`,
30983
+ zIndex: 1000
30984
+ };
31046
30985
  const AnswerPill = props => {
31047
30986
  const {
31048
30987
  selectedAnswers,
@@ -31050,7 +30989,9 @@ const AnswerPill = props => {
31050
30989
  markerRef,
31051
30990
  side,
31052
30991
  onClick,
31053
- style
30992
+ style,
30993
+ focused,
30994
+ hovered
31054
30995
  } = props;
31055
30996
  const idFactory = useUniqueIdWithMock();
31056
30997
  const answerString = selectedAnswers.length > 1 ?
@@ -31061,10 +31002,6 @@ const AnswerPill = props => {
31061
31002
  // has been labeled (answered) correctly.
31062
31003
  const correct = showCorrectness === "correct";
31063
31004
  const incorrect = showCorrectness === "incorrect";
31064
- const bringToFront = {
31065
- boxShadow: `0 8px 8px ${Color.offBlack64}`,
31066
- zIndex: 1000
31067
- };
31068
31005
  return /*#__PURE__*/React.createElement(Popper, {
31069
31006
  placement: side,
31070
31007
  referenceElement: markerRef,
@@ -31083,62 +31020,27 @@ const AnswerPill = props => {
31083
31020
  id: idFactory.get("perseus-label-image-widget-answer-pill"),
31084
31021
  onClick: correct ? undefined : onClick,
31085
31022
  ref: ref,
31086
- style: [style, popperStyle, {
31087
- ":hover": bringToFront,
31088
- ":focus": bringToFront
31089
- }, correct && styles$i.correct, incorrect && styles$i.incorrect]
31023
+ style: [style, popperStyle, styles$h.relative, correct && styles$h.correct, incorrect && styles$h.incorrect, (focused || hovered) && BringToFront]
31090
31024
  }, /*#__PURE__*/React.createElement(Renderer, {
31091
31025
  content: answerString,
31092
31026
  inline: true
31093
31027
  })));
31094
31028
  };
31095
- const styles$i = StyleSheet.create({
31029
+ const styles$h = StyleSheet.create({
31096
31030
  correct: {
31097
- backgroundColor: "#00880b" // WB green darkened by 18%
31031
+ // WB green darkened by 18%
31032
+ backgroundColor: "#00880b"
31098
31033
  },
31099
-
31100
31034
  incorrect: {
31101
31035
  backgroundColor: Color.offBlack64
31102
31036
  }
31103
31037
  });
31104
31038
 
31105
- const strings = {
31106
- hideAnswersToggleLabel: i18n._("Hide answer choices")
31107
- };
31108
-
31109
- const HideAnswersToggle = props => {
31110
- const idFactory = useUniqueIdWithMock();
31111
- const switchId = idFactory.get("hide-answers-toggle");
31112
- const labelId = idFactory.get("hide-answers-label");
31113
- return /*#__PURE__*/React.createElement(View, {
31114
- style: styles$h.switchWrapper
31115
- }, /*#__PURE__*/React.createElement(Switch, {
31116
- id: switchId,
31117
- checked: props.areAnswersHidden,
31118
- onChange: props.onChange,
31119
- "aria-labelledby": labelId
31120
- }), /*#__PURE__*/React.createElement(LabelMedium, {
31121
- id: labelId,
31122
- htmlFor: switchId,
31123
- tag: "label"
31124
- }, strings.hideAnswersToggleLabel));
31125
- };
31126
- const styles$h = StyleSheet.create({
31127
- switchWrapper: {
31128
- display: "flex",
31129
- flexDirection: "row",
31130
- flexWrap: "wrap-reverse",
31131
- alignItems: "center",
31132
- gap: "0.5em",
31133
- marginTop: "1em"
31134
- }
31135
- });
31136
-
31137
- /* eslint-disable @khanacademy/ts-no-error-suppressions */
31138
31039
  function shouldReduceMotion() {
31139
31040
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
31140
31041
  return !mediaQuery || mediaQuery.matches;
31141
31042
  }
31043
+ const MARKER_SIZE = 24;
31142
31044
  class Marker extends React.Component {
31143
31045
  constructor(...args) {
31144
31046
  super(...args);
@@ -31150,126 +31052,103 @@ class Marker extends React.Component {
31150
31052
  selected,
31151
31053
  showCorrectness,
31152
31054
  showSelected,
31153
- showPulsate,
31154
- focused
31055
+ showPulsate
31155
31056
  } = this.props;
31156
31057
 
31157
31058
  // Only a single marker may be "selected" at a time.
31158
31059
  // `showSelected` is a controlled prop, that may be set to `true` for
31159
31060
  // one marker at a time.
31160
- // `focused` is a controlled state, driven by focus events, and may
31161
- // only be `true` when there's no answer choices popup visible, and
31162
- // keyboard focus is given to the marker.
31163
- const isSelected = showSelected || focused;
31164
- let innerIcon;
31061
+ const isOpen = showSelected;
31062
+ const selectedAnswers = selected;
31165
31063
  let iconStyles;
31064
+ const iconNull = {
31065
+ path: "",
31066
+ height: 1,
31067
+ width: 1
31068
+ };
31069
+
31070
+ // default dot
31071
+ let args = {
31072
+ size: MARKER_SIZE,
31073
+ color: Color.white,
31074
+ icon: iconNull
31075
+ };
31166
31076
  if (showCorrectness) {
31167
- innerIcon = /*#__PURE__*/React.createElement(Icon, {
31168
- icon: showCorrectness === "correct" ? iconCheck : iconMinus,
31169
- size: 24,
31170
- color: Color.white
31077
+ iconStyles = [styles$g.markerGraded, showCorrectness === "correct" ? styles$g.markerCorrect : styles$g.markerIncorrect, isOpen && styles$g.markerSelected];
31078
+ args = _extends({}, args, {
31079
+ icon: showCorrectness === "correct" ? iconCheck : iconMinus
31171
31080
  });
31172
- if (showCorrectness === "correct") {
31173
- iconStyles = [styles$g.markerGraded, styles$g.markerCorrect, isSelected && styles$g.markerSelected];
31174
- } else {
31175
- iconStyles = [styles$g.markerGraded, styles$g.markerIncorrect, isSelected && styles$g.markerSelected];
31176
- }
31177
- } else if (focused) {
31178
- iconStyles = [styles$g.markerFocused, selected && selected.length > 0 && styles$g.markerFilled, showSelected && styles$g.markerSelected];
31179
- } else if (selected && selected.length > 0) {
31180
- iconStyles = [styles$g.markerFilled, isSelected && styles$g.markerSelected];
31181
- } else if (isSelected) {
31081
+ } else if (selectedAnswers && selectedAnswers.length > 0) {
31082
+ iconStyles = [styles$g.markerFilled, isOpen && styles$g.markerSelected];
31083
+ } else if (isOpen) {
31182
31084
  iconStyles = [styles$g.markerSelected];
31183
- } else {
31184
- iconStyles = [styles$g.markerPulsateBase, shouldReduceMotion() ? showPulsate && styles$g.markerUnfilledPulsateOnce : showPulsate && styles$g.markerUnfilledPulsateInfinite];
31185
- }
31186
- if (isSelected && selected && selected.length === 0) {
31187
- innerIcon = /*#__PURE__*/React.createElement(Icon, {
31085
+ args = _extends({}, args, {
31188
31086
  icon: iconChevronDown,
31189
- size: 8,
31190
- color: Color.white
31087
+ size: 8
31191
31088
  });
31089
+ } else if (showPulsate) {
31090
+ iconStyles = [styles$g.markerPulsateBase, shouldReduceMotion() ? showPulsate && styles$g.markerUnfilledPulsateOnce : showPulsate && styles$g.markerUnfilledPulsateInfinite];
31192
31091
  }
31193
31092
  return /*#__PURE__*/React.createElement(View, {
31194
31093
  style: [styles$g.markerIcon, iconStyles],
31195
31094
  ref: node => this._icon = node
31196
- }, innerIcon);
31095
+ }, /*#__PURE__*/React.createElement(Icon, args));
31197
31096
  }
31198
31097
  render() {
31098
+ var _this$_icon;
31199
31099
  const {
31200
- label,
31201
- x,
31202
- y,
31203
- showCorrectness
31100
+ showCorrectness,
31101
+ selected,
31102
+ showAnswer,
31103
+ answerSide,
31104
+ answerStyles,
31105
+ hovered,
31106
+ focused
31204
31107
  } = this.props;
31205
31108
 
31206
- // It should no longer be possible to interact with a marker after it
31207
- // has been labeled (answered) correctly.
31208
- const isDisabled = showCorrectness === "correct";
31209
- return /*#__PURE__*/React.createElement("button", {
31210
- "aria-label": label,
31211
- className: css(styles$g.unstyledButton, styles$g.marker, isDisabled && styles$g.disabled),
31212
- style: {
31213
- left: `${x}%`,
31214
- top: `${y}%`
31215
- },
31216
- tabIndex: isDisabled ? -1 : 0,
31217
- onFocus: this.props.onFocus,
31218
- onBlur: this.props.onBlur,
31219
- onClick: e => {
31220
- e.preventDefault();
31221
- this.props.onClick(e);
31222
- },
31223
- onKeyDown: e => this.props.onKeyDown(e)
31224
- }, this.renderIcon());
31109
+ // We cannot make dropdown openers untabbable, so we isolate tabbing
31110
+ // to answer pills when interaction is disabled.
31111
+ const markerDisabled = showCorrectness === "correct";
31112
+ const active = hovered || focused;
31113
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(View, {
31114
+ style: [styles$g.marker, active && !markerDisabled && styles$g.markerActive]
31115
+ }, this.renderIcon()), !!selected && showAnswer && /*#__PURE__*/React.createElement(AnswerPill, {
31116
+ selectedAnswers: selected,
31117
+ showCorrectness: showCorrectness,
31118
+ side: answerSide,
31119
+ style: answerStyles,
31120
+ markerRef: (_this$_icon = this._icon) != null ? _this$_icon : undefined,
31121
+ hovered: hovered,
31122
+ focused: focused
31123
+ }));
31225
31124
  }
31226
31125
  }
31227
31126
  Marker.defaultProps = {
31228
31127
  selected: []
31229
31128
  };
31230
- const lightShadowColor = "rgba(33, 36, 44, 0.16)";
31231
- const markerSize = 20;
31232
31129
  const styles$g = StyleSheet.create({
31233
- unstyledButton: {
31234
- padding: 0,
31235
- overflow: "visible",
31236
- color: "inherit",
31237
- font: "inherit",
31238
- lineHeight: "normal",
31239
- background: "none",
31240
- border: "none"
31241
- },
31242
31130
  marker: {
31243
31131
  position: "absolute",
31244
- outline: "none",
31245
- // Center marker position based on it's maximum size.
31246
- width: markerSize,
31247
- height: markerSize,
31248
31132
  backgroundColor: Color.white,
31249
- borderRadius: 30,
31250
- marginLeft: -15,
31251
- marginTop: -15
31252
- },
31253
- disabled: {
31254
- pointerEvents: "none"
31133
+ borderRadius: MARKER_SIZE,
31134
+ // Center marker position based on its maximum size.
31135
+ width: MARKER_SIZE,
31136
+ height: MARKER_SIZE,
31137
+ marginLeft: MARKER_SIZE / -2,
31138
+ marginTop: MARKER_SIZE / -2,
31139
+ // Add a shadow to the marker to make it stand out from the image.
31140
+ boxShadow: `0 8px 8px ${Color.offBlack8}`
31255
31141
  },
31256
31142
  // The base and unfilled marker style.
31257
31143
  markerIcon: {
31258
31144
  display: "flex",
31259
31145
  alignItems: "center",
31260
31146
  justifyContent: "center",
31261
- boxSizing: "content-box",
31262
- width: markerSize,
31263
- height: markerSize,
31264
- marginLeft: 2,
31265
- cursor: "pointer",
31147
+ boxSizing: "border-box",
31148
+ width: MARKER_SIZE,
31149
+ height: MARKER_SIZE,
31266
31150
  border: `2px solid ${Color.offBlack64}`,
31267
- borderRadius: markerSize,
31268
- boxShadow: `0 8px 8px ${Color.offBlack8}`,
31269
- ":hover": {
31270
- outline: `2px solid ${Color.blue}`,
31271
- outlineOffset: 2
31272
- }
31151
+ borderRadius: MARKER_SIZE
31273
31152
  },
31274
31153
  // The animation that presents the marker to the learner
31275
31154
  markerPulsateBase: {
@@ -31290,16 +31169,13 @@ const styles$g = StyleSheet.create({
31290
31169
  animationIterationCount: "0"
31291
31170
  },
31292
31171
  markerUnfilledPulsateInfinite: {
31293
- animationIterationCount: "infinite",
31294
- ":hover": {
31295
- outline: `0px solid ${Color.blue}`
31296
- }
31172
+ animationIterationCount: "infinite"
31297
31173
  },
31298
31174
  markerUnfilledPulsateOnce: {
31299
31175
  // Doing the animation twice lets it ease-in and ease-out
31300
31176
  animationIterationCount: "2"
31301
31177
  },
31302
- markerFocused: {
31178
+ markerActive: {
31303
31179
  outline: `2px solid ${Color.blue}`,
31304
31180
  outlineOffset: 2
31305
31181
  },
@@ -31308,33 +31184,20 @@ const styles$g = StyleSheet.create({
31308
31184
  boxShadow: `0 8px 8px ${Color.offBlack8}`,
31309
31185
  border: `solid 4px ${Color.white}`,
31310
31186
  backgroundColor: Color.blue,
31311
- borderRadius: markerSize,
31312
- transform: "rotate(180deg)",
31313
- ":hover": {
31314
- outline: `2px solid ${Color.blue}`,
31315
- outlineOffset: -2
31316
- }
31187
+ borderRadius: MARKER_SIZE,
31188
+ transform: "rotate(180deg)"
31317
31189
  },
31318
31190
  // The learner has made a selection
31319
31191
  markerFilled: {
31320
- width: markerSize - 2,
31321
- height: markerSize - 2,
31322
- borderRadius: markerSize - 2,
31323
31192
  backgroundColor: "#ECF3FE",
31324
- marginLeft: 3,
31325
- marginTop: 0,
31326
- border: `4px solid ${Color.blue}`,
31327
- boxShadow: `0 1px 1px 0 ${lightShadowColor}`
31193
+ border: `4px solid ${Color.blue}`
31328
31194
  },
31329
31195
  markerGraded: {
31330
- width: markerSize,
31331
- height: markerSize,
31332
- marginLeft: 1,
31333
- marginTop: 1,
31196
+ width: MARKER_SIZE,
31197
+ height: MARKER_SIZE,
31334
31198
  justifyContent: "center",
31335
31199
  alignItems: "center",
31336
- border: `2px solid ${Color.white}`,
31337
- boxShadow: `0 8px 8px ${Color.offBlack8}`
31200
+ border: `2px solid ${Color.white}`
31338
31201
  },
31339
31202
  markerCorrect: {
31340
31203
  background: "#00880b" // WB green darkened by 18%
@@ -31583,14 +31446,9 @@ class LabelImage extends React.Component {
31583
31446
  super(props);
31584
31447
  // The rendered markers on the question image for labeling.
31585
31448
  this._markers = void 0;
31586
- // The popup component containing the answer choices.
31587
- this._selectedMarkerPopup = void 0;
31588
- // The rendered list of answer choices for the currently selected marker.
31589
- this._answerChoices = void 0;
31590
31449
  this._markers = [];
31591
31450
  this.state = {
31592
31451
  activeMarkerIndex: -1,
31593
- focusedMarkerIndex: -1,
31594
31452
  markersInteracted: false,
31595
31453
  hideAnswers: false
31596
31454
  };
@@ -31628,26 +31486,6 @@ class LabelImage extends React.Component {
31628
31486
  markers: updatedMarkers
31629
31487
  }, null, true);
31630
31488
  }
31631
- dismissMarkerPopup() {
31632
- const {
31633
- activeMarkerIndex
31634
- } = this.state;
31635
-
31636
- // No popup should be open if there's no selected marker.
31637
- if (activeMarkerIndex === -1) {
31638
- return;
31639
- }
31640
- this.setState({
31641
- activeMarkerIndex: -1
31642
- }, () => {
31643
- const marker = this._markers[activeMarkerIndex];
31644
- // Set focus on the just-deselected-marker, to enable to resume
31645
- // navigating between the markers using the keyboard.
31646
- if (marker) {
31647
- ReactDOM.findDOMNode(marker).focus();
31648
- }
31649
- });
31650
- }
31651
31489
  handleMarkerChange(index, marker) {
31652
31490
  const {
31653
31491
  markers,
@@ -31666,32 +31504,25 @@ class LabelImage extends React.Component {
31666
31504
  markers: updatedMarkers
31667
31505
  });
31668
31506
  }
31669
- activateMarker(index) {
31507
+ activateMarker(index, opened) {
31508
+ var _this$props$analytics;
31509
+ (_this$props$analytics = this.props.analytics) == null || _this$props$analytics.onAnalyticsEvent({
31510
+ type: "perseus:label-image:marker-interacted-with",
31511
+ payload: null
31512
+ });
31670
31513
  const {
31671
31514
  activeMarkerIndex
31672
31515
  } = this.state;
31673
-
31674
- // Select the marker, revealing answer choices.
31675
- if (activeMarkerIndex !== index) {
31516
+ // Set index of opened marker
31517
+ if (activeMarkerIndex !== index && opened) {
31676
31518
  this.setState({
31677
31519
  activeMarkerIndex: index,
31678
31520
  markersInteracted: true
31679
- }, () => {
31680
- // A delay is required to allow answer choices to first render
31681
- // before trying to set focus on the first choice.
31682
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
31683
- // eslint-disable-next-line no-restricted-syntax
31684
- setTimeout(() => {
31685
- if (this._answerChoices) {
31686
- this._answerChoices.focusAnswer();
31687
- }
31688
- }, 100);
31689
31521
  });
31690
31522
  } else {
31691
- // The answer choices are shown within a popup attached to the
31692
- // selected marker. Close the popup and set focus back to the
31693
- // marker.
31694
- this.dismissMarkerPopup();
31523
+ this.setState({
31524
+ activeMarkerIndex: -1
31525
+ });
31695
31526
  }
31696
31527
  }
31697
31528
  handleMarkerKeyDown(index, e) {
@@ -31739,8 +31570,7 @@ class LabelImage extends React.Component {
31739
31570
  handleAnswerChoicesChangeForMarker(index, selection) {
31740
31571
  const {
31741
31572
  choices,
31742
- markers,
31743
- multipleAnswers
31573
+ markers
31744
31574
  } = this.props;
31745
31575
 
31746
31576
  // Compile the user selected answer choices.
@@ -31748,36 +31578,6 @@ class LabelImage extends React.Component {
31748
31578
  this.handleMarkerChange(index, _extends({}, markers[index], {
31749
31579
  selected: selected.length ? selected : undefined
31750
31580
  }));
31751
- if (!multipleAnswers) {
31752
- this.dismissMarkerPopup();
31753
- }
31754
- }
31755
- renderAnswerChoicesForMarker(index, marker) {
31756
- const {
31757
- choices,
31758
- multipleAnswers
31759
- } = this.props;
31760
-
31761
- // The user selected answer choices.
31762
- const selected = marker.selected;
31763
- return /*#__PURE__*/React.createElement("div", {
31764
- className: classNames("perseus-label-image-widget-answer-choices", css(styles$f.scrollableChoices))
31765
- }, /*#__PURE__*/React.createElement(AnswerChoices, {
31766
- choices: choices.map(choice => ({
31767
- content: choice,
31768
- checked: selected ? selected.includes(choice) : false
31769
- })),
31770
- multipleSelect: multipleAnswers,
31771
- onChange: selection => {
31772
- var _this$props$analytics;
31773
- (_this$props$analytics = this.props.analytics) == null || _this$props$analytics.onAnalyticsEvent({
31774
- type: "perseus:label-image:choiced-interacted-with",
31775
- payload: null
31776
- });
31777
- this.handleAnswerChoicesChangeForMarker(index, selection);
31778
- },
31779
- ref: node => this._answerChoices = node
31780
- }));
31781
31581
  }
31782
31582
  renderMarkers() {
31783
31583
  const {
@@ -31792,41 +31592,6 @@ class LabelImage extends React.Component {
31792
31592
 
31793
31593
  // Render all markers for widget.
31794
31594
  return markers.map((marker, index) => {
31795
- const score = LabelImage.gradeMarker(marker);
31796
- // Once the question is answered, show markers
31797
- // with correct answers, otherwise passthrough
31798
- // the correctness state.
31799
- const showCorrectness = questionCompleted && score.hasAnswers && score.isCorrect ? "correct" : marker.showCorrectness;
31800
- const element = /*#__PURE__*/React.createElement(Marker, _extends({}, marker, {
31801
- showCorrectness: showCorrectness,
31802
- showSelected: index === activeMarkerIndex,
31803
- showPulsate: !markersInteracted,
31804
- key: `${marker.x}.${marker.y}`,
31805
- onClick: () => {
31806
- var _this$props$analytics2;
31807
- (_this$props$analytics2 = this.props.analytics) == null || _this$props$analytics2.onAnalyticsEvent({
31808
- type: "perseus:label-image:marker-interacted-with",
31809
- payload: null
31810
- });
31811
- this.activateMarker(index);
31812
- },
31813
- onKeyDown: e => this.handleMarkerKeyDown(index, e),
31814
- ref: node => this._markers[index] = node,
31815
- focused: index === this.state.focusedMarkerIndex,
31816
- onFocus: () => {
31817
- this.setState({
31818
- focusedMarkerIndex: index
31819
- });
31820
- },
31821
- onBlur: () => {
31822
- if (index === this.state.focusedMarkerIndex) {
31823
- this.setState({
31824
- focusedMarkerIndex: -1
31825
- });
31826
- }
31827
- }
31828
- }));
31829
-
31830
31595
  // Determine whether page is rendered in a narrow browser window.
31831
31596
  const isNarrowPage = window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")).matches;
31832
31597
 
@@ -31854,40 +31619,72 @@ class LabelImage extends React.Component {
31854
31619
  bottom: "top"
31855
31620
  }[markerPosition];
31856
31621
  }
31857
- const answerChoicesActive = index === activeMarkerIndex;
31858
- const adjustPopoverDistance = {
31859
- [side]: 15 // move the popover closer to the marker
31860
- };
31622
+ const score = LabelImage.gradeMarker(marker);
31623
+ // Once the question is answered, show markers
31624
+ // with correct answers, otherwise passthrough
31625
+ // the correctness state.
31626
+ const showCorrectness = questionCompleted && score.hasAnswers && score.isCorrect ? "correct" : marker.showCorrectness;
31861
31627
 
31628
+ // Disable marker interaction once the question is answered correctly.
31629
+ const disabled = showCorrectness === "correct";
31862
31630
  const adjustPillDistance = {
31863
- [`margin${markerPosition.charAt(0).toUpperCase() + markerPosition.slice(1)}`]: 15 // move pill further from marker
31631
+ [`margin${markerPosition.charAt(0).toUpperCase() + markerPosition.slice(1)}`]: 10 // move pill further from marker
31864
31632
  };
31865
31633
 
31866
- const showAnswerChoice = !answerChoicesActive && !this.state.hideAnswers;
31867
- return /*#__PURE__*/React.createElement("div", {
31868
- key: `answers-${marker.x}.${marker.y}`
31869
- }, /*#__PURE__*/React.createElement(Popover, {
31870
- content: () => /*#__PURE__*/React.createElement(PopoverContentCore, {
31871
- style: [adjustPopoverDistance, styles$f.choicesPopover]
31872
- }, this.renderAnswerChoicesForMarker(index, marker)),
31873
- placement: side,
31874
- onClose: () => this.dismissMarkerPopup(),
31875
- opened: answerChoicesActive,
31876
- ref: node => this._selectedMarkerPopup = node,
31877
- showTail: false,
31878
- dismissEnabled: true
31879
- }, element), !!marker.selected && showAnswerChoice && /*#__PURE__*/React.createElement(AnswerPill, {
31880
- selectedAnswers: marker.selected,
31881
- showCorrectness: showCorrectness,
31882
- markerRef:
31883
- // this throws a warning because it's inadvisable to
31884
- // call ReactDOM fns in render. we aren't storing
31885
- // actual refs in this array, so not much I can do
31886
- // outside of a refactor.
31887
- ReactDOM.findDOMNode(this._markers[index]),
31888
- side: side,
31889
- onClick: () => this.activateMarker(index),
31890
- style: adjustPillDistance
31634
+ const answerChoicesActive = index === activeMarkerIndex;
31635
+ const showAnswerChoice = marker.selected && !answerChoicesActive && !this.state.hideAnswers;
31636
+ return /*#__PURE__*/React.createElement(View, {
31637
+ style: {
31638
+ position: "absolute",
31639
+ left: `${marker.x}%`,
31640
+ top: `${marker.y}%`,
31641
+ // reset to allow child (answer pill) to control z-index
31642
+ zIndex: "unset"
31643
+ }
31644
+ }, /*#__PURE__*/React.createElement(AnswerChoices, {
31645
+ key: `answers-${marker.x}.${marker.y}`,
31646
+ choices: this.props.choices.map(choice => ({
31647
+ content: choice,
31648
+ checked: marker.selected ? marker.selected.includes(choice) : false
31649
+ })),
31650
+ multipleSelect: this.props.multipleAnswers,
31651
+ onChange: selection => {
31652
+ var _this$props$analytics2;
31653
+ (_this$props$analytics2 = this.props.analytics) == null || _this$props$analytics2.onAnalyticsEvent({
31654
+ type: "perseus:label-image:choiced-interacted-with",
31655
+ payload: null
31656
+ });
31657
+ this.handleAnswerChoicesChangeForMarker(index, selection);
31658
+ },
31659
+ onToggle: opened => this.activateMarker(index, opened)
31660
+ // cannot change answer choices once question is answered
31661
+ ,
31662
+ disabled: disabled,
31663
+ opener: ({
31664
+ opened,
31665
+ text
31666
+ }) => /*#__PURE__*/React.createElement(Clickable, {
31667
+ role: "button",
31668
+ key: `marker-${marker.x}.${marker.y}`,
31669
+ "aria-label":
31670
+ // already translated in: /widgets/graded-group.tsx
31671
+ showCorrectness === "correct" ? i18n._("Correct!") : marker.label
31672
+ }, ({
31673
+ hovered,
31674
+ focused,
31675
+ pressed
31676
+ }) => /*#__PURE__*/React.createElement(Marker, _extends({}, marker, {
31677
+ showCorrectness: showCorrectness,
31678
+ showSelected: opened,
31679
+ showPulsate: !markersInteracted,
31680
+ ref: node => this._markers[index] = node,
31681
+ showAnswer: showAnswerChoice,
31682
+ answerSide: side,
31683
+ answerStyles: adjustPillDistance,
31684
+ analytics: this.props.analytics,
31685
+ focused: focused || pressed,
31686
+ hovered: hovered
31687
+ })))
31891
31688
  }));
31892
31689
  });
31893
31690
  }
@@ -32003,18 +31800,6 @@ const styles$f = StyleSheet.create({
32003
31800
  },
32004
31801
  imageInteractionDisabled: {
32005
31802
  pointerEvents: "none"
32006
- },
32007
- scrollableChoices: {
32008
- paddingTop: 8,
32009
- paddingBottom: 8,
32010
- // Enable scroll for answer choices within popup on mobile phones.
32011
- [mediaQueries.xsOrSmaller]: {
32012
- maxHeight: 250,
32013
- overflowY: "auto"
32014
- }
32015
- },
32016
- choicesPopover: {
32017
- padding: 0
32018
31803
  }
32019
31804
  });
32020
31805
  const LabelImageWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) => {
@@ -44164,7 +43949,7 @@ var Video$1 = {
44164
43949
  };
44165
43950
 
44166
43951
  /* eslint-disable import/no-named-as-default */
44167
- var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, LightsPuzzle$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, ReactionDiagram, Sequence$1, Simulator$1, Sorter$1, Table$1, Transformer$1, Unit, Video$1];
43952
+ var extraWidgets = [Categorizer$1, CSProgram$1, Dropdown$1, Explanation$1, Definition$1, Grapher$1, GradedGroup$1, GradedGroupSet$1, Group$1, Iframe$1, Image$1, Interactive, InteractiveGraph$1, LabelImage$1, LightsPuzzle$1, Matrix$1, Matcher$1, Measurer$1, Molecule$1, NumberLine$1, Orderer$1, Passage$1, PassageRef$1, PassageRefTarget$1, Plotter$1, ReactionDiagram, Sequence$1, Simulator$1, Sorter$1, Table$1, Transformer$1, Unit, Video$1, DeprecatedStandin];
44168
43953
 
44169
43954
  /**
44170
43955
  * This should be called by all clients, specifying whether extra widgets are
@@ -44179,6 +43964,11 @@ const init = function init(options) {
44179
43964
  registerWidgets(basicWidgets);
44180
43965
  registerWidgets(extraWidgets);
44181
43966
 
43967
+ // This section replaces widgets
43968
+ // When it's time to replace the transformer widget
43969
+ // uncomment this line
43970
+ // Widgets.replaceWidget("transformer", "always-correct");
43971
+
44182
43972
  // Pass skipMathJax: true if MathJax is already loaded and configured.
44183
43973
  const skipMathJax = options.skipMathJax;
44184
43974
  if (skipMathJax) {
@@ -44203,7 +43993,7 @@ const init = function init(options) {
44203
43993
 
44204
43994
  // This file is processed by a Rollup plugin (replace) to inject the production
44205
43995
  const libName = "@khanacademy/perseus";
44206
- const libVersion = "17.6.0";
43996
+ const libVersion = "17.6.2";
44207
43997
  addLibraryVersionToPerseusDebug(libName, libVersion);
44208
43998
 
44209
43999
  const apiVersion = {
@@ -45651,6 +45441,8 @@ const ItemExtras = [
45651
45441
 
45652
45442
  // prettier-ignore
45653
45443
 
45444
+ //prettier-ignore
45445
+
45654
45446
  // A background image applied to various widgets.
45655
45447
 
45656
45448
  const PerseusExpressionAnswerFormConsidered = ["correct", "wrong", "ungraded"];
@@ -46781,6 +46573,10 @@ function violatingWidgets(itemData) {
46781
46573
  */
46782
46574
  const registerAllWidgetsForTesting = () => {
46783
46575
  registerWidgets(allWidgets);
46576
+
46577
+ // When it's time to replace the transformer widget
46578
+ // uncomment this line
46579
+ // Widgets.replaceWidget("transformer", "deprecated-standin");
46784
46580
  };
46785
46581
 
46786
46582
  const EditorJsonify = {