@khanacademy/perseus 0.0.0-PR418-20230323233458

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 (449) hide show
  1. package/.babelrc.js +8 -0
  2. package/.eslintrc.js +11 -0
  3. package/CHANGELOG.md +367 -0
  4. package/README.md +5 -0
  5. package/dist/es/index.css +4071 -0
  6. package/dist/es/index.js +51108 -0
  7. package/dist/es/index.js.map +1 -0
  8. package/dist/index.css +4071 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +51027 -0
  11. package/dist/index.js.flow +2 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/strings.js +150 -0
  14. package/package.json +95 -0
  15. package/src/__stories__/hints-renderer.stories.jsx +60 -0
  16. package/src/__stories__/item-renderer.stories.jsx +27 -0
  17. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  18. package/src/__testdata__/graphie_testdata.js +53 -0
  19. package/src/__testdata__/item-renderer_testdata.js +161 -0
  20. package/src/__testdata__/renderer_testdata.js +123 -0
  21. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +232 -0
  22. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +507 -0
  23. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +585 -0
  24. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +305 -0
  25. package/src/__tests__/a11y_test.jsx +304 -0
  26. package/src/__tests__/item-renderer_test.jsx +892 -0
  27. package/src/__tests__/jipt-paragraphs_test.js +34 -0
  28. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  29. package/src/__tests__/mock-widget.jsx +35 -0
  30. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  31. package/src/__tests__/renderability_test.jsx +481 -0
  32. package/src/__tests__/renderer-api_test.jsx +309 -0
  33. package/src/__tests__/renderer_test.jsx +1792 -0
  34. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  35. package/src/__tests__/test-items/image-item.js +25 -0
  36. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  37. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  38. package/src/__tests__/test-items/table-item.js +30 -0
  39. package/src/__tests__/tex-wrangler_test.js +83 -0
  40. package/src/__tests__/util_test.js +41 -0
  41. package/src/a11y.js +46 -0
  42. package/src/all-widgets.js +9 -0
  43. package/src/article-renderer.jsx +214 -0
  44. package/src/asset-context.js +30 -0
  45. package/src/basic-widgets.js +13 -0
  46. package/src/components/__stories__/button-group.stories.jsx +61 -0
  47. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  48. package/src/components/__stories__/graph.stories.jsx +27 -0
  49. package/src/components/__stories__/graphie.stories.jsx +32 -0
  50. package/src/components/__stories__/hud.stories.jsx +36 -0
  51. package/src/components/__stories__/icon.stories.jsx +56 -0
  52. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  53. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  54. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  55. package/src/components/__stories__/input-with-examples.stories.jsx +52 -0
  56. package/src/components/__stories__/lint.stories.jsx +40 -0
  57. package/src/components/__stories__/math-input.stories.jsx +29 -0
  58. package/src/components/__stories__/math-output.stories.jsx +24 -0
  59. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  60. package/src/components/__stories__/number-input.stories.jsx +42 -0
  61. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  62. package/src/components/__stories__/range-input.stories.jsx +28 -0
  63. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  64. package/src/components/__stories__/sortable.stories.jsx +73 -0
  65. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  66. package/src/components/__stories__/svg-image.stories.jsx +71 -0
  67. package/src/components/__stories__/tex.stories.jsx +24 -0
  68. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  69. package/src/components/__stories__/text-input.stories.jsx +34 -0
  70. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  71. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  72. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  73. package/src/components/__testdata__/sorter_testdata.js +20 -0
  74. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  75. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  76. package/src/components/__tests__/number-input_test.jsx +112 -0
  77. package/src/components/__tests__/sortable_test.jsx +234 -0
  78. package/src/components/__tests__/sorter_test.jsx +111 -0
  79. package/src/components/__tests__/zoomable_test.jsx +388 -0
  80. package/src/components/button-group.jsx +131 -0
  81. package/src/components/constants.js +15 -0
  82. package/src/components/fixed-to-responsive.jsx +165 -0
  83. package/src/components/graph.jsx +413 -0
  84. package/src/components/graphie-classes.jsx +148 -0
  85. package/src/components/graphie-movables.jsx +342 -0
  86. package/src/components/graphie.jsx +353 -0
  87. package/src/components/highlighting/highlightable-content.jsx +245 -0
  88. package/src/components/highlighting/highlights.js +208 -0
  89. package/src/components/highlighting/ranges.js +299 -0
  90. package/src/components/highlighting/types.js +73 -0
  91. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  92. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  93. package/src/components/highlighting/ui/highlight-tooltip.jsx +143 -0
  94. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  95. package/src/components/highlighting/ui/selection-tracker.jsx +184 -0
  96. package/src/components/highlighting/ui/types.js +21 -0
  97. package/src/components/highlighting/ui/util.js +166 -0
  98. package/src/components/highlighting/word-indexer.jsx +155 -0
  99. package/src/components/hud.jsx +179 -0
  100. package/src/components/icon.jsx +149 -0
  101. package/src/components/image-loader.jsx +175 -0
  102. package/src/components/info-tip/info-tip.jsx +146 -0
  103. package/src/components/info-tip.jsx +38 -0
  104. package/src/components/inline-icon.jsx +81 -0
  105. package/src/components/input-with-examples.jsx +248 -0
  106. package/src/components/lint.jsx +455 -0
  107. package/src/components/math-input.jsx +293 -0
  108. package/src/components/math-output.jsx +128 -0
  109. package/src/components/multi-button-group.jsx +137 -0
  110. package/src/components/number-input.jsx +253 -0
  111. package/src/components/prop-check-box.jsx +80 -0
  112. package/src/components/range-input.jsx +61 -0
  113. package/src/components/simple-keypad-input.jsx +88 -0
  114. package/src/components/sortable.jsx +872 -0
  115. package/src/components/stub-tag-editor.jsx +54 -0
  116. package/src/components/svg-image.jsx +765 -0
  117. package/src/components/tex-buttons.jsx +320 -0
  118. package/src/components/tex.jsx +70 -0
  119. package/src/components/text-input.jsx +133 -0
  120. package/src/components/text-list-editor.jsx +157 -0
  121. package/src/components/tooltip.jsx +392 -0
  122. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  123. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  124. package/src/components/visibility-observer/visibility-observer.js +109 -0
  125. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  126. package/src/components/zoomable-tex.jsx +70 -0
  127. package/src/components/zoomable.jsx +294 -0
  128. package/src/components.js +17 -0
  129. package/src/definition-context.js +59 -0
  130. package/src/dependencies.js +22 -0
  131. package/src/error-boundary.jsx +48 -0
  132. package/src/extra-widgets.js +75 -0
  133. package/src/hint-renderer.jsx +155 -0
  134. package/src/hints-renderer.jsx +367 -0
  135. package/src/icon-paths.js +120 -0
  136. package/src/index.js +159 -0
  137. package/src/init.js +49 -0
  138. package/src/interaction-tracker.js +60 -0
  139. package/src/interactive2/__tests__/movable-mock.js +48 -0
  140. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  141. package/src/interactive2/interactive-util.js +152 -0
  142. package/src/interactive2/movable-helper-methods.js +160 -0
  143. package/src/interactive2/movable-line-options.js +392 -0
  144. package/src/interactive2/movable-line.js +322 -0
  145. package/src/interactive2/movable-point-options.js +190 -0
  146. package/src/interactive2/movable-point.jsx +597 -0
  147. package/src/interactive2/movable-polygon-options.js +351 -0
  148. package/src/interactive2/movable-polygon.js +368 -0
  149. package/src/interactive2/movable.js +273 -0
  150. package/src/interactive2/objective_.js +79 -0
  151. package/src/interactive2/types.js +3 -0
  152. package/src/interactive2/wrapped-defaults.js +86 -0
  153. package/src/interactive2/wrapped-ellipse.js +86 -0
  154. package/src/interactive2/wrapped-line.js +109 -0
  155. package/src/interactive2/wrapped-path.js +47 -0
  156. package/src/interactive2.js +91 -0
  157. package/src/item-renderer.jsx +514 -0
  158. package/src/item-version.js +12 -0
  159. package/src/jipt-paragraphs.js +52 -0
  160. package/src/jquery.mobile.vmouse.js +546 -0
  161. package/src/keypad-context.js +33 -0
  162. package/src/loading-context.js +20 -0
  163. package/src/logging/log.js +107 -0
  164. package/src/logging/log_test.js +41 -0
  165. package/src/mixins/__tests__/widget-prop-denylist_test.js +23 -0
  166. package/src/mixins/changeable.jsx +115 -0
  167. package/src/mixins/editor-jsonify.jsx +13 -0
  168. package/src/mixins/provide-keypad.jsx +131 -0
  169. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  170. package/src/mixins/widget-prop-denylist.js +49 -0
  171. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  172. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  173. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +1220 -0
  174. package/src/multi-items/__tests__/items_test.js +245 -0
  175. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  176. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  177. package/src/multi-items/__tests__/shapes_test.js +44 -0
  178. package/src/multi-items/__tests__/trees_test.js +316 -0
  179. package/src/multi-items/item-types.js +55 -0
  180. package/src/multi-items/items.js +176 -0
  181. package/src/multi-items/multi-renderer.jsx +543 -0
  182. package/src/multi-items/prop-type-builders.js +105 -0
  183. package/src/multi-items/shape-types.js +47 -0
  184. package/src/multi-items/shapes.js +53 -0
  185. package/src/multi-items/tree-types.js +42 -0
  186. package/src/multi-items/trees.js +345 -0
  187. package/src/multi-items.js +76 -0
  188. package/src/perseus-api.jsx +221 -0
  189. package/src/perseus-env.js +26 -0
  190. package/src/perseus-error.js +19 -0
  191. package/src/perseus-markdown.jsx +408 -0
  192. package/src/perseus-types.js +1475 -0
  193. package/src/question-paragraph.jsx +30 -0
  194. package/src/renderability.jsx +108 -0
  195. package/src/renderer.jsx +1937 -0
  196. package/src/server-item-renderer.jsx +467 -0
  197. package/src/sigfigs.jsx +492 -0
  198. package/src/styles/articles.less +747 -0
  199. package/src/styles/constants.js +96 -0
  200. package/src/styles/global-constants.js +344 -0
  201. package/src/styles/global-styles.js +307 -0
  202. package/src/styles/khan-exercise.css +1280 -0
  203. package/src/styles/media-queries.js +48 -0
  204. package/src/styles/perseus-renderer.less +742 -0
  205. package/src/styles/shared.js +132 -0
  206. package/src/styles/util.less +47 -0
  207. package/src/styles/variables.boostrap.less +301 -0
  208. package/src/styles/variables.less +55 -0
  209. package/src/styles/variables.shared.less +346 -0
  210. package/src/styles/widgets/categorizer.less +39 -0
  211. package/src/styles/widgets/dropdown.less +3 -0
  212. package/src/styles/widgets/expression.less +145 -0
  213. package/src/styles/widgets/graded-group.less +36 -0
  214. package/src/styles/widgets/grapher.less +11 -0
  215. package/src/styles/widgets/group.less +19 -0
  216. package/src/styles/widgets/image.less +29 -0
  217. package/src/styles/widgets/interactive-graph.less +61 -0
  218. package/src/styles/widgets/label-image.less +41 -0
  219. package/src/styles/widgets/matcher.less +8 -0
  220. package/src/styles/widgets/matrix.less +122 -0
  221. package/src/styles/widgets/measurer.less +20 -0
  222. package/src/styles/widgets/orderer.less +97 -0
  223. package/src/styles/widgets/passage.less +160 -0
  224. package/src/styles/widgets/plotter.less +29 -0
  225. package/src/styles/widgets/radio.less +56 -0
  226. package/src/styles/widgets/reaction-diagram.less +35 -0
  227. package/src/styles/widgets/sequence.less +5 -0
  228. package/src/styles/widgets/sortable.less +113 -0
  229. package/src/styles/widgets/table.less +56 -0
  230. package/src/styles/widgets/transformer.less +28 -0
  231. package/src/styles/widgets/unit.less +14 -0
  232. package/src/tex-wrangler.js +176 -0
  233. package/src/translation-linter.js +125 -0
  234. package/src/traversal.jsx +141 -0
  235. package/src/types.js +500 -0
  236. package/src/util/a11y.js +24 -0
  237. package/src/util/answer-types.js +861 -0
  238. package/src/util/answer-types_test.js +115 -0
  239. package/src/util/colors.js +94 -0
  240. package/src/util/fix-passage-refs.jsx +122 -0
  241. package/src/util/graph-utils.js +16 -0
  242. package/src/util/graphie.js +1586 -0
  243. package/src/util/interactive.js +4078 -0
  244. package/src/util/katex-preprocess.js +16 -0
  245. package/src/util/math.js +289 -0
  246. package/src/util/mobile-native-utils.js +14 -0
  247. package/src/util/mobile-native-utils_test.js +24 -0
  248. package/src/util/react-render.js +27 -0
  249. package/src/util/register-all-widgets-for-testing.js +12 -0
  250. package/src/util/scroll-utils.js +162 -0
  251. package/src/util/sizing-utils.js +46 -0
  252. package/src/util/tex.js +203 -0
  253. package/src/util/url-utils.js +36 -0
  254. package/src/util.js +984 -0
  255. package/src/version.js +11 -0
  256. package/src/widget-container.jsx +179 -0
  257. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  258. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  259. package/src/widgets/__stories__/definition.stories.jsx +108 -0
  260. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  261. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  262. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  263. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  264. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  265. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  266. package/src/widgets/__stories__/group.stories.jsx +15 -0
  267. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  268. package/src/widgets/__stories__/image.stories.jsx +62 -0
  269. package/src/widgets/__stories__/input-number.stories.jsx +102 -0
  270. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  271. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  272. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  273. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  274. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  275. package/src/widgets/__stories__/numeric-input.stories.jsx +106 -0
  276. package/src/widgets/__stories__/on-screen-keypad.stories.jsx +118 -0
  277. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  278. package/src/widgets/__stories__/passage-ref.stories.jsx +23 -0
  279. package/src/widgets/__stories__/passage.stories.jsx +27 -0
  280. package/src/widgets/__stories__/radio.stories.jsx +90 -0
  281. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  282. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  283. package/src/widgets/__stories__/unit.stories.jsx +45 -0
  284. package/src/widgets/__stories__/video-transcript-link.stories.jsx +24 -0
  285. package/src/widgets/__stories__/video.stories.jsx +15 -0
  286. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  287. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  288. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  289. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  290. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  291. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  292. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  293. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  294. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  295. package/src/widgets/__testdata__/group_testdata.js +156 -0
  296. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  297. package/src/widgets/__testdata__/image_testdata.js +38 -0
  298. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  299. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  300. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  301. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  302. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  303. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  304. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  305. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  306. package/src/widgets/__testdata__/passage-ref_testdata.js +103 -0
  307. package/src/widgets/__testdata__/passage_testdata.js +96 -0
  308. package/src/widgets/__testdata__/radio_testdata.js +262 -0
  309. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  310. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  311. package/src/widgets/__testdata__/video_testdata.js +22 -0
  312. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +996 -0
  313. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  314. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +105 -0
  315. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +157 -0
  316. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  317. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  318. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +757 -0
  319. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +320 -0
  320. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +865 -0
  321. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2897 -0
  322. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1274 -0
  323. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  324. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +404 -0
  325. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  326. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  327. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +3017 -0
  328. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  329. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +522 -0
  330. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  331. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +488 -0
  332. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  333. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  334. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  335. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +4505 -0
  336. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +161 -0
  337. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  338. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  339. package/src/widgets/__tests__/definition_test.js +137 -0
  340. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  341. package/src/widgets/__tests__/explanation_test.js +199 -0
  342. package/src/widgets/__tests__/expression_test.jsx +439 -0
  343. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +32 -0
  344. package/src/widgets/__tests__/graded-group-set_test.jsx +290 -0
  345. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  346. package/src/widgets/__tests__/grapher.cypress.jsx +546 -0
  347. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  348. package/src/widgets/__tests__/group_test.jsx +512 -0
  349. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  350. package/src/widgets/__tests__/image_test.jsx +65 -0
  351. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  352. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  353. package/src/widgets/__tests__/interactive-graph.cypress.jsx +777 -0
  354. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  355. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  356. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  357. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  358. package/src/widgets/__tests__/numeric-input_test.jsx +514 -0
  359. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  360. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  361. package/src/widgets/__tests__/passage_test.jsx +212 -0
  362. package/src/widgets/__tests__/radio/base-radio_test.jsx +443 -0
  363. package/src/widgets/__tests__/radio/choice-icon_test.jsx +91 -0
  364. package/src/widgets/__tests__/radio/choice_test.jsx +311 -0
  365. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  366. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  367. package/src/widgets/__tests__/radio_test.jsx +810 -0
  368. package/src/widgets/__tests__/renderQuestion.jsx +66 -0
  369. package/src/widgets/__tests__/unit_test.jsx +144 -0
  370. package/src/widgets/__tests__/video_test.jsx +55 -0
  371. package/src/widgets/categorizer.jsx +343 -0
  372. package/src/widgets/cs-program.jsx +227 -0
  373. package/src/widgets/definition.jsx +121 -0
  374. package/src/widgets/dropdown.jsx +141 -0
  375. package/src/widgets/explanation.jsx +315 -0
  376. package/src/widgets/expression.jsx +652 -0
  377. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  378. package/src/widgets/graded-group-set.jsx +296 -0
  379. package/src/widgets/graded-group.jsx +453 -0
  380. package/src/widgets/grapher/util.jsx +806 -0
  381. package/src/widgets/grapher.jsx +663 -0
  382. package/src/widgets/group.jsx +195 -0
  383. package/src/widgets/iframe.jsx +198 -0
  384. package/src/widgets/image.jsx +310 -0
  385. package/src/widgets/input-number.jsx +515 -0
  386. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  387. package/src/widgets/interaction/color-picker.jsx +69 -0
  388. package/src/widgets/interaction/dash-picker.jsx +46 -0
  389. package/src/widgets/interaction/element-container.jsx +97 -0
  390. package/src/widgets/interaction.jsx +742 -0
  391. package/src/widgets/interactive-graph.jsx +2820 -0
  392. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  393. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  394. package/src/widgets/label-image/answer-choices.jsx +311 -0
  395. package/src/widgets/label-image/marker.jsx +365 -0
  396. package/src/widgets/label-image/types.js +33 -0
  397. package/src/widgets/label-image.jsx +841 -0
  398. package/src/widgets/label-image_test.js +840 -0
  399. package/src/widgets/lights-puzzle.jsx +356 -0
  400. package/src/widgets/matcher.jsx +274 -0
  401. package/src/widgets/matrix.jsx +632 -0
  402. package/src/widgets/measurer.jsx +215 -0
  403. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  404. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  405. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  406. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  407. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  408. package/src/widgets/molecule.jsx +172 -0
  409. package/src/widgets/number-line.jsx +858 -0
  410. package/src/widgets/numeric-input.jsx +553 -0
  411. package/src/widgets/orderer.jsx +756 -0
  412. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +373 -0
  413. package/src/widgets/passage/passage-markdown.jsx +472 -0
  414. package/src/widgets/passage-ref-target.jsx +73 -0
  415. package/src/widgets/passage-ref.jsx +214 -0
  416. package/src/widgets/passage.jsx +576 -0
  417. package/src/widgets/plotter.jsx +1198 -0
  418. package/src/widgets/radio/__stories__/base-radio.stories.jsx +189 -0
  419. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +166 -0
  420. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +41 -0
  421. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  422. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +44 -0
  423. package/src/widgets/radio/__stories__/option-status.stories.jsx +104 -0
  424. package/src/widgets/radio/base-radio.jsx +494 -0
  425. package/src/widgets/radio/choice-a11y-text.js +62 -0
  426. package/src/widgets/radio/choice-icon/choice-icon.jsx +214 -0
  427. package/src/widgets/radio/choice-icon/cross-out-line.jsx +52 -0
  428. package/src/widgets/radio/choice-icon/shared-styles.js +16 -0
  429. package/src/widgets/radio/choice-none-above.jsx +63 -0
  430. package/src/widgets/radio/choice.jsx +382 -0
  431. package/src/widgets/radio/choice.jsx.fixture.js +46 -0
  432. package/src/widgets/radio/focus-ring.jsx +58 -0
  433. package/src/widgets/radio/option-status.jsx +98 -0
  434. package/src/widgets/radio/util.js +32 -0
  435. package/src/widgets/radio/util_test.js +19 -0
  436. package/src/widgets/radio/widget.jsx +515 -0
  437. package/src/widgets/radio.jsx +155 -0
  438. package/src/widgets/reaction-diagram.jsx +137 -0
  439. package/src/widgets/sequence.jsx +143 -0
  440. package/src/widgets/simulator.jsx +806 -0
  441. package/src/widgets/sorter.jsx +149 -0
  442. package/src/widgets/table.jsx +397 -0
  443. package/src/widgets/transformer.jsx +2876 -0
  444. package/src/widgets/unit.jsx +372 -0
  445. package/src/widgets/video-transcript-link.jsx +84 -0
  446. package/src/widgets/video-transcript-link_test.js +211 -0
  447. package/src/widgets/video.jsx +126 -0
  448. package/src/widgets.js +431 -0
  449. package/src/zoom.js +539 -0
