@khanacademy/perseus 17.0.0 → 17.1.0

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 +59 -0
  2. package/dist/dependencies.d.ts +1 -0
  3. package/dist/es/index.css +20 -0
  4. package/dist/es/index.js +443 -338
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +20 -0
  7. package/dist/index.js +444 -340
  8. package/dist/index.js.map +1 -1
  9. package/dist/perseus-types.d.ts +1 -0
  10. package/dist/strings.js +1 -0
  11. package/dist/widgets/label-image/answer-pill.d.ts +10 -0
  12. package/dist/widgets/label-image/hide-answers-toggle.d.ts +6 -0
  13. package/dist/widgets/label-image/marker.d.ts +6 -9
  14. package/dist/widgets/label-image/types.d.ts +1 -0
  15. package/dist/widgets/label-image.d.ts +101 -12
  16. package/package.json +30 -22
  17. package/src/__tests__/__snapshots__/renderer.test.tsx.snap +3 -3
  18. package/src/dependencies.ts +4 -0
  19. package/src/perseus-types.ts +2 -0
  20. package/src/styles/widgets/label-image.less +32 -0
  21. package/src/widgets/__stories__/label-image.stories.tsx +73 -3
  22. package/src/widgets/__testdata__/label-image.testdata.ts +118 -1
  23. package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +2 -2
  24. package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +62 -0
  25. package/src/widgets/label-image/__tests__/answer-pill.test.tsx +83 -0
  26. package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +39 -0
  27. package/src/widgets/label-image/answer-choices.tsx +7 -1
  28. package/src/widgets/label-image/answer-pill.tsx +89 -0
  29. package/src/widgets/label-image/hide-answers-toggle.tsx +40 -0
  30. package/src/widgets/label-image/marker.tsx +116 -174
  31. package/src/widgets/label-image/types.ts +1 -0
  32. package/src/widgets/label-image.test.ts +142 -52
  33. package/src/widgets/label-image.tsx +212 -135
  34. package/tsconfig-build.tsbuildinfo +1 -1
package/dist/es/index.js CHANGED
@@ -2,7 +2,7 @@ import * as KAS from '@khanacademy/kas';
2
2
  import { keyTranslator, CursorContext, createMathField, mathQuillInstance, getCursorContext, DesktopKeypad, KeypadInput, KeypadType, KeyArray, keypadElementPropType } from '@khanacademy/math-input';
3
3
  import * as PerseusLinter from '@khanacademy/perseus-linter';
4
4
  import { linterContextDefault, Rule, linterContextProps } from '@khanacademy/perseus-linter';
5
- import { View, RenderStateRoot, Text } from '@khanacademy/wonder-blocks-core';
5
+ import { View, RenderStateRoot, useUniqueIdWithMock, Text } from '@khanacademy/wonder-blocks-core';
6
6
  import * as i18n from '@khanacademy/wonder-blocks-i18n';
7
7
  import Tooltip$1 from '@khanacademy/wonder-blocks-tooltip';
8
8
  import classNames from 'classnames';
@@ -27,6 +27,10 @@ import Button from '@khanacademy/wonder-blocks-button';
27
27
  import { Strut } from '@khanacademy/wonder-blocks-layout';
28
28
  import { OptionItem, SingleSelect } from '@khanacademy/wonder-blocks-dropdown';
29
29
  import debounce$1 from 'lodash.debounce';
30
+ import Pill from '@khanacademy/wonder-blocks-pill';
31
+ import { Popper } from 'react-popper';
32
+ import Switch from '@khanacademy/wonder-blocks-switch';
33
+ import { LabelMedium } from '@khanacademy/wonder-blocks-typography';
30
34
  import createReactClass from 'create-react-class';
31
35
  import Link from '@khanacademy/wonder-blocks-link';
32
36
  import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
@@ -238,7 +242,7 @@ class MathInput extends React.Component {
238
242
  className = className + " " + this.props.className;
239
243
  }