@@ -0,0 +1,553 @@
1
+ /* eslint-disable react/prop-types */
2
+ // @flow
3
+
4
+ import {linterContextDefault} from "@khanacademy/perseus-linter";
5
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
6
+ import {StyleSheet} from "aphrodite";
7
+ import * as React from "react";
8
+ import _ from "underscore";
9
+
10
+ import InputWithExamples from "../components/input-with-examples.jsx";
11
+ import SimpleKeypadInput from "../components/simple-keypad-input.jsx";
12
+ import {ApiOptions} from "../perseus-api.jsx";
13
+ import TexWrangler from "../tex-wrangler.js";
14
+ import KhanAnswerTypes from "../util/answer-types.js";
15
+ import KhanMath from "../util/math.js";
16
+
17
+ import type {
18
+ PerseusNumericInputAnswer,
19
+ PerseusNumericInputWidgetOptions,
20
+ PerseusNumericInputAnswerForm,
21
+ } from "../perseus-types";
22
+ import type {
23
+ FocusPath,
24
+ PerseusScore,
25
+ WidgetExports,
26
+ WidgetProps,
27
+ } from "../types";
28
+
29
+ const ParseTex = TexWrangler.parseTex;
30
+
31
+ const answerFormButtons = [
32
+ {title: "Integers", value: "integer", content: "6"},
33
+ {title: "Decimals", value: "decimal", content: "0.75"},
34
+ {title: "Proper fractions", value: "proper", content: "\u2157"},
35
+ {
36
+ title: "Improper fractions",
37
+ value: "improper",
38
+ content: "\u2077\u2044\u2084",
39
+ },
40
+ {title: "Mixed numbers", value: "mixed", content: "1\u00BE"},
41
+ {title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
42
+ ];
43
+
44
+ const formExamples: {[string]: (PerseusNumericInputAnswerForm) => string} = {
45
+ integer: () => i18n._("an integer, like $6$"),
46
+ proper: (form) =>
47
+ form.simplify === "optional"
48
+ ? i18n._("a *proper* fraction, like $1/2$ or $6/10$")
49
+ : i18n._("a *simplified proper* fraction, like $3/5$"),
50
+ improper: (form) =>
51
+ form.simplify === "optional"
52
+ ? i18n._("an *improper* fraction, like $10/7$ or $14/8$")
53
+ : i18n._("a *simplified improper* fraction, like $7/4$"),
54
+ mixed: () => i18n._("a mixed number, like $1\\ 3/4$"),
55
+ decimal: () => i18n._("an *exact* decimal, like $0.75$"),
56
+ pi: () =>
57
+ i18n._(
58
+ "a multiple of pi, like $12\\ \\text{pi}$ or " +
59
+ "$2/3\\ \\text{pi}$",
60
+ ),
61
+ };
62
+
63
+ type UserInput = {|
64
+ currentValue: string,
65
+ |};
66
+
67
+ export type Rubric = PerseusNumericInputWidgetOptions;
68
+
69
+ type ExternalProps = WidgetProps<PerseusNumericInputWidgetOptions, Rubric>;
70
+
71
+ type Props = {|
72
+ ...ExternalProps,
73
+ size: $NonMaybeType<ExternalProps["size"]>,
74
+ rightAlign: $NonMaybeType<ExternalProps["rightAlign"]>,
75
+ apiOptions: $NonMaybeType<ExternalProps["apiOptions"]>,
76
+ coefficient: $NonMaybeType<ExternalProps["coefficient"]>,
77
+ answerForms: $NonMaybeType<ExternalProps["answerForms"]>,
78
+ labelText: $NonMaybeType<ExternalProps["labelText"]>,
79
+ linterContext: $NonMaybeType<ExternalProps["linterContext"]>,
80
+ multipleNumberInput: $NonMaybeType<ExternalProps["multipleNumberInput"]>,
81
+ currentValue: string,
82
+ |};
83
+
84
+ type DefaultProps = {|
85
+ currentValue: Props["currentValue"],
86
+ size: Props["size"],
87
+ rightAlign: Props["rightAlign"],
88
+ apiOptions: Props["apiOptions"],
89
+ coefficient: Props["coefficient"],
90
+ answerForms: Props["answerForms"],
91
+ labelText: Props["labelText"],
92
+ linterContext: Props["linterContext"],
93
+ multipleNumberInput: Props["multipleNumberInput"],
94
+ |};
95
+
96
+ type State = {|
97
+ // keeps track of the other set of values when switching
98
+ // between 0 and finite solutions
99
+ previousValues: $ReadOnlyArray<string>,
100
+ |};
101
+
102
+ export class NumericInput extends React.Component<Props, State> {
103
+ inputRef: ?(SimpleKeypadInput | InputWithExamples);
104
+
105
+ static defaultProps: DefaultProps = {
106
+ currentValue: "",
107
+ size: "normal",
108
+ rightAlign: false,
109
+ apiOptions: ApiOptions.defaults,
110
+ coefficient: false,
111
+ answerForms: [],
112
+ labelText: "",
113
+ linterContext: linterContextDefault,
114
+ multipleNumberInput: false,
115
+ };
116
+
117
+ static getUserInputFromProps(props: Props): UserInput {
118
+ return {
119
+ currentValue: props.currentValue,
120
+ };
121
+ }
122
+
123
+ static getOneCorrectAnswerFromRubric(rubric: Rubric): ?string {
124
+ const correctAnswers = rubric.answers.filter(
125
+ (answer) => answer.status === "correct",
126
+ );
127
+ const answerStrings = correctAnswers.map((answer) => {
128
+ // Figure out how this answer is supposed to be
129
+ // displayed
130
+ let format = "decimal";
131
+ if (answer.answerForms && answer.answerForms[0]) {
132
+ // NOTE(johnsullivan): This isn't exactly ideal, but
133
+ // it does behave well for all the currently known
134
+ // problems. See D14742 for some discussion on
135
+ // alternate strategies.
136
+ format = answer.answerForms[0];
137
+ }
138
+
139
+ let answerString = KhanMath.toNumericString(answer.value, format);
140
+ if (answer.maxError) {
141
+ answerString +=
142
+ " \u00B1 " +
143
+ KhanMath.toNumericString(answer.maxError, format);
144
+ }
145
+ return answerString;
146
+ });
147
+ if (answerStrings.length === 0) {
148
+ return;
149
+ }
150
+ return answerStrings[0];
151
+ }
152
+
153
+ static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
154
+ const allAnswerForms = answerFormButtons.map((e) => e["value"]);
155
+
156
+ const createValidator = (answer: PerseusNumericInputAnswer) => {
157
+ const stringAnswer = `${answer.value}`;
158
+ return KhanAnswerTypes.number.createValidatorFunctional(
159
+ stringAnswer,
160
+ {
161
+ message: answer.message,
162
+ simplify:
163
+ answer.status === "correct"
164
+ ? answer.simplify
165
+ : "optional",
166
+ inexact: true, // TODO(merlob) backfill / delete
167
+ maxError: answer.maxError,
168
+ forms:
169
+ answer.strict &&
170
+ answer.answerForms &&
171
+ answer.answerForms.length !== 0
172
+ ? answer.answerForms
173
+ : allAnswerForms,
174
+ },
175
+ );
176
+ };
177
+
178
+ // We may have received TeX; try to parse it before grading.
179
+ // If `currentValue` is not TeX, this should be a no-op.
180
+ const currentValue = ParseTex(useInput.currentValue);
181
+ const correctAnswers = rubric.answers.filter(
182
+ (answer) => answer.status === "correct",
183
+ );
184
+
185
+ const normalizedAnswerExpected = correctAnswers.every(
186
+ (answer) => Math.abs(answer.value) <= 1,
187
+ );
188
+
189
+ // Look through all correct answers for one that matches either
190
+ // precisely or approximately and return the appropriate message:
191
+ // - if precise, return the message that the answer came with
192
+ // - if it needs to be simplified, etc., show that message
193
+ let result = correctAnswers
194
+ .map((answer) => {
195
+ // The coefficient is an attribute of the widget
196
+ let localValue = currentValue;
197
+ if (rubric.coefficient) {
198
+ if (!localValue) {
199
+ localValue = 1;
200
+ } else if (localValue === "-") {
201
+ localValue = -1;
202
+ }
203
+ }
204
+ const validate = createValidator(answer);
205
+ return validate(
206
+ maybeParsePercentInput(
207
+ localValue,
208
+ normalizedAnswerExpected,
209
+ ),
210
+ );
211
+ })
212
+ .find((match) => match.correct || match.empty);
213
+
214
+ if (!result) {
215
+ // Otherwise, if the guess is not correct
216
+ const otherAnswers = [].concat(
217
+ rubric.answers.filter((answer) => answer.status === "ungraded"),
218
+ rubric.answers.filter((answer) => answer.status === "wrong"),
219
+ );
220
+
221
+ // Look through all other answers and if one matches either
222
+ // precisely or approximately return the answer's message
223
+ const match = otherAnswers.find((answer) => {
224
+ const validate = createValidator(answer);
225
+ return validate(
226
+ maybeParsePercentInput(
227
+ currentValue,
228
+ normalizedAnswerExpected,
229
+ ),
230
+ ).correct;
231
+ });
232
+ result = {
233
+ empty: match ? match.status === "ungraded" : false,
234
+ correct: match ? match.status === "correct" : false,
235
+ message: match ? match.message : null,
236
+ guess: currentValue,
237
+ };
238
+ }
239
+
240
+ // TODO(eater): Seems silly to translate result to this
241
+ // invalid/points thing and immediately translate it
242
+ // back in ItemRenderer.scoreInput()
243
+ if (result.empty) {
244
+ return {
245
+ type: "invalid",
246
+ message: result.message,
247
+ };
248
+ }
249
+ return {
250
+ type: "points",
251
+ earned: result.correct ? 1 : 0,
252
+ total: 1,
253
+ message: result.message,
254
+ };
255
+ }
256
+
257
+ state: State = {
258
+ // keeps track of the other set of values when switching
259
+ // between 0 and finite solutions
260
+ previousValues: [""],
261
+ };
262
+
263
+ // TODO(Nicole, Jeremy): This is maybe never used and should be removed
264
+ examples: () => $ReadOnlyArray<string> = () => {
265
+ // if the set of specified forms are empty, allow all forms
266
+ const forms =
267
+ this.props.answerForms?.length !== 0
268
+ ? this.props.answerForms
269
+ : Object.keys(formExamples).map((name) => {
270
+ return {
271
+ name: name,
272
+ simplify: "required",
273
+ };
274
+ });
275
+
276
+ let examples = _.map(forms, (form) => {
277
+ return formExamples[form.name](form);
278
+ });
279
+ // Ensure no duplicate tooltip text from simplified and unsimplified
280
+ // versions of the same format
281
+ examples = _.uniq(examples);
282
+
283
+ return [i18n._("**Your answer should be** ")].concat(examples);
284
+ };
285
+
286
+ shouldShowExamples: () => boolean = () => {
287
+ const noFormsAccepted = this.props.answerForms?.length === 0;
288
+ // To check if all answer forms are accepted, we must first
289
+ // find the *names* of all accepted forms, and see if they are
290
+ // all present, ignoring duplicates
291
+ const answerFormNames: $ReadOnlyArray<string> = _.uniq(
292
+ this.props.answerForms?.map((form) => form.name),
293
+ );
294
+ const allFormsAccepted =
295
+ answerFormNames.length >= Object.keys(formExamples).length;
296
+ return !noFormsAccepted && !allFormsAccepted;
297
+ };
298
+
299
+ simpleValidate: (Rubric) => PerseusScore = (rubric) => {
300
+ return NumericInput.validate(this.getUserInput(), rubric);
301
+ };
302
+
303
+ focus: () => boolean = () => {
304
+ this.inputRef?.focus();
305
+ return true;
306
+ };
307
+
308
+ focusInputPath: () => void = () => {
309
+ this.inputRef?.focus();
310
+ };
311
+
312
+ blurInputPath: () => void = () => {
313
+ this.inputRef?.blur();
314
+ };
315
+
316
+ getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
317
+ // The widget itself is an input, so we return a single empty list to
318
+ // indicate this.
319
+ /* c8 ignore next */
320
+ return [[]];
321
+ };
322
+
323
+ getGrammarTypeForPath: (FocusPath) => string = (inputPath) => {
324
+ /* c8 ignore next */
325
+ return "number";
326
+ };
327
+
328
+ setInputValue: (FocusPath, string, ?() => mixed) => void = (
329
+ path,
330
+ newValue,
331
+ cb,
332
+ ) => {
333
+ /* c8 ignore next */
334
+ this.props.onChange(
335
+ {
336
+ currentValue: newValue,
337
+ },
338
+ cb,
339
+ );
340
+ };
341
+
342
+ getUserInput: () => UserInput = () => {
343
+ return NumericInput.getUserInputFromProps(this.props);
344
+ };
345
+
346
+ handleChange: (string, ?() => mixed) => void = (newValue, cb) => {
347
+ this.props.onChange({currentValue: newValue}, cb);
348
+ this.props.trackInteraction();
349
+ };
350
+
351
+ _getInputType: () => "tex" | "text" = () => {
352
+ if (this.props.apiOptions.staticRender) {
353
+ return "tex";
354
+ }
355
+ return "text";
356
+ };
357
+
358
+ _handleFocus: () => void = () => {
359
+ this.props.onFocus([]);
360
+ // HACK(kevinb): We want to dismiss the feedback popover that webapp
361
+ // displays as soon as a user clicks in in the input field so we call
362
+ // interactionCallback directly.
363
+ const {interactionCallback} = this.props.apiOptions;
364
+ if (interactionCallback) {
365
+ interactionCallback();
366
+ }
367
+ };
368
+
369
+ _handleBlur: () => void = () => {
370
+ this.props.onBlur([]);
371
+ };
372
+
373
+ render(): React.Node {
374
+ let labelText = this.props.labelText;
375
+ if (labelText == null || labelText === "") {
376
+ labelText = i18n._("Your answer:");
377
+ }
378
+
379
+ // To right align a custom keypad we need to wrap it.
380
+ const maybeRightAlignKeypadInput = (keypadInput) => {
381
+ return this.props.rightAlign ? (
382
+ <div className="perseus-input-right-align">{keypadInput}</div>
383
+ ) : (
384
+ keypadInput
385
+ );
386
+ };
387
+
388
+ if (this.props.apiOptions.customKeypad) {
389
+ // TODO(charlie): Support "Review Mode".
390
+ return maybeRightAlignKeypadInput(
391
+ <SimpleKeypadInput
392
+ ref={(ref) => (this.inputRef = ref)}
393
+ value={this.props.currentValue}
394
+ keypadElement={this.props.keypadElement}
395
+ onChange={this.handleChange}
396
+ onFocus={this._handleFocus}
397
+ onBlur={this._handleBlur}
398
+ />,
399
+ );
400
+ }
401
+
402
+ // Note: This is _very_ similar to what `input-number.jsx` does. If
403
+ // you modify this, double-check if you also need to modify that
404
+ // component.
405
+ const styles = StyleSheet.create({
406
+ input: {
407
+ textAlign: this.props.rightAlign ? "right" : "left",
408
+ width: this.props.size === "small" ? 40 : 80,
409
+ padding: 0,
410
+ height: "auto",
411
+ },
412
+ });
413
+
414
+ return (
415
+ <div>
416
+ <InputWithExamples
417
+ ref={(ref) => (this.inputRef = ref)}
418
+ value={this.props.currentValue}
419
+ onChange={this.handleChange}
420
+ labelText={labelText}
421
+ type={this._getInputType()}
422
+ examples={this.examples()}
423
+ shouldShowExamples={this.shouldShowExamples()}
424
+ onFocus={this._handleFocus}
425
+ onBlur={this._handleBlur}
426
+ id={this.props.widgetId}
427
+ disabled={this.props.apiOptions.readOnly}
428
+ style={styles.input}
429
+ />
430
+ </div>
431
+ );
432
+ }
433
+ }
434
+
435
+ // TODO(thomas): Currently we receive a list of lists of acceptable answer types
436
+ // and union them down into a single set. It's worth considering whether it
437
+ // wouldn't make more sense to have a single set of acceptable answer types for
438
+ // a given *problem* rather than for each possible [correct/wrong] *answer*.
439
+ // When should two answers to a problem take different answer types?
440
+ // See D27790 for more discussion.
441
+ export const unionAnswerForms: (
442
+ $ReadOnlyArray<$ReadOnlyArray<PerseusNumericInputAnswerForm>>,
443
+ ) => $ReadOnlyArray<PerseusNumericInputAnswerForm> = function (
444
+ answerFormsList,
445
+ ) {
446
+ // Takes a list of lists of answer forms, and returns a list of the forms
447
+ // in each of these lists in the same order that they're listed in the
448
+ // `formExamples` forms from above.
449
+
450
+ // uniqueBy takes a list of elements and a function which compares whether
451
+ // two elements are equal, and returns a list of unique elements. This is
452
+ // just a helper function here, but works generally.
453
+ const uniqueBy = function (list, iteratee) {
454
+ return list.reduce((uniqueList, element) => {
455
+ // For each element, decide whether it's already in the list of
456
+ // unique items.
457
+ const inList = _.find(uniqueList, iteratee.bind(null, element));
458
+ if (inList) {
459
+ return uniqueList;
460
+ }
461
+ return uniqueList.concat([element]);
462
+ }, []);
463
+ };
464
+
465
+ // Pull out all of the forms from the different lists.
466
+ const allForms = answerFormsList.flat();
467
+ // Pull out the unique forms using uniqueBy.
468
+ const uniqueForms = uniqueBy(allForms, _.isEqual);
469
+ // Sort them by the order they appear in the `formExamples` list.
470
+ const formExampleKeys = Object.keys(formExamples);
471
+ return _.sortBy(uniqueForms, (form) => {
472
+ return formExampleKeys.indexOf(form.name);
473
+ });
474
+ };
475
+
476
+ export type RenderProps = {|
477
+ answerForms: $ReadOnlyArray<{|
478
+ simplify: ?("required" | "correct" | "enforced"),
479
+ name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi",
480
+ |}>,
481
+ labelText: string,
482
+ size: "normal" | "small",
483
+ coefficient: boolean,
484
+ rightAlign?: boolean,
485
+ static: boolean,
486
+ |};
487
+
488
+ // This function checks if the user inputted a percent value, parsing
489
+ // it as a number (and maybe scaling) so that it can be graded.
490
+ // NOTE(michaelpolyak): Unlike `KhanAnswerTypes.number.percent()` which
491
+ // can accept several input forms with or without "%", the decision
492
+ // to parse based on the presence of "%" in the input, is so that we
493
+ // don't accidently scale the user typed value before grading, CP-930.
494
+ export function maybeParsePercentInput(
495
+ inputValue: string | number,
496
+ normalizedAnswerExpected: boolean,
497
+ ): string | number {
498
+ // If the input value is not a string ending with "%", then there's
499
+ // nothing more to do. The value will be graded as inputted by user.
500
+ if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
501
+ return inputValue;
502
+ }
503
+
504
+ const value = parseFloat(inputValue.slice(0, -1));
505
+ // If the input value stripped of the "%" cannot be parsed as a
506
+ // number (the slice is not really necessary for parseFloat to work
507
+ // if the string starts with a number) then return the original
508
+ // input for grading.
509
+ if (isNaN(value)) {
510
+ return inputValue;
511
+ }
512
+
513
+ // Next, if all correct answers are in the range of |0,1| then we
514
+ // scale the user typed value. We assume this is the correct thing
515
+ // to do since the input value ends with "%".
516
+ if (normalizedAnswerExpected) {
517
+ return value / 100;
518
+ }
519
+
520
+ // Otherwise, we return input valuåe (number) stripped of the "%".
521
+ return value;
522
+ }
523
+
524
+ const propsTransform = function (
525
+ widgetOptions: PerseusNumericInputWidgetOptions,
526
+ ): RenderProps {
527
+ const rendererProps = _.extend(_.omit(widgetOptions, "answers"), {
528
+ answerForms: unionAnswerForms(
529
+ // Pull out the name of each form and whether that form has
530
+ // required simplification.
531
+ widgetOptions.answers.map((answer) => {
532
+ return _.map(answer.answerForms, (form) => {
533
+ return {
534
+ simplify: answer.simplify,
535
+ name: form,
536
+ };
537
+ });
538
+ }),
539
+ ),
540
+ });
541
+
542
+ return rendererProps;
543
+ };
544
+
545
+ export default ({
546
+ name: "numeric-input",
547
+ displayName: "Number text box",
548
+ defaultAlignment: "inline-block",
549
+ accessible: true,
550
+ widget: NumericInput,
551
+ transform: propsTransform,
552
+ isLintable: true,
553
+ }: WidgetExports<typeof NumericInput>);