240
244
  return /*#__PURE__*/React.createElement(View, {
241
- style: [styles$A.outerWrapper, this.state.focused && styles$A.wrapperFocused, this.props.hasError && styles$A.wrapperError]
245
+ style: [styles$C.outerWrapper, this.state.focused && styles$C.wrapperFocused, this.props.hasError && styles$C.wrapperError]
242
246
  }, /*#__PURE__*/React.createElement("div", {
243
247
  style: {
244
248
  display: "flex",
@@ -267,7 +271,7 @@ class MathInput extends React.Component {
267
271
  var _this$props$keypadBut, _this$props;
268
272
  return /*#__PURE__*/React.createElement(PopoverContentCore, {
269
273
  closeButtonVisible: true,
270
- style: styles$A.popoverContent
274
+ style: styles$C.popoverContent
271
275
  }, /*#__PURE__*/React.createElement(DesktopKeypad, _extends({
272
276
  onAnalyticsEvent: this.props.analytics.onAnalyticsEvent,
273
277
  extraKeys: this.props.extraKeys,
@@ -311,9 +315,9 @@ const MathInputIcon = ({
311
315
  color = Color.offBlack;
312
316
  break;
313
317
  }
314
- const dynamicClass = active || focused ? styles$A.iconActive : styles$A.iconInactive;
318
+ const dynamicClass = active || focused ? styles$C.iconActive : styles$C.iconInactive;
315
319
  return /*#__PURE__*/React.createElement(View, {
316
- style: [styles$A.iconContainer, dynamicClass]
320
+ style: [styles$C.iconContainer, dynamicClass]
317
321
  }, /*#__PURE__*/React.createElement("svg", {
318
322
  xmlns: "http://www.w3.org/2000/svg",
319
323
  width: "16",
@@ -358,7 +362,7 @@ const inputFocused = {
358
362
  borderColor: Color.blue,
359
363
  margin: -1
360
364
  };
361
- const styles$A = StyleSheet.create({
365
+ const styles$C = StyleSheet.create({
362
366
  iconContainer: {
363
367
  display: "flex",
364
368
  justifyContent: "center",
@@ -17553,33 +17557,33 @@ class Lint extends React.Component {
17553
17557
  let warningText;
17554
17558
  let warningTextStyle;
17555
17559
  if (this.props.severity === Severity.Error) {
17556
- severityStyle = styles$z.indicatorError;
17560
+ severityStyle = styles$B.indicatorError;
17557
17561
  warningText = "Error";
17558
- warningTextStyle = styles$z.publishBlockingError;
17562
+ warningTextStyle = styles$B.publishBlockingError;
17559
17563
  } else if (this.props.severity === Severity.Warning) {
17560
- severityStyle = styles$z.indicatorWarning;
17564
+ severityStyle = styles$B.indicatorWarning;
17561
17565
  warningText = "Warning";
17562
- warningTextStyle = styles$z.warning;
17566
+ warningTextStyle = styles$B.warning;
17563
17567
  } else {
17564
- severityStyle = styles$z.indicatorGuideline;
17568
+ severityStyle = styles$B.indicatorGuideline;
17565
17569
  warningText = "Recommendation";
17566
- warningTextStyle = styles$z.warning;
17570
+ warningTextStyle = styles$B.warning;
17567
17571
  }
17568
17572
  return /*#__PURE__*/React.createElement("a", {
17569
17573
  href: `https://khanacademy.org/r/linter-rules#${this.props.ruleName}`,
17570
17574
  target: "lint-help-window",
17571
17575
  className: css(style)
17572
17576
  }, /*#__PURE__*/React.createElement("span", {
17573
- className: css(styles$z.indicator, severityStyle)
17577
+ className: css(styles$B.indicator, severityStyle)
17574
17578
  }, this.props.severity === 1 && /*#__PURE__*/React.createElement(InlineIcon, exclamationIcon)), /*#__PURE__*/React.createElement("div", {
17575
- className: css(styles$z.tooltip, tooltipAbove && styles$z.tooltipAbove)
17579
+ className: css(styles$B.tooltip, tooltipAbove && styles$B.tooltipAbove)
17576
17580
  }, this.props.message.split("\n\n").map((m, i) => /*#__PURE__*/React.createElement("p", {
17577
17581
  key: i,
17578
- className: css(styles$z.tooltipParagraph)
17582
+ className: css(styles$B.tooltipParagraph)
17579
17583
  }, /*#__PURE__*/React.createElement("span", {
17580
17584
  className: css(warningTextStyle)
17581
17585
  }, warningText, ":", " "), m)), /*#__PURE__*/React.createElement("div", {
17582
- className: css(styles$z.tail, tooltipAbove && styles$z.tailAbove)
17586
+ className: css(styles$B.tail, tooltipAbove && styles$B.tailAbove)
17583
17587
  })));
17584
17588
  };
17585
17589
  }
@@ -17617,20 +17621,20 @@ class Lint extends React.Component {
17617
17621
  }
17618
17622
  if (blockHighlight) {
17619
17623
  return /*#__PURE__*/React.createElement("span", {
17620
- className: css(styles$z.lintContainer, styles$z.lintContainerBlock)
17621
- }, this.renderLink(styles$z.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17624
+ className: css(styles$B.lintContainer, styles$B.lintContainerBlock)
17625
+ }, this.renderLink(styles$B.radioWidgetHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17622
17626
  }
17623
17627
  if (inline) {
17624
17628
  return /*#__PURE__*/React.createElement("span", {
17625
- className: css(styles$z.lintContainer)
17626
- }, this.renderLink(styles$z.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17629
+ className: css(styles$B.lintContainer)
17630
+ }, this.renderLink(styles$B.inlineHoverTarget), /*#__PURE__*/React.createElement("span", null, children));
17627
17631
  }
17628
17632
  return /*#__PURE__*/React.createElement("div", {
17629
- className: css(styles$z.lintContainer)
17630
- }, this.renderLink(styles$z.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
17633
+ className: css(styles$B.lintContainer)
17634
+ }, this.renderLink(styles$B.hoverTarget), /*#__PURE__*/React.createElement("div", null, children));
17631
17635
  }
17632
17636
  }
17633
- const styles$z = StyleSheet.create({
17637
+ const styles$B = StyleSheet.create({
17634
17638
  // This is the class of the outermost element.
17635
17639
  // We use relative positioning so that the lint indicator can be
17636
17640
  // positioned absolutely relative to the position of the linty container.
@@ -19479,7 +19483,7 @@ class Renderer extends React.Component {
19479
19483
  paddingLeft: margin,
19480
19484
  paddingRight: margin
19481
19485
  };
19482
- const mobileInnerStyle = _extends({}, innerStyle, styles$y.mobileZoomableParentFix);
19486
+ const mobileInnerStyle = _extends({}, innerStyle, styles$A.mobileZoomableParentFix);
19483
19487
  return /*#__PURE__*/React.createElement("div", {
19484
19488
  key: state.key,
19485
19489
  className: "perseus-block-math",
@@ -19600,7 +19604,7 @@ class Renderer extends React.Component {
19600
19604
  paddingLeft: 0,
19601
19605
  paddingRight: 0
19602
19606
  };
19603
- const mobileInnerStyle = _extends({}, innerStyle, styles$y.mobileZoomableParentFix);
19607
+ const mobileInnerStyle = _extends({}, innerStyle, styles$A.mobileZoomableParentFix);
19604
19608
  const wrappedOutput = /*#__PURE__*/React.createElement("div", {
19605
19609
  style: _extends({}, mobileInnerStyle, {
19606
19610
  overflowX: "auto"
@@ -20245,7 +20249,7 @@ Renderer.defaultProps = {
20245
20249
  onSerializedStateUpdated: () => {},
20246
20250
  linterContext: PerseusLinter.linterContextDefault
20247
20251
  };
20248
- const styles$y = {
20252
+ const styles$A = {
20249
20253
  mobileZoomableParentFix: {
20250
20254
  // NOTE(abdul): There is an issue where transform animations will
20251
20255
  // cause the Zoomable component to disappear when running on the
@@ -21262,10 +21266,10 @@ class InputNumber extends React.Component {
21262
21266
  // Note: This is _very_ similar to what `numeric-input.jsx` does. If
21263
21267
  // you modify this, double-check if you also need to modify that
21264
21268
  // component.
21265
- const inputStyles = [styles$x.default, this.props.size === "small" ? styles$x.small : null, this.props.rightAlign ? styles$x.rightAlign : styles$x.leftAlign];
21269
+ const inputStyles = [styles$z.default, this.props.size === "small" ? styles$z.small : null, this.props.rightAlign ? styles$z.rightAlign : styles$z.leftAlign];
21266
21270
  // Unanswered
21267
21271
  if (rubric && !this.props.currentValue) {
21268
- inputStyles.push(styles$x.answerStateUnanswered);
21272
+ inputStyles.push(styles$z.answerStateUnanswered);
21269
21273
  }
21270
21274
  return /*#__PURE__*/React.createElement(InputWithExamples
21271
21275
  // eslint-disable-next-line react/no-string-refs
@@ -21350,7 +21354,7 @@ InputNumber.defaultProps = {
21350
21354
  apiOptions: ApiOptions.defaults,
21351
21355
  linterContext: linterContextDefault
21352
21356
  };
21353
- const styles$x = StyleSheet.create({
21357
+ const styles$z = StyleSheet.create({
21354
21358
  default: {
21355
21359
  width: 80,
21356
21360
  height: "auto"
@@ -22351,7 +22355,7 @@ const FocusRing = function FocusRing(props) {
22351
22355
  };
22352
22356
  return /*#__PURE__*/React.createElement("span", {
22353
22357
  "data-test-id": "focus-ring",
22354
- className: css(styles$w.ring),
22358
+ className: css(styles$y.ring),
22355
22359
  style: style
22356
22360
  }, children);
22357
22361
  };
@@ -22360,7 +22364,7 @@ FocusRing.defaultProps = {
22360
22364
  color: kaGreen$1,
22361
22365
  multipleSelect: false
22362
22366
  };
22363
- const styles$w = StyleSheet.create({
22367
+ const styles$y = StyleSheet.create({
22364
22368
  ring: {
22365
22369
  margin: "auto",
22366
22370
  display: "inline-block",
@@ -22397,7 +22401,7 @@ function getChoiceLetter(pos) {
22397
22401
  }
22398
22402
 
22399
22403
  const CHOICE_ICON_SIZE = 24;
22400
- const styles$v = StyleSheet.create({
22404
+ const styles$x = StyleSheet.create({
22401
22405
  iconWrapper: {
22402
22406
  display: "inline-block",
22403
22407
  position: "relative"
@@ -22420,7 +22424,7 @@ const CrossOutLine = function CrossOutLine(props) {
22420
22424
  width: CROSS_OUT_LINE_SIZE,
22421
22425
  height: CROSS_OUT_LINE_SIZE,
22422
22426
  viewBox: `0 0 ${CROSS_OUT_LINE_SIZE} ${CROSS_OUT_LINE_SIZE}`,
22423
- className: css(styles$u.crossOutLine),
22427
+ className: css(styles$w.crossOutLine),
22424
22428
  "data-test-id": "choice-icon__cross-out-line"
22425
22429
  }, /*#__PURE__*/React.createElement("line", {
22426
22430
  // The line stretches from the bottom-left to top-right.
@@ -22435,7 +22439,7 @@ const CrossOutLine = function CrossOutLine(props) {
22435
22439
  strokeLinecap: "round"
22436
22440
  }));
22437
22441
  };
22438
- const styles$u = StyleSheet.create({
22442
+ const styles$w = StyleSheet.create({
22439
22443
  crossOutLine: {
22440
22444
  // Center the icon within the container.
22441
22445
  position: "absolute",
@@ -22516,7 +22520,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22516
22520
  } = props;
22517
22521
  const dynamicStyles = getDynamicStyles(checked, showCorrectness, pressed, multipleSelect, correct);
22518
22522
  return /*#__PURE__*/React.createElement("div", {
22519
- className: css(styles$v.iconWrapper)
22523
+ className: css(styles$x.iconWrapper)
22520
22524
  }, /*#__PURE__*/React.createElement(FocusRing, {
22521
22525
  color: Color.blue,
22522
22526
  visible: focused || hovered,
@@ -22525,13 +22529,13 @@ const ChoiceIcon = function ChoiceIcon(props) {
22525
22529
  // @ts-expect-error - TS2322 - Type '{ backgroundColor: string | null | undefined; borderColor: string; color: string; borderRadius: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
22526
22530
  style: dynamicStyles,
22527
22531
  "data-test-id": "choice-icon__library-choice-icon",
22528
- className: css(styles$t.circle, showCorrectness && correct && styles$t.circleCorrect, showCorrectness && !correct && styles$t.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$t.circleIncorrectAnswered)
22532
+ className: css(styles$v.circle, showCorrectness && correct && styles$v.circleCorrect, showCorrectness && !correct && styles$v.circleIncorrect, showCorrectness && !correct && (checked || previouslyAnswered) && styles$v.circleIncorrectAnswered)
22529
22533
  // used in BaseRadio editMode to check
22530
22534
  // if we actually clicked on the radio icon
22531
22535
  ,
22532
22536
  "data-is-radio-icon": true
22533
22537
  }, /*#__PURE__*/React.createElement("div", {
22534
- className: css(styles$t.innerWrapper)
22538
+ className: css(styles$v.innerWrapper)
22535
22539
  }, /*#__PURE__*/React.createElement(ChoiceInner, {
22536
22540
  pos: pos,
22537
22541
  showCorrectness: showCorrectness,
@@ -22540,7 +22544,7 @@ const ChoiceIcon = function ChoiceIcon(props) {
22540
22544
  color: dynamicStyles.borderColor
22541
22545
  }));
22542
22546
  };
22543
- const styles$t = StyleSheet.create({
22547
+ const styles$v = StyleSheet.create({
22544
22548
  innerWrapper: {
22545
22549
  width: CHOICE_ICON_SIZE,
22546
22550
  height: CHOICE_ICON_SIZE,
@@ -22632,19 +22636,19 @@ const OptionStatus = function OptionStatus(props) {
22632
22636
  }
22633
22637
  let textStyle;
22634
22638
  if (correct) {
22635
- textStyle = styles$s.correct;
22639
+ textStyle = styles$u.correct;
22636
22640
  } else {
22637
22641
  if (checked || previouslyAnswered) {
22638
- textStyle = styles$s.incorrectAnswered;
22642
+ textStyle = styles$u.incorrectAnswered;
22639
22643
  } else {
22640
- textStyle = styles$s.incorrect;
22644
+ textStyle = styles$u.incorrect;
22641
22645
  }
22642
22646
  }
22643
22647
  return /*#__PURE__*/React.createElement("div", {
22644
- className: css(styles$s.text, textStyle)
22648
+ className: css(styles$u.text, textStyle)
22645
22649
  }, renderText(checked, correct, crossedOut));
22646
22650
  };
22647
- const styles$s = StyleSheet.create({
22651
+ const styles$u = StyleSheet.create({
22648
22652
  text: {
22649
22653
  alignItems: "center",
22650
22654
  display: "flex",
@@ -22719,8 +22723,8 @@ const Choice = function Choice(props) {
22719
22723
  crossedOut: updatedCrossedOut
22720
22724
  });
22721
22725
  }
22722
- const descriptionClassName = classNames("description", css(styles$r.description));
22723
- const rationaleClassName = classNames("perseus-radio-rationale-content", css(styles$r.rationale));
22726
+ const descriptionClassName = classNames("description", css(styles$t.description));
22727
+ const rationaleClassName = classNames("perseus-radio-rationale-content", css(styles$t.rationale));
22724
22728
 
22725
22729
  // We want to show the choices as dimmed out when the choices are disabled.
22726
22730
  // However, we don't want to do this when we're in review mode in the
@@ -22880,7 +22884,7 @@ const Choice = function Choice(props) {
22880
22884
  "data-test-id": `perseus-radio-rationale-content-${pos}`
22881
22885
  }, rationale));
22882
22886
  };
22883
- const styles$r = StyleSheet.create({
22887
+ const styles$t = StyleSheet.create({
22884
22888
  description: {
22885
22889
  display: "inline-block",
22886
22890
  width: "100%"
@@ -23072,10 +23076,10 @@ const BaseRadio = function BaseRadio(props) {
23072
23076
  const isMobile = apiOptions.isMobile;
23073
23077
  const firstChoiceHighlighted = choices[0].highlighted;
23074
23078
  const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
23075
- const className = classNames("perseus-widget-radio", !editMode && "perseus-rendered-radio", css(styles$q.radio, styles$q.responsiveRadioContainer, firstChoiceHighlighted && isMobile && styles$q.radioContainerFirstHighlighted, lastChoiceHighlighted && isMobile && styles$q.radioContainerLastHighlighted));
23076
- const instructionsClassName = classNames("instructions", css(styles$q.instructions, isMobile && styles$q.instructionsMobile));
23079
+ 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));
23080
+ const instructionsClassName = classNames("instructions", css(styles$s.instructions, isMobile && styles$s.instructionsMobile));
23077
23081
  const instructions = getInstructionsText(multipleSelect, countChoices, numCorrect);
23078
- const responsiveClassName = css(styles$q.responsiveFieldset);
23082
+ const responsiveClassName = css(styles$s.responsiveFieldset);
23079
23083
  const fieldset = /*#__PURE__*/React.createElement("fieldset", {
23080
23084
  className: `perseus-widget-radio-fieldset ${responsiveClassName}`
23081
23085
  }, /*#__PURE__*/React.createElement("legend", {
@@ -23126,7 +23130,7 @@ const BaseRadio = function BaseRadio(props) {
23126
23130
  const aphroditeClassName = checked => {
23127
23131
  // Whether or not to show correctness borders
23128
23132
  // for this choice and the next choice.
23129
- return css(sharedStyles.aboveScratchpad, styles$q.item, styles$q.responsiveItem, checked && styles$q.selectedItem, checked && choice.highlighted && styles$q.aboveBackdrop, checked && choice.highlighted && apiOptions.isMobile && styles$q.aboveBackdropMobile, nextChoiceHighlighted && apiOptions.isMobile && styles$q.nextHighlighted);
23133
+ 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);
23130
23134
  };
23131
23135
 
23132
23136
  // HACK(abdulrahman): Preloads the selection-state
@@ -23200,14 +23204,14 @@ const BaseRadio = function BaseRadio(props) {
23200
23204
  // Allow for horizontal scrolling if content is too wide, which may be
23201
23205
  // an issue especially on phones.
23202
23206
  return /*#__PURE__*/React.createElement("div", {
23203
- className: css(styles$q.responsiveContainer)
23207
+ className: css(styles$s.responsiveContainer)
23204
23208
  }, fieldset);
23205
23209
  };
23206
23210
  BaseRadio.defaultProps = {
23207
23211
  editMode: false,
23208
23212
  multipleSelect: false
23209
23213
  };
23210
- const styles$q = StyleSheet.create({
23214
+ const styles$s = StyleSheet.create({
23211
23215
  instructions: {
23212
23216
  display: "block",
23213
23217
  color: gray17,
@@ -23855,9 +23859,9 @@ class Categorizer extends React.Component {
23855
23859
  indexedItems = Util.shuffle(indexedItems, this.props.problemNum);
23856
23860
  }
23857
23861
  const table = /*#__PURE__*/React.createElement("table", {
23858
- className: "categorizer-table " + css(styles$p.mobileTable)
23862
+ className: "categorizer-table " + css(styles$r.mobileTable)
23859
23863
  }, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("td", {
23860
- className: css(styles$p.emptyHeaderCell)
23864
+ className: css(styles$r.emptyHeaderCell)
23861
23865
  }), this.props.categories.map((category, i) => {
23862
23866
  // Array index is the correct key here, as that's
23863
23867
  // how category grading actually works -- no way
@@ -23865,7 +23869,7 @@ class Categorizer extends React.Component {
23865
23869
  // middle. (If we later add that, this should be
23866
23870
  // fixed.)
23867
23871
  return /*#__PURE__*/React.createElement("th", {
23868
- className: css(styles$p.header),
23872
+ className: css(styles$r.header),
23869
23873
  key: i
23870
23874
  }, /*#__PURE__*/React.createElement(Renderer, {
23871
23875
  content: category,
@@ -23885,7 +23889,7 @@ class Categorizer extends React.Component {
23885
23889
  })), self.props.categories.map((catName, catNum) => {
23886
23890
  const selected = self.props.values[itemNum] === catNum;
23887
23891
  return /*#__PURE__*/React.createElement("td", {
23888
- className: "category " + css(styles$p.cell),
23892
+ className: "category " + css(styles$r.cell),
23889
23893
  key: catNum
23890
23894
  }, /*#__PURE__*/React.createElement("div", {
23891
23895
  className: ClassNames.INTERACTIVE,
@@ -23904,7 +23908,7 @@ class Categorizer extends React.Component {
23904
23908
  onChange: this.onChange.bind(this, itemNum, catNum),
23905
23909
  onClick: e => e.stopPropagation()
23906
23910
  }), !isMobile && /*#__PURE__*/React.createElement("span", {
23907
- className: css(styles$p.radioSpan, selected && styles$p.checkedRadioSpan, this.props.static && selected && styles$p.staticCheckedRadioSpan)
23911
+ className: css(styles$r.radioSpan, selected && styles$r.checkedRadioSpan, this.props.static && selected && styles$r.staticCheckedRadioSpan)
23908
23912
  }, selected ? /*#__PURE__*/React.createElement(InlineIcon, iconCircle) : /*#__PURE__*/React.createElement(InlineIcon, iconCircleThin))));
23909
23913
  }));
23910
23914
  /* eslint-enable max-len */
@@ -23916,7 +23920,7 @@ class Categorizer extends React.Component {
23916
23920
  "categorizer-container": true,
23917
23921
  "static-mode": this.props.static
23918
23922
  });
23919
- const inlineStyles = this.props.apiOptions.isMobile ? [styles$p.fullBleedContainer] : [];
23923
+ const inlineStyles = this.props.apiOptions.isMobile ? [styles$r.fullBleedContainer] : [];
23920
23924
  return /*#__PURE__*/React.createElement("div", {
23921
23925
  className: extraClassNames + " " + css(...inlineStyles)
23922
23926
  }, table);
@@ -23961,7 +23965,7 @@ const pageMargin = 16;
23961
23965
 
23962
23966
  // Stylesheets aren't directly testable
23963
23967
  /* c8 ignore next */
23964
- const styles$p = StyleSheet.create({
23968
+ const styles$r = StyleSheet.create({
23965
23969
  mobileTable: {
23966
23970
  [mediaQueries.smOrSmaller]: {
23967
23971
  minWidth: "auto"
@@ -24197,7 +24201,7 @@ class CSProgram extends React.Component {
24197
24201
  // creator "went wild".
24198
24202
  // http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
24199
24203
  return /*#__PURE__*/React.createElement("div", {
24200
- className: css(styles$o.widthOverride, styleContainer && styles$o.container)
24204
+ className: css(styles$q.widthOverride, styleContainer && styles$q.container)
24201
24205
  }, /*#__PURE__*/React.createElement("iframe", {
24202
24206
  sandbox: sandboxOptions,
24203
24207
  src: url,
@@ -24226,7 +24230,7 @@ CSProgram.defaultProps = {
24226
24230
  // optional message
24227
24231
  message: null
24228
24232
  };
24229
- const styles$o = StyleSheet.create({
24233
+ const styles$q = StyleSheet.create({
24230
24234
  // Override the inherited width from the perseus paragraph class
24231
24235
  // 820 is minimum width for 420px editor and 400px canvas
24232
24236
  widthOverride: {
@@ -24271,7 +24275,7 @@ class Definition extends React.Component {
24271
24275
  }) => /*#__PURE__*/React.createElement(Popover, {
24272
24276
  content: /*#__PURE__*/React.createElement(PopoverContentCore, {
24273
24277
  color: "white",
24274
- style: styles$n.tooltipBody,
24278
+ style: styles$p.tooltipBody,
24275
24279
  closeButtonVisible: true
24276
24280
  }, /*#__PURE__*/React.createElement(Renderer, {
24277
24281
  apiOptions: this.props.apiOptions,
@@ -24302,7 +24306,7 @@ Definition.defaultProps = {
24302
24306
  togglePrompt: "define me",
24303
24307
  definition: "definition goes here"
24304
24308
  };
24305
- const styles$n = {
24309
+ const styles$p = {
24306
24310
  tooltipBody: {
24307
24311
  color: Color.offBlack,
24308
24312
  fontSize: 20,
@@ -24466,15 +24470,15 @@ class Explanation extends React.Component {
24466
24470
  const onClick = this._onClick;
24467
24471
  if (isMobile) {
24468
24472
  linkContainer = /*#__PURE__*/React.createElement("div", {
24469
- className: css(styles$m.linkContainer)
24473
+ className: css(styles$o.linkContainer)
24470
24474
  }, /*#__PURE__*/React.createElement("a", {
24471
- className: css(styles$m.mobileExplanationLink),
24475
+ className: css(styles$o.mobileExplanationLink),
24472
24476
  href: href,
24473
24477
  onClick: onClick,
24474
24478
  role: "button",
24475
24479
  "aria-expanded": this.state.expanded
24476
24480
  }, linkAnchor), this.state.expanded && /*#__PURE__*/React.createElement("svg", {
24477
- className: css(styles$m.disclosureArrow)
24481
+ className: css(styles$o.disclosureArrow)
24478
24482
  }, /*#__PURE__*/React.createElement("polygon", {
24479
24483
  style: {
24480
24484
  fill: backgroundColor
@@ -24487,13 +24491,13 @@ class Explanation extends React.Component {
24487
24491
  // an anchor tag (if rendered in a modal) or a Wonder Blocks
24488
24492
  // link component in lesson page, see: `article-page.jsx`.
24489
24493
  linkContainer = /*#__PURE__*/React.createElement("div", {
24490
- className: css(styles$m.linkContainer, styles$m.articleLink)
24494
+ className: css(styles$o.linkContainer, styles$o.articleLink)
24491
24495
  }, /*#__PURE__*/React.createElement(Link, {
24492
- className: css(styles$m.explanationLink)
24496
+ className: css(styles$o.explanationLink)
24493
24497
  // HACK(michaelpolyak): WB Link doesn't passthrough
24494
24498
  // class name.
24495
24499
  ,
24496
- style: styles$m.explanationLink,
24500
+ style: styles$o.explanationLink,
24497
24501
  href: href,
24498
24502
  onClick: onClick,
24499
24503
  role: "button",
@@ -24503,9 +24507,9 @@ class Explanation extends React.Component {
24503
24507
  // NOTE: For exercises, the baseElements `Link` is an
24504
24508
  // anchor tag, see: `perseus-api.jsx`.
24505
24509
  linkContainer = /*#__PURE__*/React.createElement("div", {
24506
- className: css(styles$m.linkContainer, styles$m.exerciseLink)
24510
+ className: css(styles$o.linkContainer, styles$o.exerciseLink)
24507
24511
  }, /*#__PURE__*/React.createElement(Link, {
24508
- className: css(styles$m.explanationLink),
24512
+ className: css(styles$o.explanationLink),
24509
24513
  href: href,
24510
24514
  onClick: onClick,
24511
24515
  role: "button",
@@ -24513,11 +24517,11 @@ class Explanation extends React.Component {
24513
24517
  }, `[${linkAnchor}]`));
24514
24518
  }
24515
24519
  }
24516
- const expandedStyle = isMobile ? styles$m.contentExpandedMobile : styles$m.contentExpanded;
24520
+ const expandedStyle = isMobile ? styles$o.contentExpandedMobile : styles$o.contentExpanded;
24517
24521
  return /*#__PURE__*/React.createElement("div", {
24518
- className: css(styles$m.container)
24522
+ className: css(styles$o.container)
24519
24523
  }, linkContainer, this.state.expanded && /*#__PURE__*/React.createElement("div", {
24520
- className: css(styles$m.content, isMobile && styles$m.contentMobile, this.state.expanded && expandedStyle)
24524
+ className: css(styles$o.content, isMobile && styles$o.contentMobile, this.state.expanded && expandedStyle)
24521
24525
  // eslint-disable-next-line react/no-string-refs
24522
24526
  ,
24523
24527
  ref: "content"
@@ -24541,7 +24545,7 @@ const verticalContentPadding = 10;
24541
24545
  const arrowWidth = 30;
24542
24546
  const arrowHeight = 14;
24543
24547
  const backgroundColor = gray95;
24544
- const styles$m = StyleSheet.create({
24548
+ const styles$o = StyleSheet.create({
24545
24549
  container: {
24546
24550
  display: "inline",
24547
24551
  position: "relative"
@@ -24646,15 +24650,15 @@ class GradedGroupAnswerBar extends React.Component {
24646
24650
  onCheckAnswer,
24647
24651
  onNextQuestion
24648
24652
  } = this.props;
24649
- const answerBarStyle = _extends({}, styles$l.answerBar, {
24653
+ const answerBarStyle = _extends({}, styles$n.answerBar, {
24650
24654
  backgroundColor: answerBarState === "CORRECT" ? Color.offWhite : "white",
24651
24655
  // Center the "Correct!" message only when there's no next question
24652
24656
  justifyContent: answerBarState === "CORRECT" && !onNextQuestion ? "center" : "space-between"
24653
24657
  });
24654
24658
  const message = answerBarState === "INCORRECT" ? /*#__PURE__*/React.createElement("span", {
24655
- style: styles$l.text
24659
+ style: styles$n.text
24656
24660
  }, /*#__PURE__*/React.createElement("span", {
24657
- style: styles$l.tryAgainIcon
24661
+ style: styles$n.tryAgainIcon
24658
24662
  }, /*#__PURE__*/React.createElement(InlineIcon, iconTryAgain)), /*#__PURE__*/React.createElement("span", {
24659
24663
  style: {
24660
24664
  marginLeft: 8
@@ -24673,7 +24677,7 @@ class GradedGroupAnswerBar extends React.Component {
24673
24677
  return /*#__PURE__*/React.createElement("div", {
24674
24678
  style: answerBarStyle
24675
24679
  }, /*#__PURE__*/React.createElement("span", {
24676
- style: styles$l.text
24680
+ style: styles$n.text
24677
24681
  }, /*#__PURE__*/React.createElement("span", {
24678
24682
  style: {
24679
24683
  fontSize: 28,
@@ -24695,7 +24699,7 @@ class GradedGroupAnswerBar extends React.Component {
24695
24699
  }
24696
24700
  }
24697
24701
  const fontSize = 17;
24698
- const styles$l = {
24702
+ const styles$n = {
24699
24703
  answerBar: {
24700
24704
  display: "flex",
24701
24705
  alignItems: "center",
@@ -24881,7 +24885,7 @@ class GradedGroup extends React.Component {
24881
24885
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'null'.
24882
24886
  gradeStatus = i18n._("Incorrect");
24883
24887
  }
24884
- const mobileClass = this.props.inGradedGroupSet ? css(styles$k.gradedGroupInSet) : css(styles$k.gradedGroup);
24888
+ const mobileClass = this.props.inGradedGroupSet ? css(styles$m.gradedGroupInSet) : css(styles$m.gradedGroup);
24885
24889
  const classes = classNames({
24886
24890
  [mobileClass]: apiOptions.isMobile,
24887
24891
  "perseus-graded-group": true,
@@ -24900,7 +24904,7 @@ class GradedGroup extends React.Component {
24900
24904
  return /*#__PURE__*/React.createElement("div", {
24901
24905
  className: classes
24902
24906
  }, !!this.props.title && /*#__PURE__*/React.createElement("div", {
24903
- className: css(styles$k.title)
24907
+ className: css(styles$m.title)
24904
24908
  }, this.props.title), /*#__PURE__*/React.createElement(Renderer, _extends({}, this.props, {
24905
24909
  // eslint-disable-next-line react/no-string-refs
24906
24910
  ref: "renderer",
@@ -24932,7 +24936,7 @@ class GradedGroup extends React.Component {
24932
24936
  }, i18n._("Next question")), this.props.hint && this.props.hint.content && (this.state.showHint ? /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("button", {
24933
24937
  // @ts-expect-error - TS2322 - Type 'string' is not assignable to type 'number | undefined'.
24934
24938
  tabIndex: "0",
24935
- className: css(styles$k.explanationTitle),
24939
+ className: css(styles$m.explanationTitle),
24936
24940
  onClick: () => this.setState({
24937
24941
  showHint: false
24938
24942
  }),
@@ -24964,7 +24968,7 @@ class GradedGroup extends React.Component {
24964
24968
  showHint: true
24965
24969
  });
24966
24970
  },
24967
- className: css(styles$k.showHintLink)
24971
+ className: css(styles$m.showHintLink)
24968
24972
  }, i18n._("Explain"))), apiOptions.isMobile && answerBarState !== "HIDDEN" && /*#__PURE__*/React.createElement(GradedGroupAnswerBar, {
24969
24973
  apiOptions: apiOptions,
24970
24974
  answerBarState: answerBarState,
@@ -24985,7 +24989,7 @@ GradedGroup.defaultProps = {
24985
24989
  const traverseChildWidgets$3 = function traverseChildWidgets(props, traverseRenderer) {
24986
24990
  return _.extend({}, props, traverseRenderer(props));
24987
24991
  };
24988
- const styles$k = StyleSheet.create({
24992
+ const styles$m = StyleSheet.create({
24989
24993
  gradedGroupInSet: {
24990
24994
  // Reset a few desktop-only styles that come from graded-group.less
24991
24995
  marginLeft: 0,
@@ -25058,18 +25062,18 @@ class Indicators extends React.Component {
25058
25062
  return /*#__PURE__*/React.createElement("ul", {
25059
25063
  // reduntantly add class name for use in .less files--
25060
25064
  // the styles object key gets hashed
25061
- className: classNames(css(styles$j.indicatorContainer), "indicatorContainer")
25065
+ className: classNames(css(styles$l.indicatorContainer), "indicatorContainer")
25062
25066
  }, this.props.gradedGroups.map(({
25063
25067
  title
25064
25068
  }, i) => /*#__PURE__*/React.createElement("li", {
25065
- className: css(styles$j.indicator),
25069
+ className: css(styles$l.indicator),
25066
25070
  key: title
25067
25071
  }, /*#__PURE__*/React.createElement(Clickable, {
25068
25072
  role: "button",
25069
25073
  "aria-label": i18n._("Skip to %(title)s", {
25070
25074
  title
25071
25075
  }),
25072
- style: styles$j.indicatorButton,
25076
+ style: styles$l.indicatorButton,
25073
25077
  onClick: () => this.props.onChangeCurrentGroup(i),
25074
25078
  onKeyDown: e => this.handleKeyDown(e, i)
25075
25079
  }, ({
@@ -25077,9 +25081,9 @@ class Indicators extends React.Component {
25077
25081
  focused,
25078
25082
  pressed
25079
25083
  }) => /*#__PURE__*/React.createElement(View, {
25080
- style: [styles$j.indicatorDot, (hovered || focused || pressed) && styles$j.indicatorDotFocused]
25084
+ style: [styles$l.indicatorDot, (hovered || focused || pressed) && styles$l.indicatorDotFocused]
25081
25085
  }, i === this.props.currentGroup && /*#__PURE__*/React.createElement(View, {
25082
- style: styles$j.indicatorDotActive
25086
+ style: styles$l.indicatorDotActive
25083
25087
  }, /*#__PURE__*/React.createElement("span", {
25084
25088
  className: css(a11y.srOnly)
25085
25089
  }, i18n._("Current"))))))));
@@ -25145,7 +25149,7 @@ class GradedGroupSet extends React.Component {
25145
25149
  } = getDependencies();
25146
25150
  if (JIPT.useJIPT && this.props.gradedGroups.length > 1) {
25147
25151
  return /*#__PURE__*/React.createElement("div", {
25148
- className: css(styles$j.container)
25152
+ className: css(styles$l.container)
25149
25153
  }, this.props.gradedGroups.map((gradedGroup, i) => {
25150
25154
  return (
25151
25155
  /*#__PURE__*/
@@ -25168,13 +25172,13 @@ class GradedGroupSet extends React.Component {
25168
25172
  const numGroups = this.props.gradedGroups.length;
25169
25173
  const handleNextQuestion = this.state.currentGroup < numGroups - 1 ? this.handleNextQuestion : null;
25170
25174
  return /*#__PURE__*/React.createElement("div", {
25171
- className: css(styles$j.container)
25175
+ className: css(styles$l.container)
25172
25176
  }, /*#__PURE__*/React.createElement("div", {
25173
- className: css(styles$j.top)
25177
+ className: css(styles$l.top)
25174
25178
  }, /*#__PURE__*/React.createElement("div", {
25175
- className: css(styles$j.title)
25179
+ className: css(styles$l.title)
25176
25180
  }, currentGroup.title), /*#__PURE__*/React.createElement("div", {
25177
- className: css(styles$j.spacer)
25181
+ className: css(styles$l.spacer)
25178
25182
  }), /*#__PURE__*/React.createElement(Indicators, {
25179
25183
  currentGroup: this.state.currentGroup,
25180
25184
  gradedGroups: this.props.gradedGroups,
@@ -25216,7 +25220,7 @@ var GradedGroupSet$1 = {
25216
25220
  tracking: "all",
25217
25221
  isLintable: true
25218
25222
  };
25219
- const styles$j = StyleSheet.create({
25223
+ const styles$l = StyleSheet.create({
25220
25224
  top: {
25221
25225
  display: "flex",
25222
25226
  flexDirection: "row"
@@ -25321,7 +25325,7 @@ class ButtonGroup extends React.Component {
25321
25325
  id: "" + i,
25322
25326
  ref: "button" + i,
25323
25327
  key: "" + i,
25324
- className: css(styles$i.buttonStyle, button.value === value && styles$i.selectedStyle),
25328
+ className: css(styles$k.buttonStyle, button.value === value && styles$k.selectedStyle),
25325
25329
  onClick: () => this.toggleSelect(button.value)
25326
25330
  }, button.content || "" + button.value);
25327
25331
  });
@@ -25338,7 +25342,7 @@ ButtonGroup.defaultProps = {
25338
25342
  value: null,
25339
25343
  allowEmpty: true
25340
25344
  };
25341
- const styles$i = StyleSheet.create({
25345
+ const styles$k = StyleSheet.create({
25342
25346
  buttonStyle: {
25343
25347
  backgroundColor: "white",
25344
25348
  border: "1px solid #ccc",
@@ -30885,7 +30889,7 @@ class AnswerChoice extends React.Component {
30885
30889
  checked
30886
30890
  } = this.props;
30887
30891
  return /*#__PURE__*/React.createElement("span", {
30888
- className: css(styles$h.checkmarkContainer)
30892
+ className: css(styles$j.checkmarkContainer)
30889
30893
  }, checked && /*#__PURE__*/React.createElement(Icon, {
30890
30894
  icon: iconCheckMedium,
30891
30895
  size: 16,
@@ -30905,11 +30909,11 @@ class AnswerChoice extends React.Component {
30905
30909
  } = this.state;
30906
30910
  const answerId = `${groupName}-${index}`;
30907
30911
  return /*#__PURE__*/React.createElement("div", {
30908
- className: css(styles$h.answerChoice, isInputFocused && styles$h.answerFocused),
30912
+ className: css(styles$j.answerChoice, isInputFocused && styles$j.answerFocused),
30909
30913
  onClick: e => this.handleClick(e),
30910
30914
  onKeyDown: e => this.handleKeyDown(e)
30911
30915
  }, /*#__PURE__*/React.createElement("span", {
30912
- className: css(styles$h.answerInputWrapper)
30916
+ className: css(styles$j.answerInputWrapper)
30913
30917
  }, /*#__PURE__*/React.createElement("input", {
30914
30918
  className: css(sharedStyles.perseusSrOnly),
30915
30919
  checked: checked,
@@ -30977,7 +30981,13 @@ class AnswerChoices extends React.Component {
30977
30981
  const {
30978
30982
  groupName
30979
30983
  } = this.state;
30980
- return /*#__PURE__*/React.createElement("fieldset", null, choices.map((choice, index) => /*#__PURE__*/React.createElement(AnswerChoice, _extends({}, choice, {
30984
+ return /*#__PURE__*/React.createElement("fieldset", {
30985
+ style: {
30986
+ border: "none",
30987
+ margin: 0,
30988
+ padding: 0
30989
+ }
30990
+ }, choices.map((choice, index) => /*#__PURE__*/React.createElement(AnswerChoice, _extends({}, choice, {
30981
30991
  key: choice.content,
30982
30992
  groupName: groupName,
30983
30993
  index: index,
@@ -30989,7 +30999,7 @@ class AnswerChoices extends React.Component {
30989
30999
  }))));
30990
31000
  }
30991
31001
  }
30992
- const styles$h = StyleSheet.create({
31002
+ const styles$j = StyleSheet.create({
30993
31003
  answerChoice: {
30994
31004
  display: "flex",
30995
31005
  alignItems: "center",
@@ -31020,44 +31030,122 @@ const styles$h = StyleSheet.create({
31020
31030
  }
31021
31031
  });
31022
31032
 
31033
+ const AnswerPill = props => {
31034
+ const {
31035
+ selectedAnswers,
31036
+ showCorrectness,
31037
+ markerRef,
31038
+ side,
31039
+ onClick,
31040
+ style
31041
+ } = props;
31042
+ const idFactory = useUniqueIdWithMock();
31043
+ const answerString = selectedAnswers.length > 1 ?
31044
+ // always need `ngettext` for variable numbers even if we don't use the singular, see https://khanacademy.slack.com/archives/C0918TZ5G/p1700163024293079
31045
+ i18n.ngettext("%(num)s answer", "%(num)s answers", selectedAnswers.length) : selectedAnswers[0];
31046
+
31047
+ // It should no longer be possible to interact with an answer after it
31048
+ // has been labeled (answered) correctly.
31049
+ const correct = showCorrectness === "correct";
31050
+ const incorrect = showCorrectness === "incorrect";
31051
+ const bringToFront = {
31052
+ boxShadow: `0 8px 8px ${Color.offBlack64}`,
31053
+ zIndex: 1000
31054
+ };
31055
+ return /*#__PURE__*/React.createElement(Popper, {
31056
+ placement: side,
31057
+ referenceElement: markerRef,
31058
+ modifiers: [{
31059
+ name: "preventOverflow",
31060
+ options: {
31061
+ rootBoundary: "viewport"
31062
+ }
31063
+ }]
31064
+ }, ({
31065
+ ref,
31066
+ style: popperStyle
31067
+ }) => /*#__PURE__*/React.createElement(Pill, {
31068
+ size: "large",
31069
+ kind: "accent",
31070
+ id: idFactory.get("perseus-label-image-widget-answer-pill"),
31071
+ onClick: correct ? undefined : onClick,
31072
+ ref: ref,
31073
+ style: [style, popperStyle, {
31074
+ ":hover": bringToFront,
31075
+ ":focus": bringToFront
31076
+ }, correct && styles$i.correct, incorrect && styles$i.incorrect]
31077
+ }, /*#__PURE__*/React.createElement(Renderer, {
31078
+ content: answerString
31079
+ })));
31080
+ };
31081
+ const styles$i = StyleSheet.create({
31082
+ correct: {
31083
+ backgroundColor: "#00880b" // WB green darkened by 18%
31084
+ },
31085
+
31086
+ incorrect: {
31087
+ backgroundColor: Color.offBlack64
31088
+ }
31089
+ });
31090
+
31091
+ const strings = {
31092
+ hideAnswersToggleLabel: i18n._("Hide answer choices")
31093
+ };
31094
+
31095
+ const HideAnswersToggle = props => {
31096
+ const idFactory = useUniqueIdWithMock();
31097
+ const switchId = idFactory.get("hide-answers-toggle");
31098
+ const labelId = idFactory.get("hide-answers-label");
31099
+ return /*#__PURE__*/React.createElement(View, {
31100
+ style: styles$h.switchWrapper
31101
+ }, /*#__PURE__*/React.createElement(Switch, {
31102
+ id: switchId,
31103
+ checked: props.areAnswersHidden,
31104
+ onChange: props.onChange,
31105
+ "aria-labelledby": labelId
31106
+ }), /*#__PURE__*/React.createElement(LabelMedium, {
31107
+ id: labelId,
31108
+ htmlFor: switchId
31109
+ }, strings.hideAnswersToggleLabel));
31110
+ };
31111
+ const styles$h = StyleSheet.create({
31112
+ switchWrapper: {
31113
+ display: "flex",
31114
+ flexDirection: "row",
31115
+ flexWrap: "wrap-reverse",
31116
+ alignItems: "center",
31117
+ gap: "0.5em",
31118
+ marginTop: "1em"
31119
+ }
31120
+ });
31121
+
31023
31122
  /* eslint-disable @khanacademy/ts-no-error-suppressions */
31123
+ function shouldReduceMotion() {
31124
+ const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
31125
+ return !mediaQuery || mediaQuery.matches;
31126
+ }
31024
31127
  class Marker extends React.Component {
31025
31128
  constructor(...args) {
31026
31129
  super(...args);
31027
31130
  // The marker icon element.
31028
31131
  this._icon = void 0;
31029
- this.state = {
31030
- isFocused: false
31031
- };
31032
- }
31033
- handleFocus() {
31034
- this.setState({
31035
- isFocused: true
31036
- });
31037
- }
31038
- handleBlur() {
31039
- this.setState({
31040
- isFocused: false
31041
- });
31042
31132
  }
31043
31133
  renderIcon() {
31044
31134
  const {
31045
31135
  selected,
31046
31136
  showCorrectness,
31047
31137
  showSelected,
31048
- showPulsate
31138
+ showPulsate,
31139
+ focused
31049
31140
  } = this.props;
31050
- const {
31051
- isFocused
31052
- } = this.state;
31053
31141
 
31054
31142
  // Only a single marker may be "selected" at a time.
31055
31143
  // `showSelected` is a controlled prop, that may be set to `true` for
31056
31144
  // one marker at a time.
31057
- // `isFocused` is a controlled state, driven by focus events, and may
31145
+ // `focused` is a controlled state, driven by focus events, and may
31058
31146
  // only be `true` when there's no answer choices popup visible, and
31059
31147
  // keyboard focus is given to the marker.
31060
- const isSelected = showSelected || isFocused;
31148
+ const isSelected = showSelected || focused;
31061
31149
  let innerIcon;
31062
31150
  let iconStyles;
31063
31151
  if (showCorrectness) {
@@ -31067,17 +31155,28 @@ class Marker extends React.Component {
31067
31155
  color: Color.white
31068
31156
  });
31069
31157
  if (showCorrectness === "correct") {
31070
- iconStyles = [styles$g.markerCorrect, isSelected && styles$g.markerCorrectSelected];
31158
+ iconStyles = [styles$g.markerGraded, styles$g.markerCorrect, isSelected && styles$g.markerSelected];
31071
31159
  } else {
31072
- iconStyles = [styles$g.markerIncorrect, isSelected && styles$g.markerIncorrectSelected];
31160
+ iconStyles = [styles$g.markerGraded, styles$g.markerIncorrect, isSelected && styles$g.markerSelected];
31073
31161
  }
31162
+ } else if (focused) {
31163
+ iconStyles = [styles$g.markerFocused, selected && selected.length > 0 && styles$g.markerFilled, showSelected && styles$g.markerSelected];
31074
31164
  } else if (selected && selected.length > 0) {
31075
- iconStyles = [styles$g.markerFilled, isSelected && styles$g.markerFilledSelected];
31165
+ iconStyles = [styles$g.markerFilled, isSelected && styles$g.markerSelected];
31166
+ } else if (isSelected) {
31167
+ iconStyles = [styles$g.markerSelected];
31076
31168
  } else {
31077
- iconStyles = [isSelected ? styles$g.markerUnfilledSelected : showPulsate && styles$g.markerUnfilledPulsate];
31169
+ iconStyles = [styles$g.markerPulsateBase, shouldReduceMotion() ? showPulsate && styles$g.markerUnfilledPulsateOnce : showPulsate && styles$g.markerUnfilledPulsateInfinite];
31078
31170
  }
31079
- return /*#__PURE__*/React.createElement("div", {
31080
- className: css(styles$g.markerIcon, ...iconStyles),
31171
+ if (isSelected && selected && selected.length === 0) {
31172
+ innerIcon = /*#__PURE__*/React.createElement(Icon, {
31173
+ icon: iconChevronDown,
31174
+ size: 8,
31175
+ color: Color.white
31176
+ });
31177
+ }
31178
+ return /*#__PURE__*/React.createElement(View, {
31179
+ style: [styles$g.markerIcon, iconStyles],
31081
31180
  ref: node => this._icon = node
31082
31181
  }, innerIcon);
31083
31182
  }
@@ -31100,13 +31199,12 @@ class Marker extends React.Component {
31100
31199
  top: `${y}%`
31101
31200
  },
31102
31201
  tabIndex: isDisabled ? -1 : 0,
31103
- onFocus: () => this.handleFocus(),
31104
- onBlur: () => this.handleBlur()
31105
- // @ts-expect-error - TS2345 - Argument of type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to parameter of type 'MouseEvent'.
31106
- ,
31107
- onClick: e => this.props.onClick(e)
31108
- // @ts-expect-error - TS2345 - Argument of type 'KeyboardEvent<HTMLButtonElement>' is not assignable to parameter of type 'KeyboardEvent'.
31109
- ,
31202
+ onFocus: this.props.onFocus,
31203
+ onBlur: this.props.onBlur,
31204
+ onClick: e => {
31205
+ e.preventDefault();
31206
+ this.props.onClick(e);
31207
+ },
31110
31208
  onKeyDown: e => this.props.onKeyDown(e)
31111
31209
  }, this.renderIcon());
31112
31210
  }
@@ -31114,15 +31212,8 @@ class Marker extends React.Component {
31114
31212
  Marker.defaultProps = {
31115
31213
  selected: []
31116
31214
  };
31117
- const markerColor = "#1865f2";
31118
- const selectedColor = "#2552b0";
31119
- const activeColor = selectedColor;
31120
- const correctColor = "#00a60e";
31121
- const correctActiveColor = "#167b1f";
31122
- const incorrectColor = "#909195";
31123
- const incorrectActiveColor = "#6c6e73";
31124
- const markerShadowColor = "rgba(33, 36, 44, 0.32)";
31125
31215
  const lightShadowColor = "rgba(33, 36, 44, 0.16)";
31216
+ const markerSize = 20;
31126
31217
  const styles$g = StyleSheet.create({
31127
31218
  unstyledButton: {
31128
31219
  padding: 0,
@@ -31135,9 +31226,12 @@ const styles$g = StyleSheet.create({
31135
31226
  },
31136
31227
  marker: {
31137
31228
  position: "absolute",
31229
+ outline: "none",
31138
31230
  // Center marker position based on it's maximum size.
31139
- width: 30,
31140
- height: 30,
31231
+ width: markerSize,
31232
+ height: markerSize,
31233
+ backgroundColor: Color.white,
31234
+ borderRadius: 30,
31141
31235
  marginLeft: -15,
31142
31236
  marginTop: -15
31143
31237
  },
@@ -31147,136 +31241,92 @@ const styles$g = StyleSheet.create({
31147
31241
  // The base and unfilled marker style.
31148
31242
  markerIcon: {
31149
31243
  display: "flex",
31150
- position: "relative",
31244
+ alignItems: "center",
31245
+ justifyContent: "center",
31151
31246
  boxSizing: "content-box",
31152
- width: 16,
31153
- height: 16,
31154
- // Center icon within marker.
31155
- marginLeft: 5,
31247
+ width: markerSize,
31248
+ height: markerSize,
31249
+ marginLeft: 2,
31156
31250
  cursor: "pointer",
31157
- backgroundColor: markerColor,
31158
- border: "solid 2px #ffffff",
31159
- borderRadius: 16,
31160
- boxShadow: `0 2px 6px 0 ${markerShadowColor}`
31251
+ border: `2px solid ${Color.offBlack64}`,
31252
+ borderRadius: markerSize,
31253
+ boxShadow: `0 8px 8px ${Color.offBlack8}`,
31254
+ ":hover": {
31255
+ outline: `2px solid ${Color.blue}`,
31256
+ outlineOffset: 2
31257
+ }
31161
31258
  },
31162
- markerUnfilledPulsate: {
31259
+ // The animation that presents the marker to the learner
31260
+ markerPulsateBase: {
31163
31261
  animationName: {
31164
31262
  "0%": {
31165
- transform: "scale(1)"
31263
+ transform: "scale(1)",
31264
+ backgroundColor: Color.blue
31166
31265
  },
31167
31266
  "100%": {
31168
- transform: "scale(1.5)"
31267
+ transform: "scale(1.3)",
31268
+ backgroundColor: Color.blue
31169
31269
  }
31170
31270
  },
31171
31271
  animationDirection: "alternate",
31172
31272
  animationDuration: "0.8s",
31173
- animationIterationCount: "infinite",
31174
31273
  animationTimingFunction: "ease-in",
31175
- transformOrigin: "50% 50%"
31274
+ transformOrigin: "50% 50%",
31275
+ animationIterationCount: "0"
31176
31276
  },
31177
- markerUnfilledSelected: {
31178
- "::before": {
31179
- content: "''",
31180
- display: "inline-block",
31181
- position: "absolute",
31182
- width: 20,
31183
- height: 20,
31184
- marginLeft: -4,
31185
- marginTop: -4,
31186
- border: `solid 2px ${selectedColor}`,
31187
- borderRadius: 20
31188
- },
31189
- ":active": {
31190
- backgroundColor: activeColor,
31191
- boxShadow: "none",
31192
- "::before": {
31193
- display: "none"
31194
- }
31277
+ markerUnfilledPulsateInfinite: {
31278
+ animationIterationCount: "infinite",
31279
+ ":hover": {
31280
+ outline: `0px solid ${Color.blue}`
31195
31281
  }
31196
31282
  },
31197
- markerFilled: {
31198
- width: 8,
31199
- height: 8,
31200
- // Center icon within marker.
31201
- marginLeft: 9,
31202
- borderRadius: 8,
31203
- boxShadow: `0 1px 1px 0 ${lightShadowColor}`
31283
+ markerUnfilledPulsateOnce: {
31284
+ // Doing the animation twice lets it ease-in and ease-out
31285
+ animationIterationCount: "2"
31204
31286
  },
31205
- markerFilledSelected: {
31206
- "::before": {
31207
- content: "''",
31208
- display: "inline-block",
31209
- position: "absolute",
31210
- width: 12,
31211
- height: 12,
31212
- marginLeft: -4,
31213
- marginTop: -4,
31214
- border: `solid 2px ${selectedColor}`,
31215
- borderRadius: 12
31216
- },
31217
- ":active": {
31218
- backgroundColor: activeColor,
31219
- boxShadow: "none",
31220
- "::before": {
31221
- display: "none"
31222
- }
31287
+ markerFocused: {
31288
+ outline: `2px solid ${Color.blue}`,
31289
+ outlineOffset: 2
31290
+ },
31291
+ // The learner is making an initial selection
31292
+ markerSelected: {
31293
+ boxShadow: `0 8px 8px ${Color.offBlack8}`,
31294
+ border: `solid 4px ${Color.white}`,
31295
+ backgroundColor: Color.blue,
31296
+ borderRadius: markerSize,
31297
+ transform: "rotate(180deg)",
31298
+ ":hover": {
31299
+ outline: `2px solid ${Color.blue}`,
31300
+ outlineOffset: -2
31223
31301
  }
31224
31302
  },
31225
- markerCorrect: {
31226
- width: 24,
31227
- height: 24,
31228
- marginLeft: 1,
31229
- marginTop: 1,
31230
- justifyContent: "center",
31231
- alignItems: "center",
31232
- background: correctColor,
31303
+ // The learner has made a selection
31304
+ markerFilled: {
31305
+ width: markerSize - 2,
31306
+ height: markerSize - 2,
31307
+ borderRadius: markerSize - 2,
31308
+ backgroundColor: "#ECF3FE",
31309
+ marginLeft: 3,
31310
+ marginTop: 0,
31311
+ border: `4px solid ${Color.blue}`,
31233
31312
  boxShadow: `0 1px 1px 0 ${lightShadowColor}`
31234
31313
  },
31235
- markerCorrectSelected: {
31236
- "::before": {
31237
- content: "''",
31238
- display: "inline-block",
31239
- position: "absolute",
31240
- width: 28,
31241
- height: 28,
31242
- border: `solid 2px ${selectedColor}`,
31243
- borderRadius: 28
31244
- },
31245
- ":active": {
31246
- backgroundColor: correctActiveColor,
31247
- boxShadow: "none",
31248
- "::before": {
31249
- display: "none"
31250
- }
31251
- }
31252
- },
31253
- markerIncorrect: {
31254
- width: 24,
31255
- height: 24,
31314
+ markerGraded: {
31315
+ width: markerSize,
31316
+ height: markerSize,
31256
31317
  marginLeft: 1,
31257
31318
  marginTop: 1,
31258
31319
  justifyContent: "center",
31259
31320
  alignItems: "center",
31260
- background: incorrectColor,
31261
- boxShadow: `0 1px 1px 0 ${lightShadowColor}`
31321
+ border: `2px solid ${Color.white}`,
31322
+ boxShadow: `0 8px 8px ${Color.offBlack8}`
31262
31323
  },
31263
- markerIncorrectSelected: {
31264
- "::before": {
31265
- content: "''",
31266
- display: "inline-block",
31267
- position: "absolute",
31268
- width: 28,
31269
- height: 28,
31270
- border: `solid 2px ${selectedColor}`,
31271
- borderRadius: 28
31272
- },
31273
- ":active": {
31274
- backgroundColor: incorrectActiveColor,
31275
- boxShadow: "none",
31276
- "::before": {
31277
- display: "none"
31278
- }
31279
- }
31324
+ markerCorrect: {
31325
+ background: "#00880b" // WB green darkened by 18%
31326
+ },
31327
+
31328
+ markerIncorrect: {
31329
+ background: Color.offBlack64
31280
31330
  }
31281
31331
  });
31282
31332
 
@@ -31354,7 +31404,20 @@ class LabelImage extends React.Component {
31354
31404
  /**
31355
31405
  * Determine the image side given a marker position (as percent of size).
31356
31406
  */
31357
- static imageSideForMarkerPosition(x, y) {
31407
+ static imageSideForMarkerPosition(x, y, preferredDirection) {
31408
+ // Special handling for preferred size
31409
+ if (preferredDirection && preferredDirection !== "NONE") {
31410
+ if (preferredDirection === "LEFT" && x > 20) {
31411
+ return "right";
31412
+ } else if (preferredDirection === "RIGHT" && x < 80) {
31413
+ return "left";
31414
+ } else if (preferredDirection === "UP" && y > 20) {
31415
+ return "bottom";
31416
+ } else if (preferredDirection === "DOWN" && y < 80) {
31417
+ return "top";
31418
+ }
31419
+ }
31420
+
31358
31421
  // Special handling for when marker is positioned near the horizontal
31359
31422
  // edges of the image. We want to ensure the returned side would not
31360
31423
  // result in a popup rendering that may overflow outside the page.
@@ -31509,51 +31572,14 @@ class LabelImage extends React.Component {
31509
31572
  this._selectedMarkerPopup = void 0;
31510
31573
  // The rendered list of answer choices for the currently selected marker.
31511
31574
  this._answerChoices = void 0;
31512
- this.handleDocumentClick = e => {
31513
- // Dismiss open popup with answer choices if user clicks outside it.
31514
- if (this._selectedMarkerPopup && this._answerChoices) {
31515
- const answerChoices = ReactDOM.findDOMNode(this._answerChoices);
31516
- const targetElement = e.target;
31517
- // HACK(michaelpolyak): We want to determine if the click target is
31518
- // contained within the popup layer. As there's no public interface
31519
- // to get this layer from the popup, we traverse several levels of
31520
- // answer choices parents to test whether the click target is
31521
- // contained within.
31522
- const containsEventTarget = (element, depth = 3) => element && (element.contains(targetElement) || depth > 0 && containsEventTarget(element.parentElement, depth - 1));
31523
- if (!containsEventTarget(answerChoices)) {
31524
- // Close popup and set focus back to the marker for which it was
31525
- // open.
31526
- this.dismissMarkerPopup();
31527
- }
31528
- }
31529
- };
31530
- this.handleDocumentKeyDown = e => {
31531
- // Dismiss open popup with answer choices if user presses Escape key.
31532
- if (this._selectedMarkerPopup && e.keyCode === 27) {
31533
- // Ensure other listeners are not triggered on key down event that
31534
- // closes the popup, as this will also dismiss the modal that the
31535
- // widget may be rendered within.
31536
- e.stopPropagation();
31537
-
31538
- // Close popup and set focus back to the marker for which it was
31539
- // open.
31540
- this.dismissMarkerPopup();
31541
- }
31542
- };
31543
31575
  this._markers = [];
31544
31576
  this.state = {
31545
- selectedMarkerIndex: -1,
31546
- markersInteracted: false
31577
+ activeMarkerIndex: -1,
31578
+ focusedMarkerIndex: -1,
31579
+ markersInteracted: false,
31580
+ hideAnswers: false
31547
31581
  };
31548
31582
  }
31549
- componentDidMount() {
31550
- document.addEventListener("click", this.handleDocumentClick, true);
31551
- document.addEventListener("keydown", this.handleDocumentKeyDown, true);
31552
- }
31553
- componentWillUnmount() {
31554
- document.removeEventListener("click", this.handleDocumentClick, true);
31555
- document.removeEventListener("keydown", this.handleDocumentKeyDown, true);
31556
- }
31557
31583
  simpleValidate(rubric) {
31558
31584
  return LabelImage.validate(this.getUserInput(), rubric);
31559
31585
  }
@@ -31589,17 +31615,17 @@ class LabelImage extends React.Component {
31589
31615
  }
31590
31616
  dismissMarkerPopup() {
31591
31617
  const {
31592
- selectedMarkerIndex: index
31618
+ activeMarkerIndex
31593
31619
  } = this.state;
31594
31620
 
31595
31621
  // No popup should be open if there's no selected marker.
31596
- if (index === -1) {
31622
+ if (activeMarkerIndex === -1) {
31597
31623
  return;
31598
31624
  }
31599
31625
  this.setState({
31600
- selectedMarkerIndex: -1
31626
+ activeMarkerIndex: -1
31601
31627
  }, () => {
31602
- const marker = this._markers[index];
31628
+ const marker = this._markers[activeMarkerIndex];
31603
31629
  // Set focus on the just-deselected-marker, to enable to resume
31604
31630
  // navigating between the markers using the keyboard.
31605
31631
  if (marker) {
@@ -31625,16 +31651,15 @@ class LabelImage extends React.Component {
31625
31651
  markers: updatedMarkers
31626
31652
  });
31627
31653
  }
31628
- handleMarkerClick(index, e) {
31654
+ activateMarker(index) {
31629
31655
  const {
31630
- selectedMarkerIndex
31656
+ activeMarkerIndex
31631
31657
  } = this.state;
31632
- e.preventDefault();
31633
31658
 
31634
31659
  // Select the marker, revealing answer choices.
31635
- if (selectedMarkerIndex !== index) {
31660
+ if (activeMarkerIndex !== index) {
31636
31661
  this.setState({
31637
- selectedMarkerIndex: index,
31662
+ activeMarkerIndex: index,
31638
31663
  markersInteracted: true
31639
31664
  }, () => {
31640
31665
  // A delay is required to allow answer choices to first render
@@ -31704,9 +31729,9 @@ class LabelImage extends React.Component {
31704
31729
  } = this.props;
31705
31730
 
31706
31731
  // Compile the user selected answer choices.
31707
- const selected = choices.filter((choice, index) => selection[index]);
31732
+ const selected = choices.filter((_, index) => selection[index]);
31708
31733
  this.handleMarkerChange(index, _extends({}, markers[index], {
31709
- selected
31734
+ selected: selected.length ? selected : undefined
31710
31735
  }));
31711
31736
  if (!multipleAnswers) {
31712
31737
  this.dismissMarkerPopup();
@@ -31728,50 +31753,82 @@ class LabelImage extends React.Component {
31728
31753
  checked: selected ? selected.includes(choice) : false
31729
31754
  })),
31730
31755
  multipleSelect: multipleAnswers,
31731
- onChange: selection => this.handleAnswerChoicesChangeForMarker(index, selection),
31756
+ onChange: selection => {
31757
+ var _this$props$analytics;
31758
+ (_this$props$analytics = this.props.analytics) == null || _this$props$analytics.onAnalyticsEvent({
31759
+ type: "perseus:label-image:choiced-interacted-with",
31760
+ payload: null
31761
+ });
31762
+ this.handleAnswerChoicesChangeForMarker(index, selection);
31763
+ },
31732
31764
  ref: node => this._answerChoices = node
31733
31765
  }));
31734
31766
  }
31735
31767
  renderMarkers() {
31736
31768
  const {
31737
31769
  markers,
31738
- questionCompleted
31770
+ questionCompleted,
31771
+ preferredPopoverDirection
31739
31772
  } = this.props;
31740
31773
  const {
31741
- selectedMarkerIndex,
31774
+ activeMarkerIndex,
31742
31775
  markersInteracted
31743
31776
  } = this.state;
31744
31777
 
31745
31778
  // Render all markers for widget.
31746
31779
  return markers.map((marker, index) => {
31747
31780
  const score = LabelImage.gradeMarker(marker);
31781
+ // Once the question is answered, show markers
31782
+ // with correct answers, otherwise passthrough
31783
+ // the correctness state.
31784
+ const showCorrectness = questionCompleted && score.hasAnswers && score.isCorrect ? "correct" : marker.showCorrectness;
31748
31785
  const element = /*#__PURE__*/React.createElement(Marker, _extends({}, marker, {
31749
- // Once the question is answered, show markers
31750
- // with correct answers, otherwise passthrough
31751
- // the correctness state.
31752
- showCorrectness: questionCompleted && score.hasAnswers && score.isCorrect ? "correct" : marker.showCorrectness,
31753
- showSelected: index === selectedMarkerIndex,
31786
+ showCorrectness: showCorrectness,
31787
+ showSelected: index === activeMarkerIndex,
31754
31788
  showPulsate: !markersInteracted,
31755
31789
  key: `${marker.x}.${marker.y}`,
31756
- onClick: e => this.handleMarkerClick(index, e),
31790
+ onClick: () => {
31791
+ var _this$props$analytics2;
31792
+ (_this$props$analytics2 = this.props.analytics) == null || _this$props$analytics2.onAnalyticsEvent({
31793
+ type: "perseus:label-image:marker-interacted-with",
31794
+ payload: null
31795
+ });
31796
+ this.activateMarker(index);
31797
+ },
31757
31798
  onKeyDown: e => this.handleMarkerKeyDown(index, e),
31758
- ref: node => this._markers[index] = node
31799
+ ref: node => this._markers[index] = node,
31800
+ focused: index === this.state.focusedMarkerIndex,
31801
+ onFocus: () => {
31802
+ this.setState({
31803
+ focusedMarkerIndex: index
31804
+ });
31805
+ },
31806
+ onBlur: () => {
31807
+ if (index === this.state.focusedMarkerIndex) {
31808
+ this.setState({
31809
+ focusedMarkerIndex: -1
31810
+ });
31811
+ }
31812
+ }
31759
31813
  }));
31760
31814
 
31761
- // The user selected marker is wrapped with a popup that shows its
31762
- // answer choices, otherwise it's returned as is.
31763
- if (index !== selectedMarkerIndex) {
31764
- return element;
31765
- }
31766
-
31767
31815
  // Determine whether page is rendered in a narrow browser window.
31768
31816
  const isNarrowPage = window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")).matches;
31817
+
31818
+ // Determine whether the image is wider than it is tall.
31819
+ const isWideImage = this.props.imageWidth / 2 > this.props.imageHeight;
31769
31820
  let side;
31821
+ let markerPosition;
31770
31822
  // Position popup closest to the center, preferring it renders
31771
31823
  // entirely within the image area.
31772
- if (isNarrowPage) {
31824
+ if (isNarrowPage || isWideImage) {
31773
31825
  side = marker.y > 50 ? "top" : "bottom";
31826
+ markerPosition = marker.y > 50 ? "bottom" : "top";
31774
31827
  } else {
31828
+ markerPosition = LabelImage.imageSideForMarkerPosition(marker.x, marker.y, preferredPopoverDirection);
31829
+ if (markerPosition === "center") {
31830
+ markerPosition = "bottom";
31831
+ }
31775
31832
  // This mirrors the calculated side of where the marker is
31776
31833
  // located within the image, so that popup appears closer to
31777
31834
  // the image center.
@@ -31779,24 +31836,44 @@ class LabelImage extends React.Component {
31779
31836
  left: "right",
31780
31837
  top: "bottom",
31781
31838
  right: "left",
31782
- bottom: "top",
31783
- center: "top"
31784
- }[LabelImage.imageSideForMarkerPosition(marker.x, marker.y)];
31839
+ bottom: "top"
31840
+ }[markerPosition];
31785
31841
  }
31842
+ const answerChoicesActive = index === activeMarkerIndex;
31843
+ const adjustPopoverDistance = {
31844
+ [side]: 15 // move the popover closer to the marker
31845
+ };
31786
31846
 
31787
- // TODO(michaelpolyak): Ideally we would always render markers
31788
- // wrapped in the popover. Setting `opened={false}` for those
31789
- // markers that are unselected (to hide their popup), this would
31790
- // keep the React tree more stable.
31791
- return /*#__PURE__*/React.createElement(Popover, {
31847
+ const adjustPillDistance = {
31848
+ [`margin${markerPosition.charAt(0).toUpperCase() + markerPosition.slice(1)}`]: 5 // move pill further from marker
31849
+ };
31850
+
31851
+ const showAnswerChoice = !answerChoicesActive && !this.state.hideAnswers;
31852
+ return /*#__PURE__*/React.createElement("div", {
31853
+ key: `answers-${marker.x}.${marker.y}`
31854
+ }, /*#__PURE__*/React.createElement(Popover, {
31792
31855
  content: () => /*#__PURE__*/React.createElement(PopoverContentCore, {
31793
- style: styles$f.choicesPopover
31856
+ style: [adjustPopoverDistance, styles$f.choicesPopover]
31794
31857
  }, this.renderAnswerChoicesForMarker(index, marker)),
31795
31858
  placement: side,
31796
- opened: true,
31797
- key: `${marker.x}.${marker.y}`,
31798
- ref: node => this._selectedMarkerPopup = node
31799
- }, element);
31859
+ onClose: () => this.dismissMarkerPopup(),
31860
+ opened: answerChoicesActive,
31861
+ ref: node => this._selectedMarkerPopup = node,
31862
+ showTail: false,
31863
+ dismissEnabled: true
31864
+ }, element), !!marker.selected && showAnswerChoice && /*#__PURE__*/React.createElement(AnswerPill, {
31865
+ selectedAnswers: marker.selected,
31866
+ showCorrectness: showCorrectness,
31867
+ markerRef:
31868
+ // this throws a warning because it's inadvisable to
31869
+ // call ReactDOM fns in render. we aren't storing
31870
+ // actual refs in this array, so not much I can do
31871
+ // outside of a refactor.
31872
+ ReactDOM.findDOMNode(this._markers[index]),
31873
+ side: side,
31874
+ onClick: () => this.activateMarker(index),
31875
+ style: adjustPillDistance
31876
+ }));
31800
31877
  });
31801
31878
  }
31802
31879
  renderInstructions() {
@@ -31831,7 +31908,7 @@ class LabelImage extends React.Component {
31831
31908
  imageHeight
31832
31909
  } = this.props;
31833
31910
  const {
31834
- selectedMarkerIndex
31911
+ activeMarkerIndex
31835
31912
  } = this.state;
31836
31913
  return /*#__PURE__*/React.createElement("div", null, this.renderInstructions(), /*#__PURE__*/React.createElement("div", {
31837
31914
  className: css(styles$f.markersCanvas),
@@ -31847,7 +31924,7 @@ class LabelImage extends React.Component {
31847
31924
  // dismiss the popup. If the image is larger in size
31848
31925
  // than its rendered in the widget, this would
31849
31926
  // result in a zoom of the image.
31850
- selectedMarkerIndex !== -1 && styles$f.imageInteractionDisabled)
31927
+ activeMarkerIndex !== -1 && styles$f.imageInteractionDisabled)
31851
31928
  }, /*#__PURE__*/React.createElement(context$1.Consumer, null, ({
31852
31929
  setAssetStatus
31853
31930
  }) => /*#__PURE__*/React.createElement(SvgImage, {
@@ -31856,7 +31933,19 @@ class LabelImage extends React.Component {
31856
31933
  width: imageWidth,
31857
31934
  height: imageHeight,
31858
31935
  setAssetStatus: setAssetStatus
31859
- }))), this.renderMarkers()));
31936
+ }))), this.renderMarkers()), /*#__PURE__*/React.createElement(HideAnswersToggle, {
31937
+ areAnswersHidden: this.state.hideAnswers,
31938
+ onChange: hideAnswers => {
31939
+ var _this$props$analytics3;
31940
+ (_this$props$analytics3 = this.props.analytics) == null || _this$props$analytics3.onAnalyticsEvent({
31941
+ type: "perseus:label-image:toggle-answers-hidden",
31942
+ payload: null
31943
+ });
31944
+ this.setState({
31945
+ hideAnswers
31946
+ });
31947
+ }
31948
+ }));
31860
31949
  }
31861
31950
  }
31862
31951
  const styles$f = StyleSheet.create({
@@ -31913,10 +32002,26 @@ const styles$f = StyleSheet.create({
31913
32002
  padding: 0
31914
32003
  }
31915
32004
  });
32005
+ const LabelImageWithDependencies = /*#__PURE__*/React.forwardRef((props, ref) => {
32006
+ const deps = useDependencies();
32007
+ return /*#__PURE__*/React.createElement(LabelImage, _extends({
32008
+ ref: ref,
32009
+ analytics: deps.analytics
32010
+ }, props));
32011
+ });
32012
+
32013
+ // HACK: Propogate "static" methods onto our wrapper component.
32014
+ // In the future we should adjust client apps to not depend on these static
32015
+ // methods and instead adjust Peresus to provide these facilities through
32016
+ // instance methods on our Renderers.
32017
+ // @ts-expect-error - TS2339 - Property 'validate' does not exist on type
32018
+ LabelImageWithDependencies.validate = LabelImage.validate;
32019
+ // @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
32020
+ LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
31916
32021
  var LabelImage$1 = {
31917
32022
  name: "label-image",
31918
32023
  displayName: "Label Image",
31919
- widget: LabelImage,
32024
+ widget: LabelImageWithDependencies,
31920
32025
  accessible: true,
31921
32026
  isLintable: true
31922
32027
  };
@@ -44079,7 +44184,7 @@ const init = function init(options) {
44079
44184
 
44080
44185
  // This file is processed by a Rollup plugin (replace) to inject the production
44081
44186
  const libName = "@khanacademy/perseus";
44082
- const libVersion = "17.0.0";
44187
+ const libVersion = "17.1.0";
44083
44188
  addLibraryVersionToPerseusDebug(libName, libVersion);
44084
44189
 
44085
44190
  const apiVersion = {